@isomorph.ai/cli 0.2.1 → 0.2.2
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
|
@@ -50,6 +50,6 @@ Before 0.2.0 the CLI was published as `@fourier-labs/harbour` with the command `
|
|
|
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);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.2.
|
|
1
|
+
export const CLI_VERSION = "0.2.2";
|
|
@@ -118,7 +118,11 @@ function safeSourcePath(path) {
|
|
|
118
118
|
function toBytes(value) { return typeof value === "string" ? new TextEncoder().encode(value) : value; }
|
|
119
119
|
function bytesToBase64(bytes) { return Buffer.from(bytes).toString("base64"); }
|
|
120
120
|
function base64ToBytes(value) {
|
|
121
|
-
|
|
121
|
+
// Length-multiple-of-four plus a single character class: the same language as
|
|
122
|
+
// the padded-quartet grammar, checked without a quantified group. V8 backtracks
|
|
123
|
+
// per quartet of a grouped quantifier, so the grammar form overflowed the stack
|
|
124
|
+
// on any file over ~3 MB (a 42 MB SQLite file shipped inside an app package).
|
|
125
|
+
if (value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value))
|
|
122
126
|
throw new Error("SOURCE_ARCHIVE_INVALID: file is not valid base64.");
|
|
123
127
|
return new Uint8Array(Buffer.from(value, "base64"));
|
|
124
128
|
}
|