@fourier-labs/harbour 0.1.38 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -186,7 +186,7 @@ The person you are working with may not be a developer. They say what they want
186
186
 
187
187
  ## Getting ready (do this yourself, once per machine and folder)
188
188
 
189
- 1. CLI, before \`harbour init\` or anything else: compare the installed CLI with the current release and upgrade it whenever it is behind — a CLI that merely runs is not good enough, only a current one is. \`harbour agent-setup\` prints both versions and says plainly when the installed one is missing or older (its \`--json\` result carries \`cli.upgradeRequired\` and \`cli.remediation\`); without that output, compare \`harbour --version\` with \`npx -y @fourier-labs/harbour --version\` yourself. If either says the installed one is missing or older, run \`npm i -g @fourier-labs/harbour\` and confirm \`harbour --version\` now matches, then continue. Every later command runs the *installed* CLI, so a stale one builds an app that passes every local check and is then refused by the deployment pipeline (\`kit_bundle_incompatible\`) minutes later, with nothing in the app to fix. The kit needs Node 22+ on a supported Mac; it installs and runs its local database, files and gateway without Docker.
189
+ 1. CLI, before \`harbour init\` or anything else: compare the installed CLI with the current release and upgrade it whenever it is behind — a CLI that merely runs is not good enough, only a current one is. \`harbour agent-setup\` prints both versions and says plainly when the installed one is missing or older (its \`--json\` result carries \`cli.upgradeRequired\` and \`cli.remediation\`); without that output, compare \`harbour --version\` with \`npx -y @fourier-labs/harbour --version\` yourself. If either says the installed one is missing or older, run \`npm i -g @fourier-labs/harbour\` and confirm \`harbour --version\` now matches, then continue. Every later command runs the *installed* CLI, so a stale one builds an app that passes every local check and is then refused by the deployment pipeline (\`kit_bundle_incompatible\`) minutes later, with nothing in the app to fix. The kit needs Node 22+ on macOS Apple silicon, Linux x64 or Windows x64; it installs and runs its local database, files and gateway without Docker.
190
190
  2. Folder: if the current folder has no \`.harbour/\` directory, run \`harbour init --app-root .\` — an empty folder gets a small starter app, an existing Vite + React app gets the kit files added and nothing overwritten. Then read the "Harbour development kit" block in CLAUDE.md / AGENTS.md; it holds the per-app rules.
191
191
  3. Sign-in, needed only for company systems and shipping: run \`harbour login\`. It opens the browser and the person finishes the sign-in there — the one step they do themselves; tell them so in one line. If login says the company is not connected yet, ask them for the Harbour link their IT/admin gave them and run \`harbour connect <link>\` first.
192
192
 
@@ -4,7 +4,7 @@ import { readAppSchema } from "./app-schema.js";
4
4
  import { describeRetainedChecks, syncRetainedChecks } from "./retained-checks.js";
5
5
  import { CliError } from "./output.js";
6
6
  import { DEPENDENT_READ_OPERATIONS, READ_OPERATIONS, kitPaths, readDeclaration, readKitLock, resourceNames, sourceDigest } from "./kit.js";
7
- import { LocalRuntime, allocatePorts, ensureSdk, freePort, readDevLock, runCommand, runningOrigin } from "./local-runtime.js";
7
+ import { LocalRuntime, allocatePorts, ensureSdk, freePort, nodePackageCommand, readDevLock, runCommand, runningOrigin } from "./local-runtime.js";
8
8
  import { CLI_VERSION } from "./version.js";
9
9
  /** The name the gate reports operation coverage under — the pipeline's rule for it. */
10
10
  export const FLOW_CHECK = "flow.check-failed";
@@ -24,9 +24,11 @@ export async function runChecks(root, options) {
24
24
  const previous = await readReport(root);
25
25
  if (previous && previous.sourceDigest !== (await sourceDigest(root)).digest)
26
26
  output("Source changed since the last report; the previous report is no longer valid.");
27
- const typecheck = await run("npx", ["tsc", "--noEmit"], { cwd: root, quiet: true });
27
+ const npx = nodePackageCommand("npx", ["tsc", "--noEmit"]);
28
+ const typecheck = await run(npx.command, npx.args, { cwd: root, quiet: true });
28
29
  record({ name: "typecheck", status: typecheck.code === 0 ? "pass" : "fail", ...(typecheck.code === 0 ? {} : { detail: lastLines(typecheck.stdout || typecheck.stderr) }) });
29
- const build = await run("npm", ["run", "build"], { cwd: root, quiet: true });
30
+ const npm = nodePackageCommand("npm", ["run", "build"]);
31
+ const build = await run(npm.command, npm.args, { cwd: root, quiet: true });
30
32
  record({ name: "build", status: build.code === 0 ? "pass" : "fail", ...(build.code === 0 ? {} : { detail: lastLines(build.stderr || build.stdout) }) });
31
33
  // The journeys run here with the app's own node and SDK: the SDK the bundle
32
34
  // pins, installed now if node_modules holds an older copy.
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { createForwarder } from "./forwarder.js";
3
3
  import { readKitLock } from "./kit.js";
4
4
  import { GovernanceClient, ensureLinkedApp } from "./integrations.js";
5
- import { acquireDevLock, allocatePorts, ensureSdk, LocalRuntime, releaseDevLock, runCommand } from "./local-runtime.js";
5
+ import { acquireDevLock, allocatePorts, ensureSdk, LocalRuntime, nodePackageCommand, releaseDevLock, runCommand } from "./local-runtime.js";
6
6
  import { CliError } from "./output.js";
7
7
  /**
8
8
  * Starts the kit-managed Postgres and App Gateway, applies migrations, then
@@ -45,7 +45,8 @@ export async function startDev(root, options) {
45
45
  if (watched > 0)
46
46
  options.output(`Installed the Harbour realtime outbox for ${watched} table(s) (harbour.realtime change events).`);
47
47
  const origin = `http://127.0.0.1:${ports.origin}`;
