@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.
- package/dist/packages/harbour-cli/src/agent-setup.js +1 -1
- package/dist/packages/harbour-cli/src/check.js +5 -3
- package/dist/packages/harbour-cli/src/dev.js +3 -2
- package/dist/packages/harbour-cli/src/kit-bundle.manifest.js +13 -5
- package/dist/packages/harbour-cli/src/local-runtime.js +71 -14
- package/dist/packages/harbour-cli/src/version.js +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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:
|
|
12
|
-
"sessionFixture": "public.ecr.aws/y6t4p3i8/harbour-session-fixture@sha256:
|
|
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:
|
|
17
|
-
"sha256": "
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
40
|
+
"version": "0.1.40"
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
}
|