diff --git a/packages/cli/src/instance.mjs b/packages/cli/src/instance.mjs index a6bdf1f5..75463f31 100644 --- a/packages/cli/src/instance.mjs +++ b/packages/cli/src/instance.mjs @@ -1,5 +1,4 @@ import { randomUUID } from "node:crypto"; -import postgres from "postgres"; export async function bootstrapInstance(flags, options = {}) { if (flags.help) { @@ -28,7 +27,11 @@ export async function bootstrapInstance(flags, options = {}) { return failure(flags, "--github-account-type must be organization or user"); const targetType = input.githubAccountType === "user" ? "User" : "Organization"; - const sql = (options.postgres ?? postgres)(databaseUrl, { max: 1 }); + // Imported here rather than at module scope: `facility init` and `doctor` are + // documented as runnable straight from a checkout, where dependencies are not + // installed, and `instance bootstrap` is the only command that needs a driver. + const driver = options.postgres ?? (await import("postgres")).default; + const sql = driver(databaseUrl, { max: 1 }); try { const result = await sql.begin(async (tx) => { await tx`SELECT pg_advisory_xact_lock(hashtext('facility-instance-bootstrap'))`; diff --git a/packages/cli/test/plain-checkout.test.mjs b/packages/cli/test/plain-checkout.test.mjs new file mode 100644 index 00000000..ce1a4ef1 --- /dev/null +++ b/packages/cli/test/plain-checkout.test.mjs @@ -0,0 +1,66 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import { cpSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +// README documents running the installer straight from a checkout: +// +// git clone https://github.com/theam/facility.git /absolute/path/to/facility +// cd your-repository +// node /absolute/path/to/facility/packages/cli/bin/facility.mjs init +// +// There is no install step, and the installer is described as not requiring +// Facility to be deployed at all. Copying the published files somewhere with no +// node_modules reproduces that: the CLI depends on `postgres` and nothing else, +// so any command that eagerly loads it dies here with ERR_MODULE_NOT_FOUND. +function plainCheckout() { + const dir = mkdtempSync(join(tmpdir(), "facility-plain-")); + for (const entry of ["bin", "src", "templates", "modules", "package.json"]) { + cpSync(join(pkgRoot, entry), join(dir, entry), { recursive: true }); + } + return join(dir, "bin", "facility.mjs"); +} + +function targetRepo() { + const dir = mkdtempSync(join(tmpdir(), "facility-target-")); + execFileSync("git", ["init", "-b", "main"], { cwd: dir }); + writeFileSync( + join(dir, "package.json"), + `${JSON.stringify({ name: "demo-app", private: true, scripts: { test: "vitest run" } }, null, 2)}\n`, + ); + writeFileSync(join(dir, "package-lock.json"), "{}\n"); + return dir; +} + +test("informational commands run from a checkout with no dependencies installed", () => { + const cli = plainCheckout(); + for (const args of [["--version"], ["--help"]]) { + const result = spawnSync(process.execPath, [cli, ...args], { encoding: "utf8" }); + assert.doesNotMatch( + `${result.stdout}${result.stderr}`, + /ERR_MODULE_NOT_FOUND|Cannot find package/, + `facility ${args.join(" ")} must not require an installed dependency`, + ); + assert.equal(result.status, 0, `${result.stdout}${result.stderr}`); + } +}); + +test("init installs the method from a checkout with no dependencies installed", () => { + const cli = plainCheckout(); + const dir = targetRepo(); + const result = spawnSync(process.execPath, [cli, "init", "--yes", `--dir=${dir}`], { + cwd: dir, + encoding: "utf8", + }); + assert.doesNotMatch( + `${result.stdout}${result.stderr}`, + /ERR_MODULE_NOT_FOUND|Cannot find package/, + "facility init must not require an installed dependency", + ); + assert.equal(result.status, 0, `${result.stdout}${result.stderr}`); +});