48
- vite = spawn("npm", ["run", "dev"], { cwd: root, env: { ...env, HARBOUR_LOCAL_ORIGIN: origin, HARBOUR_VITE_PORT: String(ports.vite) }, stdio: ["ignore", "inherit", "inherit"] });
48
+ const npm = nodePackageCommand("npm", ["run", "dev"]);
49
+ vite = spawn(npm.command, npm.args, { cwd: root, env: { ...env, HARBOUR_LOCAL_ORIGIN: origin, HARBOUR_VITE_PORT: String(ports.vite) }, stdio: ["ignore", "inherit", "inherit"] });
49
50
  vite.on("error", () => options.output("Vite could not be started; is npm installed and `npm install` done?"));
50
51
  const company = options.company;
51
52
  let warnedAuth = false;
@@ -1,6 +1,6 @@
1
1
  export const PUBLISHED_KIT_BUNDLE = {
2
2
  "schema": "harbour.kit-bundle/1.0",
3
- "kitVersion": "0.1.38",
3
+ "kitVersion": "0.1.40",
4
4
  "sdk": {
5
5
  "package": "@harbour/app-sdk",
6
6
  "version": "1.1.1",
@@ -8,13 +8,21 @@ export const PUBLISHED_KIT_BUNDLE = {
8
8
  "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:f499af191dd5a42a41e314cf93b0c7d957e0e1987cbdfc96e7c53eee06d86d25"
9
9
  },
10
10
  "images": {
11
- "appGateway": "public.ecr.aws/y6t4p3i8/harbour-app-gateway@sha256:c082203ecbed88005b9cd0b6de5f0b7953f077f80038d7905016439ef04f4251",
12
- "sessionFixture": "public.ecr.aws/y6t4p3i8/harbour-session-fixture@sha256:9396c6fd1231981a146c9ad37424f4ba91d9092e2ae0f18d8e2921bbd302a194"
11
+ "appGateway": "public.ecr.aws/y6t4p3i8/harbour-app-gateway@sha256:abaed52c0de3469bc64a21f7c542b206333fc6185d3624b5e045568dae877c27",
12
+ "sessionFixture": "public.ecr.aws/y6t4p3i8/harbour-session-fixture@sha256:8efcd7ce5ce78c047a2c526d3de76e00cae01ecb55eab5f060a74a33b8650e9a"
13
13
  },
14
14
  "nativeRuntime": {
15
15
  "darwinArm64": {
16
- "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:36222559ea1d6543c93e13dee30b433fe9f6c661ef3cce081a68182fc24e8b33",
17
- "sha256": "36222559ea1d6543c93e13dee30b433fe9f6c661ef3cce081a68182fc24e8b33"
16
+ "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:5f40b4de50ce81abcb5ee22aa8a984b46d5a5fc719f0eee035ae6bfb3dbf47cd",
17
+ "sha256": "5f40b4de50ce81abcb5ee22aa8a984b46d5a5fc719f0eee035ae6bfb3dbf47cd"
18
+ },
19
+ "linuxX64": {
20
+ "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:163a54cdab03f193499a35dc4821e10ae9a40c80aeec9c12005b2959f50cc01b",
21
+ "sha256": "163a54cdab03f193499a35dc4821e10ae9a40c80aeec9c12005b2959f50cc01b"
22
+ },
23
+ "windowsX64": {
24
+ "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:1c26d2066be2e673cba0c335fee8b1057fa314ed85325e406903f3e6f9e18351",
25
+ "sha256": "1c26d2066be2e673cba0c335fee8b1057fa314ed85325e406903f3e6f9e18351"
18
26
  }
19
27
  },
20
28
  "brief": {
@@ -2,11 +2,16 @@ import { spawn } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { copyFile, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
4
4
  import { createServer } from "node:net";
5
- import { dirname, join, relative } from "node:path";
5
+ import { dirname, join, relative, win32 } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import EmbeddedPostgres from "embedded-postgres";
8
8
  import { kitPaths, projectName } from "./kit.js";
9
9
  import { CliError } from "./output.js";
10
+ export function nodePackageCommand(tool, args, platform = process.platform, execPath = process.execPath) {
11
+ if (platform !== "win32")
12
+ return { command: tool, args };
13
+ return { command: execPath, args: [win32.join(win32.dirname(execPath), "node_modules", "npm", "bin", `${tool}-cli.js`), ...args] };
14
+ }
10
15
  export const runCommand = (command, args, options = {}) => new Promise(resolve => {
11
16
  const child = spawn(command, args, { cwd: options.cwd, env: { ...process.env, ...options.env }, stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"] });
12
17
  let stdout = "";
@@ -20,6 +25,11 @@ export const runCommand = (command, args, options = {}) => new Promise(resolve =
20
25
  child.stdin?.end(options.stdin);
21
26
  }
22
27
  });
28
+ const runWindowsController = (command, args) => new Promise(resolve => {
29
+ const child = spawn(command, args, { stdio: "ignore", windowsHide: true });
30
+ child.on("error", () => resolve(127));
31
+ child.on("close", code => resolve(code ?? 1));
32
+ });
23
33
  export const LOCAL = {
24
34
  tenant: "local-tenant",
25
35
  app: "local-app",
@@ -139,6 +149,7 @@ export class LocalRuntime {
139
149
  run;
140
150
  project;
141
151
  postgres;
152
+ windowsPgCtl;
142
153
  gateway;
143
154
  gates = new Map();
144
155
  gateOutput = new Map();
@@ -195,17 +206,41 @@ export class LocalRuntime {
195
206
  * person can apply, never as a generic "could not start".
196
207
  */
197
208
  async up(output) {
198
- void output;
199
209
  if (!this.ports)
200
210
  throw new CliError("LOCAL_RUNTIME_FAILED", "The local runtime was not prepared.");
201
211
  const paths = kitPaths(this.root);
202
- await hydrateEmbeddedPostgres(this.run);
203
- this.postgres = new EmbeddedPostgres({ databaseDir: join(paths.state, "postgres"), port: this.ports.postgres, user: LOCAL.dbUser, password: LOCAL.dbPassword, persistent: true, onLog: () => undefined, onError: () => undefined });
204
- if (!(await readdir(join(paths.state, "postgres")).catch(() => [])).length)
205
- await this.postgres.initialise();
206
- await this.postgres.start();
207
- await this.postgres.createDatabase(LOCAL.database).catch(error => { if (!String(error).includes("already exists"))
208
- throw error; });
212
+ const postgresBinaries = await hydrateEmbeddedPostgres(this.run);
213
+ const postgresMessages = [];
214
+ this.postgres = new EmbeddedPostgres({ databaseDir: join(paths.state, "postgres"), port: this.ports.postgres, user: LOCAL.dbUser, password: LOCAL.dbPassword, persistent: true, onLog: message => postgresMessages.push(String(message).trim()), onError: error => postgresMessages.push(String(error)) });
215
+ try {
216
+ if (!(await readdir(join(paths.state, "postgres")).catch(() => [])).length)
217
+ await this.postgres.initialise();
218
+ output?.("Local database files are ready.");
219
+ if (process.platform === "win32") {
220
+ this.windowsPgCtl = postgresBinaries.pg_ctl;
221
+ const started = await runWindowsController(this.windowsPgCtl, ["start", "-w", "-D", join(paths.state, "postgres"), "-l", join(paths.state, "postgres.log"), "-o", `-p ${this.ports.postgres}`]);
222
+ if (started !== 0)
223
+ throw new Error(`pg_ctl could not start Postgres (exit ${started})`);
224
+ }
225
+ else {
226
+ await this.postgres.start();
227
+ }
228
+ output?.("Local database is running.");
229
+ }
230
+ catch (error) {
231
+ const detail = postgresMessages.filter(Boolean).slice(-3).join(" ") || (error instanceof Error ? error.message : String(error));
232
+ throw new CliError("LOCAL_RUNTIME_FAILED", `Local Postgres could not start${detail ? `: ${detail}` : "."}`);
233
+ }
234
+ const root = this.postgres.getPgClient("postgres", "127.0.0.1");
235
+ try {
236
+ await root.connect();
237
+ await root.query(`CREATE DATABASE ${LOCAL.database}`).catch(error => { if (!String(error).includes("already exists"))
238
+ throw error; });
239
+ }
240
+ finally {
241
+ await root.end().catch(() => undefined);
242
+ }
243
+ output?.("Local app database is ready.");
209
244
  const admin = this.postgres.getPgClient(LOCAL.database, "127.0.0.1");
210
245
  await admin.connect();
211
246
  await admin.query(`DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${LOCAL.gatewayRole}') THEN CREATE ROLE ${LOCAL.gatewayRole} LOGIN PASSWORD '${LOCAL.dbPassword}'; END IF; END $$;`);
@@ -232,7 +267,12 @@ export class LocalRuntime {
232
267
  this.gateway?.kill("SIGTERM");
233
268
  for (const child of this.gates.values())
234
269
  child.kill("SIGTERM");
235
- await this.postgres?.stop().catch(() => undefined);
270
+ if (this.windowsPgCtl) {
271
+ await runWindowsController(this.windowsPgCtl, ["stop", "-w", "-D", join(kitPaths(this.root).state, "postgres"), "-m", "fast"]);
272
+ }
273
+ else {
274
+ await this.postgres?.stop().catch(() => undefined);
275
+ }
236
276
  return true;
237
277
  }
238
278
  /** Removes this project's containers and named volumes only. */
@@ -343,7 +383,7 @@ export class LocalRuntime {
343
383
  }
344
384
  }
345
385
  async function hydrateEmbeddedPostgres(run) {
346
- const packageName = `@embedded-postgres/${process.platform}-${process.arch}`;
386
+ const packageName = embeddedPostgresPackage(process.platform, process.arch);
347
387
  let entry;
348
388
  try {
349
389
  entry = fileURLToPath(import.meta.resolve(packageName));
@@ -356,13 +396,20 @@ async function hydrateEmbeddedPostgres(run) {
356
396
  const result = await run(process.execPath, [script], { cwd: packageRoot, quiet: true });
357
397
  if (result.code !== 0)
358
398
  throw new CliError("KIT_RUNTIME_UNAVAILABLE", "The bundled Postgres files could not be prepared.");
399
+ const binaries = await import(packageName);
400
+ if (!binaries.pg_ctl)
401
+ throw new CliError("KIT_RUNTIME_UNAVAILABLE", "The bundled Postgres controller is missing.");
402
+ return { pg_ctl: binaries.pg_ctl };
403
+ }
404
+ export function embeddedPostgresPackage(platform, arch) {
405
+ return `@embedded-postgres/${platform === "win32" ? "windows" : platform}-${arch}`;
359
406
  }
360
407
  async function ensureNativeGateway(root, bundle) {
361
- const runtime = process.platform === "darwin" && process.arch === "arm64" ? bundle.nativeRuntime?.darwinArm64 : undefined;
408
+ const runtime = nativeGatewayArtifact(bundle, process.platform, process.arch);
362
409
  if (!runtime)
363
410
  throw new CliError("KIT_RUNTIME_UNAVAILABLE", `This kit bundle has no native Harbour runtime for ${process.platform}-${process.arch}.`);
364
411
  const directory = join(kitPaths(root).local, "runtime");
365
- const target = join(directory, `harbour-app-gateway-${runtime.sha256.slice(0, 12)}`);
412
+ const target = join(directory, `harbour-app-gateway-${runtime.sha256.slice(0, 12)}${process.platform === "win32" ? ".exe" : ""}`);
366
413
  if (!(await exists(target))) {
367
414
  const bytes = await downloadSdk(runtime.url);
368
415
  const digest = createHash("sha256").update(bytes).digest("hex");
@@ -373,6 +420,15 @@ async function ensureNativeGateway(root, bundle) {
373
420
  }
374
421
  return target;
375
422
  }
423
+ export function nativeGatewayArtifact(bundle, platform, arch) {
424
+ if (platform === "darwin" && arch === "arm64")
425
+ return bundle.nativeRuntime?.darwinArm64;
426
+ if (platform === "linux" && arch === "x64")
427
+ return bundle.nativeRuntime?.linuxX64;
428
+ if (platform === "win32" && arch === "x64")
429
+ return bundle.nativeRuntime?.windowsX64;
430
+ return undefined;
431
+ }
376
432
  /** The three names the pipeline's retained-check runner exports for the signed identities, from a session env. */
377
433
  export function identityEnvironment(session) {
378
434
  return {
@@ -426,7 +482,8 @@ export async function ensureSdk(root, bundle, env, run, output, fetchImpl = fetc
426
482
  else if (installed && (await readFile(join(root, "package.json"), "utf8").catch(() => "")).includes(relative(root, staged)))
427
483
  return "present";
428
484
  output(`Installing ${bundle.sdk.package} from the kit bundle.`);
429
- const result = await run("npm", ["install", "--no-audit", "--no-fund", relative(root, staged)], { cwd: root, quiet: true });
485
+ const npm = nodePackageCommand("npm", ["install", "--no-audit", "--no-fund", relative(root, staged)]);
486
+ const result = await run(npm.command, npm.args, { cwd: root, quiet: true });
430
487
  if (result.code !== 0)
431
488
  throw new CliError("SDK_INSTALL_FAILED", `npm could not install the kit SDK tarball: ${result.stderr.trim().split("\n").at(-1) ?? "npm error"}`);
432
489
  return "installed";
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.1.38";
1
+ export const CLI_VERSION = "0.1.40";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fourier-labs/harbour",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "description": "Harbour productionisation helper",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,7 +37,7 @@
37
37
  "harbour": {
38
38
  "kitBundle": {
39
39
  "repository": "public.ecr.aws/y6t4p3i8/harbour-kit-bundle",
40
- "version": "0.1.38"
40
+ "version": "0.1.40"
41
41
  }
42
42
  }
43
43
  }