@isomorph.ai/cli 0.7.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.
@@ -6,7 +6,7 @@ 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";
@@ -252,7 +252,9 @@ else {
252
252
  // from the folder (the current one unless --app-root says otherwise) and,
253
253
  // without --operation, follow the deployment last started from it.
254
254
  const target = appRoot(root ?? ".");
255
- const appId = (await readKitLock(target))?.appId;
255
+ const lock = await readKitLock(target);
256
+ assertLockCompany(lock, tenant);
257
+ const appId = lock?.appId;
256
258
  if (!appId)
257
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>.");
258
260
  const ref = operationRef ?? (await readDeployNote(target, tenant))?.operationRef;
@@ -7,7 +7,7 @@ import { continueCommand, follow, outcomeFor, pickSourceFailure, recordedRefusal
7
7
  import { CLI_VERSION } from "./version.js";
8
8
  import { assertSafeAppRoot, readAppTree } from "./package.js";
9
9
  import { deployBlocked, ensureLinkedApp, kitLockBody } from "./integrations.js";
10
- import { describeSourceChange, kitPaths, readAppProfile, readDeclaration, readKitLock, sourceDigest } from "./kit.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";
@@ -53,6 +53,7 @@ async function runDeploy(rootArg, governance, output, tenantId, options) {
53
53
  const lock = await readKitLock(root);
54
54
  if (!lock)
55
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);
56
57
  // The three values the person confirmed, printed so the transcript shows what
57
58
  // was recorded; recorded on the operation before anything is uploaded.
58
59
  const profile = await readAppProfile(root);
@@ -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, installDependencies, LocalRuntime, nodePackageCommand, pidAlive, readDevLock, recordDevChildren, releaseDevLock, runCommand } from "./local-runtime.js";
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();
@@ -161,6 +162,10 @@ export async function detachDev(root, options) {
161
162
  const linked = await appId();
162
163
  return { origin: running.origin, pid: running.pid, ...(linked ? { appId: linked } : {}) };
163
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);
164
169
  await mkdir(kitPaths(root).local, { recursive: true });
165
170
  const logFd = openSync(devLogPath(root), "w");
166
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 in \`migrations/\`; 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):
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\`: clear \`appId\` and \`tenantId\` in \`.isomorph/kit.lock.json\` to relink.`,
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
@@ -1,6 +1,6 @@
1
1
  export const PUBLISHED_KIT_BUNDLE = {
2
2
  "schema": "isomorph.kit-bundle/1.0",
3
- "kitVersion": "0.7.0",
3
+ "kitVersion": "0.7.1",
4
4
  "sdk": {
5
5
  "package": "@isomorph.ai/app-sdk",
6
6
  "version": "1.2.2",
@@ -8,21 +8,21 @@ export const PUBLISHED_KIT_BUNDLE = {
8
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:3669ac07c51d5160463ba9985dbdca035b6944ae4ebbaea57ea02eeed83fb8ee",
12
- "sessionFixture": "public.ecr.aws/y6t4p3i8/harbour-session-fixture@sha256:67650669576642104b03afd97bd1463d507bc9e1e83fb7314693ce7d3bdfe373"
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:aa7c6b1da2a51c6c3ff98582ed3f34959a8081c6af005961f0d4737da83402cf",
17
- "sha256": "aa7c6b1da2a51c6c3ff98582ed3f34959a8081c6af005961f0d4737da83402cf"
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:2c20bb1f3ad39917e6514aef77f1588efb433996079d3e844ef2a2cb28fabde4",
21
- "sha256": "2c20bb1f3ad39917e6514aef77f1588efb433996079d3e844ef2a2cb28fabde4"
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:892199c64f47d1ade017e2f5c78750f0cad01516fd0bd191490a6b79c1af9ff8",
25
- "sha256": "892199c64f47d1ade017e2f5c78750f0cad01516fd0bd191490a6b79c1af9ff8"
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
- return Boolean(record && typeof record === "object" && record.schema === "isomorph.kit-lock/1.0" && typeof record.appId === "string" && typeof record.tenantId === "string" && isKitBundle(record.bundle) && typeof record.createdAt === "string" && typeof record.updatedAt === "string");
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
- if (!isKitLock(parsed))
68
- throw new CliError("KIT_LOCK_INVALID", ".isomorph/kit.lock.json does not match isomorph.kit-lock/1.0.");
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 });
@@ -102,6 +102,21 @@ export function nativeGatewayConfig(project, ports, stateDir) {
102
102
  async function migrationNames(root) {
103
103
  return (await readdir(join(root, "migrations")).catch(() => [])).filter(name => name.endsWith(".sql")).sort();
104
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
+ }
105
120
  export function internalDatabaseUrl(user = LOCAL.dbUser) { return `postgresql://${user}:${LOCAL.dbPassword}@127.0.0.1/${LOCAL.database}?sslmode=disable`; }
106
121
  function uploadKey(project) { return createHash("sha256").update(`upload-key:${project}`).digest("base64"); }
107
122
  // ---- Ports and lock ------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/cli",
3
- "version": "0.7.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.7.0"
40
+ "version": "0.7.1"
41
41
  }
42
42
  }
43
43
  }