@isomorph.ai/cli 0.2.0 → 0.2.2-rc.1
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/README.md
CHANGED
|
@@ -46,10 +46,10 @@ isomorph status | retry | promote | setup | profile | audience | secrets … --o
|
|
|
46
46
|
|
|
47
47
|
## Apps set up by the old `harbour` CLI
|
|
48
48
|
|
|
49
|
-
Before 0.2.0 the CLI was published as `@fourier-labs/harbour` with the command `harbour`, and it kept the kit's files under `.harbour/`. That layout is not read any more — not by this CLI and not by the deployment pipeline. Run `isomorph init --upgrade --app-root .` once in each such app: it renames `.harbour/` to `.isomorph/` (git records renames), moves the two committed schema ids with it, rewrites the managed block in CLAUDE.md / AGENTS.md
|
|
49
|
+
Before 0.2.0 the CLI was published as `@fourier-labs/harbour` with the command `harbour`, and it kept the kit's files under `.harbour/`. That layout is not read any more — not by this CLI and not by the deployment pipeline. Run `isomorph init --upgrade --app-root .` once in each such app: it renames `.harbour/` to `.isomorph/` (git records renames), moves the two committed schema ids with it, rewrites the managed block in CLAUDE.md / AGENTS.md, replaces the project skill (the old `.claude/skills/harbour-kit/SKILL.md` is deleted when it is the kit's — the frontmatter name and the opening line every release wrote — and kept, and reported as kept, when you rewrote its body), deletes the checks the old kit generated (the next `isomorph check` regenerates them) and rewrites the SDK package name, its exported type names and the retired `HARBOUR_*` environment names (`HARBOUR_APP_URL` → `ISOMORPH_APP_URL` and the rest) in `package.json`, `vite.config.*`, `src/`, `jobs/` and the checks you wrote yourself. Run it again after updating the CLI: an app already moved by 0.2.0 loses its old skill and old environment names the same way. Every other command refuses an un-upgraded app and names that command. The old CLI can stay installed; it no longer deploys.
|
|
50
50
|
|
|
51
51
|
## Development
|
|
52
52
|
|
|
53
|
-
Built from the [governance control plane](https://github.com/Fourier-Labs-AI/harbour-governance-control-plane) repository: `npm ci`, `npm run build --prefix packages/harbour-cli`, tests in `tests/cli-kit.test.ts` and `tests/cli-packaging-contract.test.ts`. Releases are tagged `cli-v<version>`
|
|
53
|
+
Built from the [governance control plane](https://github.com/Fourier-Labs-AI/harbour-governance-control-plane) repository: `npm ci`, `npm run build --prefix packages/harbour-cli`, tests in `tests/cli-kit.test.ts` and `tests/cli-packaging-contract.test.ts`. Releases are tagged `cli-v<version>` by the release automation the moment `packages/harbour-cli/package.json` changes on `main`, so bump to a candidate first (`X.Y.Z-rc.1`, published under the `candidate` dist-tag through npm trusted publishing) and promote it with the `CLI release automation` workflow's `candidate` input; a final version on `main` without an accepted candidate is refused by the release job. 0.2.0 and 0.2.1 (the rename) were published by hand from their merge commits before the trusted publisher existed for `@isomorph.ai/cli`, which is why their `cli-v` release runs show red.
|
|
54
54
|
|
|
55
55
|
Connect with `isomorph connect name@getlokal.com`: the CLI tries `https://platform.isomorph.ai/start/getlokal.json` and validates the profile before saving it. This uses the first domain label as a guess, not a domain registry; subdomains or companies whose tenant name differs should use their company setup link. Only the guessed tenant name is sent, not the email. Company sign-in and access checks still apply.
|
|
@@ -85,10 +85,16 @@ export async function productionise(rootArg, client, output, tenantId, includePa
|
|
|
85
85
|
const gate = options.kitGate === false ? undefined : options.kitGate ?? (options.integrations ? { bundle: options.integrations.bundle } : undefined);
|
|
86
86
|
if (gate && !pending?.uploaded)
|
|
87
87
|
await preflightKitGate(root, gate.bundle, output, gate.run);
|
|
88
|
+
// A `CliError` is Isomorph's own answer (`AUTH_REQUIRED` after two 401s, a
|
|
89
|
+
// JSON-RPC refusal) and reaches the builder as it is; only a plain Error from
|
|
90
|
+
// the transport — Isomorph out of reach — is "not started". Reporting a
|
|
91
|
+
// refused sign-in as unreachable sent an agent down a worse path (2026-09-14).
|
|
88
92
|
try {
|
|
89
93
|
await client.initialize();
|
|
90
94
|
}
|
|
91
|
-
catch {
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof CliError)
|
|
97
|
+
throw error;
|
|
92
98
|
throw new CliError("NOT_STARTED", "Isomorph could not be reached before the operation started.");
|
|
93
99
|
}
|
|
94
100
|
let start;
|
|
@@ -37,6 +37,20 @@ function rpcRefusal(error, status) {
|
|
|
37
37
|
const code = labelled?.[1] ?? (typeof error.code === "number" ? `ISOMORPH_RPC_${error.code}` : `ISOMORPH_HTTP_${status}`);
|
|
38
38
|
return new CliError(code, (labelled?.[2] ?? text) || `Isomorph refused the request (HTTP ${status}).`);
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* A second 401 in a row: the sign-in itself is gone, or this company does not
|
|
42
|
+
* admit the signed-in account. The server says which in `error_description`
|
|
43
|
+
* (plain English, no URL or token); an older server sends only the code, and
|
|
44
|
+
* the fixed sentence stands in. Both fixes are named because the builder cannot
|
|
45
|
+
* tell the two causes apart: on 2026-09-14 a `productionise` 14 seconds after a
|
|
46
|
+
* successful `isomorph login` was refused by a company the account was not a
|
|
47
|
+
* member of, and "run login again" would only have repeated it.
|
|
48
|
+
*/
|
|
49
|
+
async function signInRefused(response) {
|
|
50
|
+
const body = await response.json().catch(() => undefined);
|
|
51
|
+
const said = typeof body?.error_description === "string" ? body.error_description.trim() : "";
|
|
52
|
+
return new CliError("AUTH_REQUIRED", `${said || "Isomorph sign-in expired or was revoked."} Run \`isomorph login\` again, or \`isomorph connect <work-email-or-start-url>\` if this is the wrong company.`);
|
|
53
|
+
}
|
|
40
54
|
async function readRpc(response) {
|
|
41
55
|
try {
|
|
42
56
|
return await response.json();
|
|
@@ -76,11 +90,11 @@ export class RemoteMcpClient {
|
|
|
76
90
|
let response = await this.post(body);
|
|
77
91
|
// A 401 mid-command usually means the stored token was rotated by another
|
|
78
92
|
// process; re-resolve (which reloads the store) and retry once. A second
|
|
79
|
-
// 401
|
|
93
|
+
// 401 is Isomorph refusing the sign-in, and `signInRefused` says why.
|
|
80
94
|
if (response.status === 401)
|
|
81
95
|
response = await this.post(body);
|
|
82
96
|
if (response.status === 401)
|
|
83
|
-
throw
|
|
97
|
+
throw await signInRefused(response);
|
|
84
98
|
if (!expectResponse)
|
|
85
99
|
return undefined;
|
|
86
100
|
const parsed = await readRpc(response);
|
|
@@ -26,7 +26,8 @@ export async function initKit(root, bundle, options = {}) {
|
|
|
26
26
|
const result = { root, created: [], kept: [], updated: [], mode: options.upgrade ? "upgrade" : emptyDir ? "starter" : "existing", bundleChanges: [], agents: options.env ? await agentSetup(options.env) : { created: [], updated: [], kept: [], removed: [] } };
|
|
27
27
|
if (options.upgrade) {
|
|
28
28
|
await migrateLegacyKit(root, bundle, result);
|
|
29
|
-
await
|
|
29
|
+
await retireLegacySkill(root, result);
|
|
30
|
+
await rewriteLegacyReferences(root, bundle.sdk.package, result);
|
|
30
31
|
}
|
|
31
32
|
const write = async (path, content) => {
|
|
32
33
|
const absolute = join(root, path);
|
|
@@ -76,18 +77,29 @@ const LEGACY = {
|
|
|
76
77
|
generatedPrefix: "// harbour:generated sha256:",
|
|
77
78
|
gitignoreLine: ".harbour/local/",
|
|
78
79
|
skill: ".claude/skills/harbour-kit/SKILL.md",
|
|
80
|
+
/** How every release's project skill opened, under the frontmatter `name: harbour-kit`; the body below it changed from release to release. */
|
|
81
|
+
skillOpening: 'Follow the "Harbour development kit" block',
|
|
79
82
|
sdkPackage: "@harbour/app-sdk",
|
|
80
83
|
/** The SDK's exported type names, renamed with the package. */
|
|
81
|
-
typeNames: /\bHarbour(User|ErrorCategory|Error|Client|Result|CompatSource)\b/g
|
|
84
|
+
typeNames: /\bHarbour(User|ErrorCategory|Error|Client|Result|CompatSource)\b/g,
|
|
85
|
+
/**
|
|
86
|
+
* The environment names the kit and the platform export to a check, a job and
|
|
87
|
+
* the Vite config, renamed with the CLI. Whole words: the session's
|
|
88
|
+
* `HARBOUR_LOCAL_USER_EMAIL` and the CLI↔gateway `HARBOUR_APP_GATEWAY_*` and
|
|
89
|
+
* `HARBOUR_GATE_*` names are not builder-visible and are not renamed.
|
|
90
|
+
*/
|
|
91
|
+
envNames: /\bHARBOUR_(APP_URL|SDK_MODULE|IDENTITY_CONTEXT_SECOND_USER|IDENTITY_CONTEXT_HEADER|IDENTITY_CONTEXT|GATEWAY_URL|WORKLOAD_TOKEN|SCHEDULED_AT|LOCAL_ORIGIN|VITE_PORT)\b/g
|
|
82
92
|
};
|
|
83
93
|
/**
|
|
84
94
|
* Moves an app set up by a CLI older than 0.2.0 to this kit, once, and only under
|
|
85
95
|
* `init --upgrade`: the kit directory and its five entries are renamed (git records
|
|
86
|
-
* renames), the two committed schema ids move with it, the `.gitignore` line
|
|
87
|
-
* managed block in CLAUDE.md / AGENTS.md and the
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
96
|
+
* renames), the two committed schema ids move with it, the `.gitignore` line and
|
|
97
|
+
* the managed block in CLAUDE.md / AGENTS.md are rewritten, and the checks the old
|
|
98
|
+
* kit generated are deleted for the next `check` to regenerate. A check the author
|
|
99
|
+
* wrote or edited is kept, as everywhere else. Nothing happens when there is no old
|
|
100
|
+
* layout, or when the current one already exists beside it. The old project skill
|
|
101
|
+
* and the old names in the app's own files are handled after this, on every
|
|
102
|
+
* `--upgrade`, so an app this pass already moved still loses them.
|
|
91
103
|
*/
|
|
92
104
|
async function migrateLegacyKit(root, bundle, result) {
|
|
93
105
|
const legacy = join(root, LEGACY_KIT_DIRECTORY);
|
|
@@ -124,12 +136,6 @@ async function migrateLegacyKit(root, bundle, result) {
|
|
|
124
136
|
touched(file);
|
|
125
137
|
}
|
|
126
138
|
}
|
|
127
|
-
const skill = join(root, LEGACY.skill);
|
|
128
|
-
if ((await readFile(skill, "utf8").catch(() => undefined)) === LEGACY_SKILL_FILE) {
|
|
129
|
-
await rm(skill);
|
|
130
|
-
await rmdir(dirname(skill)).catch(() => undefined);
|
|
131
|
-
touched(LEGACY.skill);
|
|
132
|
-
}
|
|
133
139
|
for (const name of await readdir(paths.checks).catch(() => [])) {
|
|
134
140
|
if (!isPristine(await readFile(join(paths.checks, name), "utf8").catch(() => ""), LEGACY.generatedPrefix))
|
|
135
141
|
continue;
|
|
@@ -138,15 +144,45 @@ async function migrateLegacyKit(root, bundle, result) {
|
|
|
138
144
|
}
|
|
139
145
|
}
|
|
140
146
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
147
|
+
* Deletes the project skill an older CLI wrote, on every `--upgrade` — an app the
|
|
148
|
+
* layout migration moved under 0.2.0 still has it beside the new skill. The file
|
|
149
|
+
* is the kit's by shape, not by bytes: each release wrote a different body under
|
|
150
|
+
* the same frontmatter name and the same opening line, so one whose body an
|
|
151
|
+
* author rewrote is kept, and reported, as theirs.
|
|
152
|
+
*/
|
|
153
|
+
async function retireLegacySkill(root, result) {
|
|
154
|
+
const path = join(root, LEGACY.skill);
|
|
155
|
+
const text = await readFile(path, "utf8").catch(() => undefined);
|
|
156
|
+
if (text === undefined)
|
|
157
|
+
return;
|
|
158
|
+
if (!isLegacyKitSkill(text)) {
|
|
159
|
+
result.kept.push(LEGACY.skill);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
await rm(path);
|
|
163
|
+
await rmdir(dirname(path)).catch(() => undefined);
|
|
164
|
+
result.updated.push(LEGACY.skill);
|
|
165
|
+
}
|
|
166
|
+
/** Frontmatter naming `harbour-kit`, and the opening every release wrote as the body's first line. */
|
|
167
|
+
function isLegacyKitSkill(text) {
|
|
168
|
+
const lines = text.split(/\r?\n/);
|
|
169
|
+
const close = lines.indexOf("---", 1);
|
|
170
|
+
if (lines[0] !== "---" || close < 0)
|
|
171
|
+
return false;
|
|
172
|
+
return lines.slice(1, close).includes("name: harbour-kit") && (lines.slice(close + 1).find(line => line.trim()) ?? "").startsWith(LEGACY.skillOpening);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Rewrites what the old kit named in the app's own files — `package.json`,
|
|
176
|
+
* `vite.config.*`, `src/`, `jobs/` and the retained checks — on every `--upgrade`:
|
|
177
|
+
* the SDK's previous package name (and, in `package.json`, the tarball path the
|
|
178
|
+
* old kit staged it under), its six exported type names, and the environment
|
|
179
|
+
* names the kit and the platform export to a check, a job and the Vite config.
|
|
180
|
+
* Whole words only, so an author's own `HarbourUserProfile` is theirs. Idempotent:
|
|
181
|
+
* an app that names none of them is left untouched and unreported.
|
|
146
182
|
*/
|
|
147
|
-
async function
|
|
148
|
-
const rewrite = (text) => text.replaceAll(`"${LEGACY.sdkPackage}"`, `"${sdkPackage}"`).replaceAll(`'${LEGACY.sdkPackage}'`, `'${sdkPackage}'`).replace(LEGACY.typeNames, "Isomorph$1");
|
|
149
|
-
for (const path of ["package.json", ...(await appSourceFiles(root))]) {
|
|
183
|
+
async function rewriteLegacyReferences(root, sdkPackage, result) {
|
|
184
|
+
const rewrite = (text) => text.replaceAll(`"${LEGACY.sdkPackage}"`, `"${sdkPackage}"`).replaceAll(`'${LEGACY.sdkPackage}'`, `'${sdkPackage}'`).replace(LEGACY.typeNames, "Isomorph$1").replace(LEGACY.envNames, "ISOMORPH_$1");
|
|
185
|
+
for (const path of ["package.json", "vite.config.ts", "vite.config.js", "vite.config.mjs", ...(await appSourceFiles(root))]) {
|
|
150
186
|
const current = await readFile(join(root, path), "utf8").catch(() => undefined);
|
|
151
187
|
if (current === undefined)
|
|
152
188
|
continue;
|
|
@@ -242,21 +278,6 @@ Follow the "Isomorph development kit" block in CLAUDE.md / AGENTS.md. Workflow:
|
|
|
242
278
|
5. \`isomorph integrations request <connection> --reason "<why>" --app-root .\` asks IT for access now — one request per connection, approved once for every environment; \`isomorph dev\` (signed in) and \`isomorph productionise\` file it for you. Pending is not ready.
|
|
243
279
|
6. \`isomorph productionise --app-root .\` saves and deploys the preview; \`isomorph promote\` after the person has tested it.
|
|
244
280
|
`;
|
|
245
|
-
/** The project skill CLI releases before 0.2.0 wrote, byte for byte: `init --upgrade` deletes the old file only when it is still exactly this. */
|
|
246
|
-
export const LEGACY_SKILL_FILE = `---
|
|
247
|
-
name: harbour-kit
|
|
248
|
-
description: Build, run and check this Harbour app with the Harbour CLI (dev, check, integrations, productionise).
|
|
249
|
-
---
|
|
250
|
-
|
|
251
|
-
Follow the "Harbour development kit" block in CLAUDE.md / AGENTS.md. Workflow:
|
|
252
|
-
|
|
253
|
-
1. \`harbour dev --app-root .\` starts Postgres, storage and one Harbour gateway (session identities, fixtures, realtime) plus Vite behind one loopback origin printed in the banner.
|
|
254
|
-
2. Edit \`src/\` and \`migrations/\`. Use the SDK only. \`.harbour/integrations.json\` starts with no connections and gains one only when the app really calls a company system: declare it with only the operations the app calls, then request access (step 5). A declared connection the app does not call blocks every deploy until IT grants it; an undeclared one cannot be requested, so it never gets a grant. README.md has the worked Slack and warehouse examples — the file itself is strict JSON and cannot hold comments.
|
|
255
|
-
3. \`.harbour/checks/\` is generated, not written by hand: \`harbour check\` reads \`migrations/\` and \`src/\` and writes one retained journey per capability the app's own code uses (plus a cross-user denial per owner-scoped table), deleting the ones the app no longer needs — so run it in the same edit that changes the app instead of adding or deleting these files yourself. It reports anything it cannot generate (\`actions\`, \`realtime\`, a table with no migration) for you to write. Edit a generated check and it becomes yours: Harbour keeps it, stops managing it and never removes it, so deleting it when the feature goes is then your job. \`harbour check\` and the deployment pipeline replay these checks against a real App Gateway and refuse the app (\`flow.check-failed\`) in both directions — a capability no check exercises, and a check that exercises something the code no longer does.
|
|
256
|
-
4. \`harbour check --app-root .\` before every hand-off; read \`.harbour/local/check-report.json\`. Real integrations are reported as not tested unless \`--integrations\` is passed (read operations only); a retained check that calls \`harbour.integrations.execute\` is answered by the gate's fixture (the contract's shape for what the app declared, nothing sent or read) and the passed check says so. A governed AI call (\`harbour.ai.chat\`) is exercised for real through the development route while \`harbour dev\` is up; the pipeline answers it with a canned completion and reports it as not tested.
|
|
257
|
-
5. \`harbour integrations request <connection> --reason "<why>" --app-root .\` asks IT for access now — one request per connection, approved once for every environment; \`harbour dev\` (signed in) and \`harbour productionise\` file it for you. Pending is not ready.
|
|
258
|
-
6. \`harbour productionise --app-root .\` saves and deploys the preview; \`harbour promote\` after the person has tested it.
|
|
259
|
-
`;
|
|
260
281
|
/**
|
|
261
282
|
* Kit infrastructure: structural files every kit app needs, on every path —
|
|
262
283
|
* `init` in an empty directory, `init` over an existing Vite + React app, and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.2.
|
|
1
|
+
export const CLI_VERSION = "0.2.2-rc.1";
|