@isomorph.ai/cli 0.6.0 → 0.7.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/dist/packages/harbour-cli/src/check.js +5 -7
- package/dist/packages/harbour-cli/src/cli.js +8 -8
- package/dist/packages/harbour-cli/src/config.js +7 -6
- package/dist/packages/harbour-cli/src/deploy.js +11 -8
- package/dist/packages/harbour-cli/src/dev.js +7 -4
- package/dist/packages/harbour-cli/src/guide.js +2 -2
- package/dist/packages/harbour-cli/src/integrations.js +9 -4
- package/dist/packages/harbour-cli/src/kit-bundle.js +21 -9
- package/dist/packages/harbour-cli/src/kit-bundle.manifest.js +12 -12
- package/dist/packages/harbour-cli/src/kit.js +36 -3
- package/dist/packages/harbour-cli/src/local-runtime.js +65 -46
- package/dist/packages/harbour-cli/src/starter.js +22 -5
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { CliError } from "./output.js";
|
|
4
4
|
import { DEPENDENT_READ_OPERATIONS, READ_OPERATIONS, kitPaths, readDeclaration, readKitLock, resourceNames, sourceDigest } from "./kit.js";
|
|
5
|
-
import { LocalRuntime, allocatePorts,
|
|
5
|
+
import { LocalRuntime, allocatePorts, freePort, installDependencies, nodePackageCommand, readDevLock, runningOrigin } from "./local-runtime.js";
|
|
6
6
|
import { CLI_VERSION } from "./version.js";
|
|
7
7
|
/**
|
|
8
8
|
* What actually failed, in the refusal itself.
|
|
@@ -45,17 +45,15 @@ export async function runChecks(root, options) {
|
|
|
45
45
|
const previous = await readReport(root);
|
|
46
46
|
if (previous && previous.sourceDigest !== (await sourceDigest(root)).digest)
|
|
47
47
|
output("Source changed since the last report; the previous report is no longer valid.");
|
|
48
|
+
// A fresh clone has no node_modules: install once, and say so, before the typecheck needs the SDK's types.
|
|
49
|
+
await installDependencies(root, bundle, run, output);
|
|
48
50
|
const npx = nodePackageCommand("npx", ["tsc", "--noEmit"]);
|
|
49
51
|
const typecheck = await run(npx.command, npx.args, { cwd: root, quiet: true });
|
|
50
52
|
record({ name: "typecheck", status: typecheck.code === 0 ? "pass" : "fail", ...(typecheck.code === 0 ? {} : { detail: lastLines(typecheck.stdout || typecheck.stderr) }) });
|
|
51
53
|
const npm = nodePackageCommand("npm", ["run", "build"]);
|
|
52
54
|
const build = await run(npm.command, npm.args, { cwd: root, quiet: true });
|
|
53
55
|
record({ name: "build", status: build.code === 0 ? "pass" : "fail", ...(build.code === 0 ? {} : { detail: lastLines(build.stderr || build.stdout) }) });
|
|
54
|
-
// The gate runs the journeys with this node and the app's own
|
|
55
|
-
// the bundle pins, installed now if node_modules holds an older copy.
|
|
56
|
-
const sdk = await ensureSdk(root, bundle, options.env ?? process.env, run, output).catch(error => { output(error instanceof Error ? error.message : String(error)); return "missing"; });
|
|
57
|
-
if (sdk === "missing")
|
|
58
|
-
output(`The kit SDK is not installed: set ISOMORPH_KIT_SDK_TARBALL to the bundle's ${bundle.sdk.package} tarball (or use a bundle with sdk.url).`);
|
|
56
|
+
// The gate runs the journeys with this node and the SDK the app's own install put in node_modules.
|
|
59
57
|
const gate = await runKitGate(root, { run, bundle, output, ...(options.fetch ? { fetch: options.fetch } : {}), ...(options.runtime ? { runtime: options.runtime } : {}), ...(options.pollMs ? { pollMs: options.pollMs } : {}) });
|
|
60
58
|
for (const check of gate.checks)
|
|
61
59
|
record(check);
|
|
@@ -94,7 +92,7 @@ export async function runChecks(root, options) {
|
|
|
94
92
|
createdAt: new Date().toISOString(),
|
|
95
93
|
sourceDigest: source.digest,
|
|
96
94
|
sourceFiles: source.entries,
|
|
97
|
-
toolchain: { node: process.version, cliVersion: CLI_VERSION, bundle: { kitVersion: bundle.kitVersion,
|
|
95
|
+
toolchain: { node: process.version, cliVersion: CLI_VERSION, bundle: { kitVersion: bundle.kitVersion, sdkVersion: bundle.sdk.version, briefFingerprint: bundle.brief.fingerprint } },
|
|
98
96
|
checks,
|
|
99
97
|
gate,
|
|
100
98
|
integrations,
|
|
@@ -6,12 +6,12 @@ import { CLI_VERSION } from "./version.js";
|
|
|
6
6
|
import { connectedAccount, login, logout, refreshStoredToken } from "./auth.js";
|
|
7
7
|
import { assertCliCurrent, companyLabel, connect, loadConfig, resolveConfig } from "./config.js";
|
|
8
8
|
import { EMBEDDED_KIT_BUNDLE } from "./kit-bundle.js";
|
|
9
|
-
import { appRoot, readAppProfile, readKitLock, requestResourceName } from "./kit.js";
|
|
9
|
+
import { appRoot, assertLockCompany, readAppProfile, readKitLock, requestResourceName } from "./kit.js";
|
|
10
10
|
import { initKit } from "./starter.js";
|
|
11
11
|
import { packageApp } from "./package.js";
|
|
12
12
|
import { agentPaths, agentSetup } from "./agent-setup.js";
|
|
13
13
|
import { detachDev, startDev } from "./dev.js";
|
|
14
|
-
import { endProcess,
|
|
14
|
+
import { endProcess, installDependencies, LocalRuntime, readDevLock, releaseDevLock, runCommand } from "./local-runtime.js";
|
|
15
15
|
import { CHECKS_FAILED_HINT, checksFailedMessage, runChecks } from "./check.js";
|
|
16
16
|
import { assertDeployReady, companySystemsLine, GovernanceClient, groupGrants, IDENTITY_WORDS, integrationsCatalog, integrationsStatus, renderGrantGroup, renderIntegrationsCatalog, requestIntegrations } from "./integrations.js";
|
|
17
17
|
import { runJob } from "./jobs.js";
|
|
@@ -87,7 +87,7 @@ const emit = (value, line) => { process.stdout.write(json ? `${JSON.stringify(va
|
|
|
87
87
|
*/
|
|
88
88
|
const exitAfterWriting = (code, out, err = "") => { process.stderr.write(err, () => process.stdout.write(out, () => process.exit(code))); };
|
|
89
89
|
/** Exit 2, like a usage error: nothing started and the fix is a command the maker runs (or a file the maker edits). */
|
|
90
|
-
const USAGE_REFUSALS = ["APP_EXISTS", "APP_UNSUPPORTED", "KIT_APP", "DEPLOY_BLOCKED", "INTEGRATIONS_NOT_READY", "AI_NOT_READY", "CLI_UPGRADE_REQUIRED", "NOT_A_MEMBER", "TENANT_AMBIGUOUS", "NOT_A_START_LINK", "PLATFORM_UNREACHABLE", "DEPLOY_IN_FLIGHT", "OPERATION_REQUIRED"];
|
|
90
|
+
const USAGE_REFUSALS = ["APP_EXISTS", "APP_UNSUPPORTED", "KIT_APP", "DEPLOY_BLOCKED", "INTEGRATIONS_NOT_READY", "AI_NOT_READY", "CLI_UPGRADE_REQUIRED", "KIT_BUNDLE_STALE", "NOT_A_MEMBER", "TENANT_AMBIGUOUS", "NOT_A_START_LINK", "PLATFORM_UNREACHABLE", "DEPLOY_IN_FLIGHT", "OPERATION_REQUIRED"];
|
|
91
91
|
if (command === "--version" || command === "version") {
|
|
92
92
|
process.stdout.write(`${CLI_VERSION}\n`);
|
|
93
93
|
// `-h` was matched anywhere in argv while `--help` was only recognised as the
|
|
@@ -138,10 +138,8 @@ else {
|
|
|
138
138
|
const result = await initKit(target, bundle, { upgrade, adopt, tenantId: config?.tenantId, env: process.env, displayRoot: root });
|
|
139
139
|
for (const line of [...result.created.map(path => `created ${path}`), ...result.updated.map(path => `updated ${path}`), ...result.kept.map(path => `kept ${path}`), ...result.bundleChanges.map(change => `bundle ${change}`), ...["created", "updated"].flatMap(state => result.agents[state].map(path => `${state} ${path} (agent guide)`))])
|
|
140
140
|
progress(line);
|
|
141
|
-
// The SDK is
|
|
142
|
-
|
|
143
|
-
if (sdk === "missing")
|
|
144
|
-
progress(`The kit SDK was not installed: set ISOMORPH_KIT_SDK_TARBALL to the bundle's ${bundle.sdk.package} tarball (or use a bundle with sdk.url), then rerun \`isomorph init\`.`);
|
|
141
|
+
// The SDK is a normal dependency: one plain `npm install` when node_modules lacks it or holds one outside the range init wrote.
|
|
142
|
+
await installDependencies(target, bundle, runCommand, progress);
|
|
145
143
|
progress(result.mode === "starter" ? "Starter created. Next: `isomorph dev --app-root <path>`. Both agents read the Isomorph block in CLAUDE.md / AGENTS.md and the `isomorph` skill installed by agent-setup."
|
|
146
144
|
: upgrade ? (result.bundleChanges.length ? "Kit bundle upgraded; running checks." : "Kit bundle already current; running checks.")
|
|
147
145
|
: result.mode === "kit" ? `This folder is already an Isomorph kit app (any missing kit file was added; nothing else was changed). Next: \`isomorph dev --app-root ${root}\`, \`isomorph check --app-root ${root}\`, \`isomorph deploy --app-root ${root}\`.`
|
|
@@ -254,7 +252,9 @@ else {
|
|
|
254
252
|
// from the folder (the current one unless --app-root says otherwise) and,
|
|
255
253
|
// without --operation, follow the deployment last started from it.
|
|
256
254
|
const target = appRoot(root ?? ".");
|
|
257
|
-
const
|
|
255
|
+
const lock = await readKitLock(target);
|
|
256
|
+
assertLockCompany(lock, tenant);
|
|
257
|
+
const appId = lock?.appId;
|
|
258
258
|
if (!appId)
|
|
259
259
|
throw new CliError("KIT_REQUIRED", `${root ? "That folder" : "The current folder"} is not a deployed Isomorph app: it has no .isomorph/kit.lock.json with an app identity.`, undefined, "Run this from the app's folder, or pass --app-root <path>.");
|
|
260
260
|
const ref = operationRef ?? (await readDeployNote(target, tenant))?.operationRef;
|
|
@@ -22,14 +22,15 @@ export function assertCliCurrent(config, running = CLI_VERSION) {
|
|
|
22
22
|
return;
|
|
23
23
|
throw new CliError("CLI_UPGRADE_REQUIRED", `This CLI is ${running}; the company's Isomorph platform needs ${minimum} or newer.`, undefined, `Run \`${CLI_INSTALL_COMMAND}\`, then run this again.`);
|
|
24
24
|
}
|
|
25
|
+
/** The numeric core and prerelease of a version string (`v1.2.3-rc.1` → `[1, 2, 3]`, `rc.1`), or `undefined` for one with no numeric core. */
|
|
26
|
+
export function parseVersion(value) {
|
|
27
|
+
const match = /^\s*v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(value);
|
|
28
|
+
return match ? { core: [Number(match[1]), Number(match[2]), Number(match[3])], pre: match[4] } : undefined;
|
|
29
|
+
}
|
|
25
30
|
/** Numeric-core semver order; a prerelease sorts before its release. An unparseable version compares equal, so nothing is called stale on a guess. */
|
|
26
31
|
export function compareCliVersions(left, right) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
return match ? { core: [Number(match[1]), Number(match[2]), Number(match[3])], pre: match[4] } : undefined;
|
|
30
|
-
};
|
|
31
|
-
const a = parse(left);
|
|
32
|
-
const b = parse(right);
|
|
32
|
+
const a = parseVersion(left);
|
|
33
|
+
const b = parseVersion(right);
|
|
33
34
|
if (!a || !b)
|
|
34
35
|
return 0;
|
|
35
36
|
for (let index = 0; index < 3; index += 1)
|
|
@@ -6,8 +6,8 @@ import { CliError } from "./output.js";
|
|
|
6
6
|
import { continueCommand, follow, outcomeFor, pickSourceFailure, recordedRefusal, runningLine, statusFetch, summarize } from "./operations.js";
|
|
7
7
|
import { CLI_VERSION } from "./version.js";
|
|
8
8
|
import { assertSafeAppRoot, readAppTree } from "./package.js";
|
|
9
|
-
import { deployBlocked, ensureLinkedApp } from "./integrations.js";
|
|
10
|
-
import { describeSourceChange, kitPaths, readAppProfile, readDeclaration, readKitLock, sourceDigest } from "./kit.js";
|
|
9
|
+
import { deployBlocked, ensureLinkedApp, kitLockBody } from "./integrations.js";
|
|
10
|
+
import { assertLockCompany, describeSourceChange, kitPaths, readAppProfile, readDeclaration, readKitLock, sourceDigest } from "./kit.js";
|
|
11
11
|
import { CHECKS_FAILED_HINT, checksFailedMessage, FLOW_CHECK, readReport, runChecks } from "./check.js";
|
|
12
12
|
import { runCommand } from "./local-runtime.js";
|
|
13
13
|
import { EMBEDDED_KIT_BUNDLE } from "./kit-bundle.js";
|
|
@@ -50,8 +50,10 @@ async function runDeploy(rootArg, governance, output, tenantId, options) {
|
|
|
50
50
|
assertSafeAppRoot(root);
|
|
51
51
|
// Only a kit app deploys from here: the checks, the gate and the profile all
|
|
52
52
|
// live in `.isomorph/`. Anything else deploys from the console.
|
|
53
|
-
|
|
53
|
+
const lock = await readKitLock(root);
|
|
54
|
+
if (!lock)
|
|
54
55
|
throw new CliError("KIT_REQUIRED", "This folder is not an Isomorph app: it has no .isomorph/kit.lock.json.", undefined, "Run `isomorph init --app-root .` first.");
|
|
56
|
+
assertLockCompany(lock, tenantId);
|
|
55
57
|
// The three values the person confirmed, printed so the transcript shows what
|
|
56
58
|
// was recorded; recorded on the operation before anything is uploaded.
|
|
57
59
|
const profile = await readAppProfile(root);
|
|
@@ -91,15 +93,16 @@ async function runDeploy(rootArg, governance, output, tenantId, options) {
|
|
|
91
93
|
output(`Isomorph is continuing operation ${note.operationRef}; no new deployment was started.`);
|
|
92
94
|
// One call opens the operation — or resumes it — and runs the deploy
|
|
93
95
|
// pre-flight first: a declared connection IT has not approved, a company
|
|
94
|
-
// whose AI setup is not ready for an app that calls it,
|
|
95
|
-
// no longer accepts
|
|
96
|
-
//
|
|
97
|
-
// exists. The reviewed profile
|
|
96
|
+
// whose AI setup is not ready for an app that calls it, a CLI the platform
|
|
97
|
+
// no longer accepts, or a kit older than the one Isomorph publishes would
|
|
98
|
+
// only park the deployment after the save, so governance refuses here, with
|
|
99
|
+
// every blocker at once, before any operation exists. The reviewed profile
|
|
100
|
+
// and the kit lock's bundle ride on the same call.
|
|
98
101
|
const body = {
|
|
99
102
|
environment: "preview",
|
|
100
103
|
app: { name: profile.name, description: profile.description, audience: profile.audience },
|
|
101
104
|
package: { sha256: archive.digest, bytes: archive.body.byteLength, manifest: { schema: "isomorph.source-package-manifest/1.0", files: manifest.files.map(file => ({ path: file.path, size: file.bytes, sha256: file.sha256 })) } },
|
|
102
|
-
graph, cliVersion: CLI_VERSION, kitBundleVersion: bundle.kitVersion, declaration, callsAi: await appCallsAi(root)
|
|
105
|
+
graph, cliVersion: CLI_VERSION, kitBundleVersion: bundle.kitVersion, declaration, callsAi: await appCallsAi(root), kitLock: kitLockBody(lock.bundle)
|
|
103
106
|
};
|
|
104
107
|
let opened;
|
|
105
108
|
try {
|
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { createForwarder } from "./forwarder.js";
|
|
6
6
|
import { readKitLock } from "./kit.js";
|
|
7
7
|
import { GovernanceClient, ensureLinkedApp, fileDeclaredRequests, IDENTITY_WORDS, renderGrantGroup } from "./integrations.js";
|
|
8
|
-
import { acquireDevLock, allocatePorts,
|
|
8
|
+
import { acquireDevLock, allocatePorts, assertMigrationNames, installDependencies, LocalRuntime, nodePackageCommand, pidAlive, readDevLock, recordDevChildren, releaseDevLock, runCommand } from "./local-runtime.js";
|
|
9
9
|
import { kitPaths } from "./kit.js";
|
|
10
10
|
import { CliError, safeError } from "./output.js";
|
|
11
11
|
/**
|
|
@@ -40,6 +40,7 @@ export async function startDev(root, options) {
|
|
|
40
40
|
const run = options.run ?? runCommand;
|
|
41
41
|
const env = options.env ?? process.env;
|
|
42
42
|
const runtime = new LocalRuntime(root, run);
|
|
43
|
+
await assertMigrationNames(root);
|
|
43
44
|
if (options.reset)
|
|
44
45
|
await runtime.reset(options.output);
|
|
45
46
|
const ports = await allocatePorts();
|
|
@@ -53,9 +54,7 @@ export async function startDev(root, options) {
|
|
|
53
54
|
await releaseDevLock(root);
|
|
54
55
|
};
|
|
55
56
|
try {
|
|
56
|
-
|
|
57
|
-
if (sdk === "missing")
|
|
58
|
-
options.output(`The kit SDK is not installed: set ISOMORPH_KIT_SDK_TARBALL to the bundle's ${options.bundle.sdk.package} tarball (or use a bundle with sdk.url).`);
|
|
57
|
+
await installDependencies(root, options.bundle, run, options.output);
|
|
59
58
|
await runtime.writeFiles(options.bundle, ports);
|
|
60
59
|
options.output("Preparing the kit-managed native runtime (public registry, no login).");
|
|
61
60
|
await runtime.pull(options.bundle);
|
|
@@ -163,6 +162,10 @@ export async function detachDev(root, options) {
|
|
|
163
162
|
const linked = await appId();
|
|
164
163
|
return { origin: running.origin, pid: running.pid, ...(linked ? { appId: linked } : {}) };
|
|
165
164
|
}
|
|
165
|
+
// Refused here, not in the child: the child's refusal would come back as
|
|
166
|
+
// "dev stopped before the app answered … read dev.log", one wrap away from
|
|
167
|
+
// the reason. `--detach` is the shape every agent runs.
|
|
168
|
+
await assertMigrationNames(root);
|
|
166
169
|
await mkdir(kitPaths(root).local, { recursive: true });
|
|
167
170
|
const logFd = openSync(devLogPath(root), "w");
|
|
168
171
|
let exited;
|
|
@@ -21,7 +21,7 @@ Isomorph SSO signs everyone in: no login forms, browser-trusted roles or public
|
|
|
21
21
|
|
|
22
22
|
## Data
|
|
23
23
|
|
|
24
|
-
SQL files
|
|
24
|
+
SQL files \`migrations/NNNN_name.sql\` (four digits, lowercase); applied migrations are frozen after the first deploy (add the next file). Every table: RLS on, one policy; ownership decided in SQL (the gate names any privilege a table still lacks):
|
|
25
25
|
|
|
26
26
|
\`\`\`sql
|
|
27
27
|
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
|
|
@@ -50,7 +50,7 @@ Shared-read, owner-write: the same, plus \`CREATE POLICY posts_read ON posts FOR
|
|
|
50
50
|
- \`CHECKS_FAILED\`: fix what \`isomorph check\` reports, then rerun.
|
|
51
51
|
- \`DEPLOY_BLOCKED\`, \`INTEGRATIONS_NOT_READY\`, \`AI_NOT_READY\`: IT's step is in the refusal; the app works meanwhile.
|
|
52
52
|
- \`CLI_UPGRADE_REQUIRED\`: \`npm i -g @isomorph.ai/cli\`.
|
|
53
|
-
- \`APP_NOT_FOUND\`:
|
|
53
|
+
- \`APP_NOT_FOUND\`: set \`appId\` and \`tenantId\` to \`""\` in \`.isomorph/kit.lock.json\` to relink.`,
|
|
54
54
|
integrations: `# Company systems (Slack, Gmail, warehouse)
|
|
55
55
|
|
|
56
56
|
## Call shape
|
|
@@ -2,6 +2,8 @@ import { basename } from "node:path";
|
|
|
2
2
|
import { linkIdempotencyKey, readDeclaration, readKitLock, requestResourceName, requestResources, writeKitLock, newKitLock } from "./kit.js";
|
|
3
3
|
import { CliError } from "./output.js";
|
|
4
4
|
import { CLI_VERSION } from "./version.js";
|
|
5
|
+
/** The lock's bundle as the preflight body carries it. */
|
|
6
|
+
export const kitLockBody = (bundle) => ({ kitVersion: bundle.kitVersion, sdk: { version: bundle.sdk.version } });
|
|
5
7
|
export class GovernanceClient {
|
|
6
8
|
apiUrl;
|
|
7
9
|
tenantId;
|
|
@@ -334,12 +336,14 @@ export function renderGrantGroup(group, nowMs = Date.now()) {
|
|
|
334
336
|
* governance whether this app can deploy to the environment: it files the
|
|
335
337
|
* declared access requests itself and answers with every blocker at once —
|
|
336
338
|
* the lanes IT still has to approve, the company's AI setup when the app calls
|
|
337
|
-
* governed AI,
|
|
339
|
+
* governed AI, a CLI older than the platform's minimum, and a kit older than
|
|
340
|
+
* the one Isomorph publishes. Prints each
|
|
338
341
|
* blocker's sentence and refuses once, before any operation exists.
|
|
339
342
|
*
|
|
340
343
|
* The code is `DEPLOY_BLOCKED` when the blockers are of more than one kind;
|
|
341
344
|
* when they are all of one kind it is the code readers already know for that
|
|
342
|
-
* kind (`INTEGRATIONS_NOT_READY`, `AI_NOT_READY`, `CLI_UPGRADE_REQUIRED
|
|
345
|
+
* kind (`INTEGRATIONS_NOT_READY`, `AI_NOT_READY`, `CLI_UPGRADE_REQUIRED`,
|
|
346
|
+
* `KIT_BUNDLE_STALE` — the code intake records for the same refusal).
|
|
343
347
|
* Observed before this: a builder was refused `INTEGRATIONS_NOT_READY`, fixed
|
|
344
348
|
* that, then `AI_NOT_READY`, then the pipeline refused `kit_bundle_incompatible`,
|
|
345
349
|
* each a separate cycle.
|
|
@@ -347,7 +351,8 @@ export function renderGrantGroup(group, nowMs = Date.now()) {
|
|
|
347
351
|
export async function assertDeployReady(root, client, tenantId, bundle, environment, output, callsAi) {
|
|
348
352
|
const declaration = await readDeclaration(root);
|
|
349
353
|
const appId = await ensureLinkedApp(root, client, tenantId, bundle);
|
|
350
|
-
const
|
|
354
|
+
const lock = await readKitLock(root);
|
|
355
|
+
const preflight = await client.deployPreflight(appId, { environment, callsAi, declaration, cliVersion: CLI_VERSION, ...(lock ? { kitLock: kitLockBody(lock.bundle) } : {}) });
|
|
351
356
|
if (preflight.ready && !blockersOf(preflight.blockers).length) {
|
|
352
357
|
output(`Isomorph confirmed the app can deploy to ${environment}.`);
|
|
353
358
|
return appId;
|
|
@@ -375,7 +380,7 @@ export function deployBlocked(value, environment, output) {
|
|
|
375
380
|
return new CliError(code, blockers.map(blocker => blocker.sentence).join(" ") || `Isomorph cannot deploy this app to ${environment} yet.`, undefined, fixes || undefined, undefined, { layer: "governance" });
|
|
376
381
|
}
|
|
377
382
|
/** The code readers already know for a refusal that is all of one kind. */
|
|
378
|
-
const BLOCKER_CODES = { integration: "INTEGRATIONS_NOT_READY", ai: "AI_NOT_READY", cli: "CLI_UPGRADE_REQUIRED" };
|
|
383
|
+
const BLOCKER_CODES = { integration: "INTEGRATIONS_NOT_READY", ai: "AI_NOT_READY", cli: "CLI_UPGRADE_REQUIRED", kit: "KIT_BUNDLE_STALE" };
|
|
379
384
|
/**
|
|
380
385
|
* Splits the operations the app calls on one connection by identity mode — the
|
|
381
386
|
* derived declaration's own `identity` (the gate derived it from the call's
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PUBLISHED_KIT_BUNDLE } from "./kit-bundle.manifest.js";
|
|
2
|
+
import { parseVersion } from "./config.js";
|
|
2
3
|
/**
|
|
3
4
|
* The local stack's supporting services. Not part of the published bundle: they run only under `isomorph dev`.
|
|
4
5
|
* MinIO is the exact image the pipeline's validation cell runs (data plane `transformbuild.validationMinIOImage`),
|
|
@@ -14,24 +15,35 @@ export const LOCAL_SERVICE_IMAGES = {
|
|
|
14
15
|
* The bundle this CLI was built with: `manifest.json` of the
|
|
15
16
|
* harbour-deployment-data-plane `publish-kit-bundle` artifact for the kit
|
|
16
17
|
* version pinned in package.json `harbour.kitBundle`, fetched at build time
|
|
17
|
-
* into `kit-bundle.manifest.ts` (the
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the
|
|
18
|
+
* into `kit-bundle.manifest.ts` (the native runtime lives in the kit's public
|
|
19
|
+
* registry, `public.ecr.aws/<alias>/`, pinned by digest so no login and no
|
|
20
|
+
* tag can substitute other bytes; the SDK is `@isomorph.ai/app-sdk` on npm at
|
|
21
|
+
* `sdk.version`). `isomorph init` records it in the app's kit.lock and writes
|
|
22
|
+
* the SDK dependency; `isomorph init --upgrade` shows what moved and bumps the
|
|
23
|
+
* dependency range; the hosted pipeline refuses a kit.lock whose
|
|
24
|
+
* `sdk.version` is older than the SDK surface its gate drives.
|
|
22
25
|
*/
|
|
23
|
-
export const EMBEDDED_KIT_BUNDLE = { ...PUBLISHED_KIT_BUNDLE, images: { ...LOCAL_SERVICE_IMAGES, ...PUBLISHED_KIT_BUNDLE.images } };
|
|
26
|
+
export const EMBEDDED_KIT_BUNDLE = { ...PUBLISHED_KIT_BUNDLE, sdk: { package: PUBLISHED_KIT_BUNDLE.sdk.package, version: publishedSdkVersion() }, images: { ...LOCAL_SERVICE_IMAGES, ...PUBLISHED_KIT_BUNDLE.images } };
|
|
27
|
+
/** The contracts type still allows a manifest without `sdk.version`; the generator refuses one, so a build that reaches here without it is a stale sync. */
|
|
28
|
+
function publishedSdkVersion() {
|
|
29
|
+
const version = PUBLISHED_KIT_BUNDLE.sdk.version;
|
|
30
|
+
if (!version || !parseVersion(version))
|
|
31
|
+
throw new Error("kit-bundle.manifest.ts names no sdk.version; run `npm run kit-bundle:sync` in packages/harbour-cli");
|
|
32
|
+
return version;
|
|
33
|
+
}
|
|
34
|
+
/** `"^<sdk.version>"`: the dependency range `init` writes and `init --upgrade` moves. */
|
|
35
|
+
export const sdkDependencyRange = (bundle) => `^${bundle.sdk.version}`;
|
|
24
36
|
export function isKitBundle(value) {
|
|
25
37
|
const record = value;
|
|
26
38
|
return Boolean(record && typeof record === "object" && record.schema === "isomorph.kit-bundle/1.0" && typeof record.kitVersion === "string"
|
|
27
|
-
&& record.sdk && typeof record.sdk.package === "string" && typeof record.sdk.
|
|
39
|
+
&& record.sdk && typeof record.sdk.package === "string" && typeof record.sdk.version === "string" && parseVersion(record.sdk.version) !== undefined
|
|
28
40
|
&& record.brief && typeof record.brief.fingerprint === "string" && record.declarationSchema === "isomorph.app-integrations/2.0");
|
|
29
41
|
}
|
|
30
|
-
/** Lines naming every digest that differs between two manifests, for `init --upgrade`. */
|
|
42
|
+
/** Lines naming every version and digest that differs between two manifests, for `init --upgrade`. */
|
|
31
43
|
export function bundleDiff(before, after) {
|
|
32
44
|
const fields = [
|
|
33
45
|
["kitVersion", before.kitVersion, after.kitVersion],
|
|
34
|
-
["sdk.
|
|
46
|
+
["sdk.version", before.sdk.version, after.sdk.version],
|
|
35
47
|
["brief.fingerprint", before.brief.fingerprint, after.brief.fingerprint]
|
|
36
48
|
];
|
|
37
49
|
return fields.filter(([, a, b]) => a !== b).map(([name, a, b]) => `${name}: ${a ?? "(none)"} -> ${b ?? "(none)"}`);
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
export const PUBLISHED_KIT_BUNDLE = {
|
|
2
2
|
"schema": "isomorph.kit-bundle/1.0",
|
|
3
|
-
"kitVersion": "0.
|
|
3
|
+
"kitVersion": "0.7.1",
|
|
4
4
|
"sdk": {
|
|
5
5
|
"package": "@isomorph.ai/app-sdk",
|
|
6
|
-
"version": "1.2.
|
|
7
|
-
"tarballSha256": "
|
|
8
|
-
"url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:
|
|
6
|
+
"version": "1.2.2",
|
|
7
|
+
"tarballSha256": "8953bb7eb6846bcddf48fd5407c098a13c8ad2cf249d702c35b5666d5af0ad53",
|
|
8
|
+
"url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:8953bb7eb6846bcddf48fd5407c098a13c8ad2cf249d702c35b5666d5af0ad53"
|
|
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:9d5510db8396cc27c509047f668b63a85ec3892db83cb658c045b2db2d7bddeb",
|
|
12
|
+
"sessionFixture": "public.ecr.aws/y6t4p3i8/harbour-session-fixture@sha256:aec4aa7dfc5128e31448f50088f15550846836ec7ef920368ca2d82b31dbf486"
|
|
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:3f4bbf08b814888f7ff64ff968362a944dfba742668927f04a331f8c4850da21",
|
|
17
|
+
"sha256": "3f4bbf08b814888f7ff64ff968362a944dfba742668927f04a331f8c4850da21"
|
|
18
18
|
},
|
|
19
19
|
"linuxX64": {
|
|
20
|
-
"url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:
|
|
21
|
-
"sha256": "
|
|
20
|
+
"url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:eea2faf265666cfe46c168d42b60cd0fc5431b43a72b3f409aa63dab7fee8524",
|
|
21
|
+
"sha256": "eea2faf265666cfe46c168d42b60cd0fc5431b43a72b3f409aa63dab7fee8524"
|
|
22
22
|
},
|
|
23
23
|
"windowsX64": {
|
|
24
|
-
"url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:
|
|
25
|
-
"sha256": "
|
|
24
|
+
"url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:3e1876f0ba701b838a707aa45e6cf4620e06f40aa8c855ce2a65437c752c369a",
|
|
25
|
+
"sha256": "3e1876f0ba701b838a707aa45e6cf4620e06f40aa8c855ce2a65437c752c369a"
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"brief": {
|
|
@@ -46,8 +46,29 @@ export async function readDeclaration(root) {
|
|
|
46
46
|
return { schema: record.schema, connections };
|
|
47
47
|
}
|
|
48
48
|
export function isKitLock(value) {
|
|
49
|
+
return kitLockProblem(value) === undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The first thing wrong with a lock, named, or undefined for a valid one. A
|
|
53
|
+
* builder told to relink by clearing `appId` and `tenantId` wrote `null`, then
|
|
54
|
+
* deleted the keys, and got "does not match isomorph.kit-lock/1.0" twice
|
|
55
|
+
* (bench, 2026-09-16); the refusal now says which field and what it takes.
|
|
56
|
+
*/
|
|
57
|
+
export function kitLockProblem(value) {
|
|
49
58
|
const record = value;
|
|
50
|
-
|
|
59
|
+
if (!record || typeof record !== "object")
|
|
60
|
+
return "it is not a JSON object";
|
|
61
|
+
if (record.schema !== "isomorph.kit-lock/1.0")
|
|
62
|
+
return "`schema` must be \"isomorph.kit-lock/1.0\"";
|
|
63
|
+
for (const field of ["appId", "tenantId"])
|
|
64
|
+
if (typeof record[field] !== "string")
|
|
65
|
+
return `\`${field}\` must be a string (\"\" to relink the app, never null or absent)`;
|
|
66
|
+
if (!isKitBundle(record.bundle))
|
|
67
|
+
return "`bundle` is not a kit bundle manifest";
|
|
68
|
+
for (const field of ["createdAt", "updatedAt"])
|
|
69
|
+
if (typeof record[field] !== "string")
|
|
70
|
+
return `\`${field}\` must be a string`;
|
|
71
|
+
return undefined;
|
|
51
72
|
}
|
|
52
73
|
export async function readKitLock(root) {
|
|
53
74
|
let raw;
|
|
@@ -64,10 +85,22 @@ export async function readKitLock(root) {
|
|
|
64
85
|
catch {
|
|
65
86
|
throw new CliError("KIT_LOCK_INVALID", ".isomorph/kit.lock.json is not valid JSON.");
|
|
66
87
|
}
|
|
67
|
-
|
|
68
|
-
|
|
88
|
+
const problem = kitLockProblem(parsed);
|
|
89
|
+
if (problem !== undefined)
|
|
90
|
+
throw new CliError("KIT_LOCK_INVALID", `.isomorph/kit.lock.json does not match isomorph.kit-lock/1.0: ${problem}.`, undefined, "Fix that field, or delete the file and run `isomorph init --app-root .` to write a fresh one.");
|
|
69
91
|
return parsed;
|
|
70
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* A lock naming another company is refused before any request is made:
|
|
95
|
+
* governance would answer `APP_NOT_FOUND` for an app it never registered, and
|
|
96
|
+
* that reads as a lost app when it is a person connected to the wrong one of
|
|
97
|
+
* their companies (`isomorph connect` lists them as `TENANT_AMBIGUOUS`).
|
|
98
|
+
*/
|
|
99
|
+
export function assertLockCompany(lock, tenantId) {
|
|
100
|
+
if (!lock?.appId || !lock.tenantId || lock.tenantId === tenantId)
|
|
101
|
+
return;
|
|
102
|
+
throw new CliError("APP_NOT_FOUND", `This app is linked to company "${lock.tenantId}" (.isomorph/kit.lock.json), but you are connected to "${tenantId}".`, undefined, `Run \`isomorph connect\` for "${lock.tenantId}" to work on the app there, or set "appId" and "tenantId" to "" in .isomorph/kit.lock.json to link it to "${tenantId}" as a new app.`, undefined, { paths: [".isomorph/kit.lock.json"] });
|
|
103
|
+
}
|
|
71
104
|
export async function writeKitLock(root, lock) {
|
|
72
105
|
const next = { ...lock, updatedAt: new Date().toISOString() };
|
|
73
106
|
await mkdir(dirname(kitPaths(root).lock), { recursive: true });
|
|
@@ -2,8 +2,10 @@ 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,
|
|
5
|
+
import { dirname, join, win32 } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { sdkDependencyRange } from "./kit-bundle.js";
|
|
8
|
+
import { parseVersion } from "./config.js";
|
|
7
9
|
import { kitPaths, projectName } from "./kit.js";
|
|
8
10
|
import { CliError } from "./output.js";
|
|
9
11
|
export function nodePackageCommand(tool, args, platform = process.platform, execPath = process.execPath) {
|
|
@@ -100,6 +102,21 @@ export function nativeGatewayConfig(project, ports, stateDir) {
|
|
|
100
102
|
async function migrationNames(root) {
|
|
101
103
|
return (await readdir(join(root, "migrations")).catch(() => [])).filter(name => name.endsWith(".sql")).sort();
|
|
102
104
|
}
|
|
105
|
+
/** The kit gate's rule for a migration file name (data plane kit_lane.go migrationNameRE), enforced here so `dev` and `check` agree. */
|
|
106
|
+
const MIGRATION_NAME = /^\d{4}_[a-z0-9][a-z0-9_-]{0,95}\.sql$/;
|
|
107
|
+
/**
|
|
108
|
+
* Refuses a migration the gate would refuse, before anything starts. `dev`
|
|
109
|
+
* used to apply `001_init.sql` happily and `check` then refused the same
|
|
110
|
+
* file (bench 2026-09-16, two imported prototypes): the rule is the gate's,
|
|
111
|
+
* stated in its words, at the first command that reads the folder.
|
|
112
|
+
*/
|
|
113
|
+
export async function assertMigrationNames(root) {
|
|
114
|
+
for (const name of await migrationNames(root)) {
|
|
115
|
+
if (MIGRATION_NAME.test(name))
|
|
116
|
+
continue;
|
|
117
|
+
throw new CliError("DEV_FAILED", `migrations/${name} must be a regular file named NNNN_name.sql (four digits, then lowercase letters, digits, _ or -).`, undefined, "Rename the file, then run this again.", undefined, { paths: [`migrations/${name}`] });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
103
120
|
export function internalDatabaseUrl(user = LOCAL.dbUser) { return `postgresql://${user}:${LOCAL.dbPassword}@127.0.0.1/${LOCAL.database}?sslmode=disable`; }
|
|
104
121
|
function uploadKey(project) { return createHash("sha256").update(`upload-key:${project}`).digest("base64"); }
|
|
105
122
|
// ---- Ports and lock ------------------------------------------------------------------
|
|
@@ -467,7 +484,7 @@ async function ensureNativeGateway(root, bundle) {
|
|
|
467
484
|
const directory = join(kitPaths(root).local, "runtime");
|
|
468
485
|
const target = join(directory, `isomorph-app-gateway-${runtime.sha256.slice(0, 12)}${process.platform === "win32" ? ".exe" : ""}`);
|
|
469
486
|
if (!(await exists(target))) {
|
|
470
|
-
const bytes = await
|
|
487
|
+
const bytes = await downloadBundleBlob(runtime.url);
|
|
471
488
|
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
472
489
|
if (digest !== runtime.sha256)
|
|
473
490
|
throw new CliError("KIT_BUNDLE_INCOMPATIBLE", `The native runtime digest ${digest} does not match the kit bundle ${runtime.sha256}.`);
|
|
@@ -497,59 +514,61 @@ export function parseSessionEnv(text) {
|
|
|
497
514
|
}
|
|
498
515
|
return env;
|
|
499
516
|
}
|
|
500
|
-
// ----
|
|
517
|
+
// ---- Dependencies and kit bundle blobs ----------------------------------------------------
|
|
501
518
|
/**
|
|
502
|
-
* The
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
* no tarball is available and node_modules lacks the package.
|
|
519
|
+
* The app's dependencies, `@isomorph.ai/app-sdk` among them, come from the public
|
|
520
|
+
* npm registry like any package (data plane ADR 0017): one plain `npm install`,
|
|
521
|
+
* nothing staged or pinned by the kit. One rule for `init`, `dev` and `check`:
|
|
522
|
+
* install when node_modules lacks the SDK (a fresh init or clone) or holds one
|
|
523
|
+
* outside the bundle's range (`init --upgrade` moved it); otherwise nothing runs.
|
|
508
524
|
*/
|
|
509
|
-
export async function
|
|
510
|
-
|
|
511
|
-
const stageDir = join(kitPaths(root).local, "sdk");
|
|
512
|
-
const staged = join(stageDir, `isomorph-app-sdk-${bundle.sdk.tarballSha256.slice(0, 12)}.tgz`);
|
|
513
|
-
const source = env.ISOMORPH_KIT_SDK_TARBALL?.trim();
|
|
514
|
-
if (!(await exists(staged))) {
|
|
515
|
-
let bytes;
|
|
516
|
-
if (source)
|
|
517
|
-
bytes = await readFile(source).catch(() => { throw new CliError("SDK_UNAVAILABLE", `ISOMORPH_KIT_SDK_TARBALL is not readable: ${source}`); });
|
|
518
|
-
else if (bundle.sdk.url) {
|
|
519
|
-
output(`Downloading ${bundle.sdk.package} from the kit bundle (sha256 ${bundle.sdk.tarballSha256.slice(0, 12)}…).`);
|
|
520
|
-
bytes = await downloadSdk(bundle.sdk.url, fetchImpl);
|
|
521
|
-
}
|
|
522
|
-
if (!bytes)
|
|
523
|
-
return installed ? "present" : "missing";
|
|
524
|
-
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
525
|
-
if (bundle.sdk.tarballSha256 !== "unknown" && digest !== bundle.sdk.tarballSha256)
|
|
526
|
-
throw new CliError("KIT_BUNDLE_INCOMPATIBLE", `The SDK tarball digest ${digest} does not match the kit bundle ${bundle.sdk.tarballSha256}. Run \`isomorph init --upgrade\` or use the matching tarball.`);
|
|
527
|
-
await mkdir(stageDir, { recursive: true });
|
|
528
|
-
await writeFile(staged, bytes);
|
|
529
|
-
}
|
|
530
|
-
else if (installed && (await readFile(join(root, "package.json"), "utf8").catch(() => "")).includes(relative(root, staged)))
|
|
525
|
+
export async function installDependencies(root, bundle, run, output) {
|
|
526
|
+
if (await sdkInstalled(root, bundle))
|
|
531
527
|
return "present";
|
|
532
|
-
output(`Installing ${bundle.sdk.package}
|
|
533
|
-
const npm = nodePackageCommand("npm", ["install", "--no-audit", "--no-fund"
|
|
528
|
+
output(`Installing the app's dependencies, ${bundle.sdk.package} ${sdkDependencyRange(bundle)} among them (npm install).`);
|
|
529
|
+
const npm = nodePackageCommand("npm", ["install", "--no-audit", "--no-fund"]);
|
|
534
530
|
const result = await run(npm.command, npm.args, { cwd: root, quiet: true });
|
|
535
531
|
if (result.code !== 0)
|
|
536
|
-
throw new CliError("
|
|
532
|
+
throw new CliError("NPM_INSTALL_FAILED", `npm install failed: ${result.stderr.trim().split("\n").at(-1) ?? "npm error"}`, undefined, "Fix the dependency problem npm names (package.json, the lockfile or the registry), then run this again.");
|
|
537
533
|
return "installed";
|
|
538
534
|
}
|
|
535
|
+
/** Whether node_modules holds the SDK at a version inside `^<bundle.sdk.version>`: same major (0.x: same minor) and not older. */
|
|
536
|
+
async function sdkInstalled(root, bundle) {
|
|
537
|
+
const manifest = await readFile(join(root, "node_modules", ...bundle.sdk.package.split("/"), "package.json"), "utf8").catch(() => undefined);
|
|
538
|
+
if (manifest === undefined)
|
|
539
|
+
return false;
|
|
540
|
+
let installed;
|
|
541
|
+
try {
|
|
542
|
+
installed = parseVersion(String(JSON.parse(manifest).version ?? ""))?.core;
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
const wanted = parseVersion(bundle.sdk.version)?.core;
|
|
548
|
+
if (!installed || !wanted)
|
|
549
|
+
return false;
|
|
550
|
+
if (installed[0] !== wanted[0] || (wanted[0] === 0 && installed[1] !== wanted[1]))
|
|
551
|
+
return false;
|
|
552
|
+
for (let index = 0; index < 3; index += 1)
|
|
553
|
+
if (installed[index] !== wanted[index])
|
|
554
|
+
return installed[index] > wanted[index];
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
539
557
|
/**
|
|
540
|
-
* Fetches
|
|
541
|
-
* stores it as a content-addressed OCI blob on a
|
|
542
|
-
* an anonymous GET with a Bearer challenge naming
|
|
543
|
-
* out pull tokens without credentials; the challenge
|
|
544
|
-
* request repeated. Redirects (a blob served from
|
|
545
|
-
* the bearer token is not forwarded across origins
|
|
546
|
-
* verifies the bytes against
|
|
547
|
-
* change where the bytes come from, never which bytes
|
|
558
|
+
* Fetches one of the kit bundle's blobs — the native runtime binary for this
|
|
559
|
+
* platform. The published bundle stores it as a content-addressed OCI blob on a
|
|
560
|
+
* public registry, which answers an anonymous GET with a Bearer challenge naming
|
|
561
|
+
* a token endpoint that hands out pull tokens without credentials; the challenge
|
|
562
|
+
* is followed once and the request repeated. Redirects (a blob served from
|
|
563
|
+
* object storage) are followed; the bearer token is not forwarded across origins
|
|
564
|
+
* by fetch itself. The caller verifies the bytes against the manifest's sha256 —
|
|
565
|
+
* a token or a redirect can change where the bytes come from, never which bytes
|
|
566
|
+
* are accepted.
|
|
548
567
|
*/
|
|
549
|
-
export async function
|
|
568
|
+
export async function downloadBundleBlob(url, fetchImpl = fetch) {
|
|
550
569
|
if (!/^https:\/\//.test(url))
|
|
551
|
-
throw new CliError("
|
|
552
|
-
const attempt = (headers = {}) => fetchImpl(url, { redirect: "follow", headers }).catch(error => { throw new CliError("
|
|
570
|
+
throw new CliError("KIT_RUNTIME_UNAVAILABLE", `The kit bundle's blob URL must be https: ${url}`);
|
|
571
|
+
const attempt = (headers = {}) => fetchImpl(url, { redirect: "follow", headers }).catch(error => { throw new CliError("KIT_RUNTIME_UNAVAILABLE", `The kit runtime could not be downloaded from ${url}: ${error instanceof Error ? error.message : String(error)}`); });
|
|
553
572
|
let response = await attempt();
|
|
554
573
|
if (response.status === 401) {
|
|
555
574
|
const challenge = parseBearerChallenge(response.headers.get("www-authenticate"));
|
|
@@ -567,7 +586,7 @@ export async function downloadSdk(url, fetchImpl = fetch) {
|
|
|
567
586
|
}
|
|
568
587
|
}
|
|
569
588
|
if (!response.ok)
|
|
570
|
-
throw new CliError("
|
|
589
|
+
throw new CliError("KIT_RUNTIME_UNAVAILABLE", `The kit runtime could not be downloaded from ${url} (HTTP ${response.status}).`);
|
|
571
590
|
return new Uint8Array(await response.arrayBuffer());
|
|
572
591
|
}
|
|
573
592
|
/** `Bearer realm="…",service="…",scope="…"` (RFC 6750 / distribution token auth); anything else is no challenge. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { bundleDiff } from "./kit-bundle.js";
|
|
3
|
+
import { bundleDiff, sdkDependencyRange } from "./kit-bundle.js";
|
|
4
4
|
import { agentSetup, MANAGED_END, MANAGED_START, upsertManagedBlock } from "./agent-setup.js";
|
|
5
5
|
import { defaultAppProfile, emptyDeclaration, newKitLock, readKitLock, renderAppProfile, writeKitLock } from "./kit.js";
|
|
6
6
|
import { CliError } from "./output.js";
|
|
@@ -80,8 +80,26 @@ export async function initKit(root, bundle, options = {}) {
|
|
|
80
80
|
}
|
|
81
81
|
else
|
|
82
82
|
result.kept.push(".isomorph/kit.lock.json");
|
|
83
|
+
// The SDK is a normal dependency of the app's own package.json: added when an
|
|
84
|
+
// existing app lacks it (adopt), moved to the bundle's range on --upgrade,
|
|
85
|
+
// otherwise left as the app has it. The CLI runs `npm install` afterwards.
|
|
86
|
+
if (existingPackage && await ensureSdkDependency(root, existingPackage, bundle, Boolean(options.upgrade)))
|
|
87
|
+
result.updated.push("package.json");
|
|
83
88
|
return result;
|
|
84
89
|
}
|
|
90
|
+
/** Writes `"<sdk package>": "^<sdk.version>"` into an existing package.json when it is missing, or when `move` is set and it differs; reports whether the file changed. */
|
|
91
|
+
async function ensureSdkDependency(root, pkg, bundle, move) {
|
|
92
|
+
const { package: name } = bundle.sdk;
|
|
93
|
+
const range = sdkDependencyRange(bundle);
|
|
94
|
+
const current = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
|
|
95
|
+
if (current === range || (current !== undefined && !move))
|
|
96
|
+
return false;
|
|
97
|
+
if (pkg.devDependencies)
|
|
98
|
+
delete pkg.devDependencies[name];
|
|
99
|
+
pkg.dependencies = { ...pkg.dependencies, [name]: range };
|
|
100
|
+
await writeFile(join(root, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
85
103
|
function isSupportedApp(pkg) {
|
|
86
104
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
87
105
|
return Boolean(deps.vite && deps.react);
|
|
@@ -162,10 +180,9 @@ function starterFiles(bundle) {
|
|
|
162
180
|
"package.json": `${JSON.stringify({
|
|
163
181
|
name: "isomorph-app", private: true, version: "0.1.0", type: "module",
|
|
164
182
|
scripts: { dev: "vite", build: "tsc --noEmit && vite build", preview: "vite preview" },
|
|
165
|
-
// The SDK is
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
dependencies: { [bundle.sdk.package]: bundle.sdk.version ?? "1.0.0", react: "^18.3.1", "react-dom": "^18.3.1" },
|
|
183
|
+
// The SDK is a public npm package like the rest: `init` runs `npm install` and
|
|
184
|
+
// `.isomorph/kit.lock.json` records the bundle's SDK version for the pipeline's floor.
|
|
185
|
+
dependencies: { [bundle.sdk.package]: sdkDependencyRange(bundle), react: "^18.3.1", "react-dom": "^18.3.1" },
|
|
169
186
|
devDependencies: { "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.4", typescript: "^5.6.3", vite: "^5.4.11" }
|
|
170
187
|
}, null, 2)}\n`,
|
|
171
188
|
"tsconfig.json": `${JSON.stringify({ compilerOptions: { target: "ES2022", lib: ["ES2022", "DOM", "DOM.Iterable"], module: "ESNext", moduleResolution: "Bundler", jsx: "react-jsx", strict: true, noEmit: true, skipLibCheck: true, isolatedModules: true }, include: ["src"] }, null, 2)}\n`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isomorph.ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Isomorph development kit CLI",
|
|
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.
|
|
40
|
+
"version": "0.7.1"
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
}
|