@isomorph.ai/cli 0.5.1 → 0.6.0
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 +3 -6
- package/dist/packages/harbour-cli/src/deploy.js +1 -3
- package/dist/packages/harbour-cli/src/guide.js +5 -16
- package/dist/packages/harbour-cli/src/integrations.js +14 -23
- package/dist/packages/harbour-cli/src/kit-bundle.manifest.js +9 -9
- package/dist/packages/harbour-cli/src/kit.js +13 -24
- package/dist/packages/harbour-cli/src/output.js +12 -2
- package/dist/packages/harbour-cli/src/starter.js +6 -9
- package/package.json +2 -2
|
@@ -118,7 +118,7 @@ Rules live beside this file: \`core.md\` before the first edit; \`integrations.m
|
|
|
118
118
|
| "run it", "show me" | \`isomorph dev --app-root . --detach --json\`; give them \`result.origin\` (the first start takes a minute or two). Locally they are a fixture user. |
|
|
119
119
|
| "check it", "is it ready?" | \`isomorph check --app-root . --json\` (the output is the report). Failures in the app's code are yours: fix and re-check until clean, after every change and before every ship, unasked and never offered as a choice. |
|
|
120
120
|
| "does it work?" (and before reporting anything as working) | open the dev link in your own browser, press the control you built or changed, read what the app shows; a green \`isomorph check\` is not that proof (fixtures answer AI and company systems). A Send in development is real: reuse explicit authorization or ask once. No browser: say the button is untested. |
|
|
121
|
-
| "I need Slack / Gmail / the warehouse" | read \`integrations.md\` beside this skill first; then, in the same turn: catalog,
|
|
121
|
+
| "I need Slack / Gmail / the warehouse" | read \`integrations.md\` beside this skill first; then, in the same turn: catalog, call only what it lists (literals), submit the request yourself, say READY or PENDING in one line. |
|
|
122
122
|
| "summarise", "draft", "AI" | read \`ai.md\` beside this skill first; then one call behind a control they press. |
|
|
123
123
|
| "ship it", "let my team try it" | show the person the three values in \`.isomorph/app.json\` (name, description, audience — "only you" when empty) and correct the file as they say; then \`isomorph deploy --app-root . --json\` (it runs the checks when needed and refuses once naming every blocker and its fix: relay that line; the app works meanwhile). Give them \`result.deployment.protectedUrl\`. \`--session-notes\`: what cost you time in Isomorph, or \`""\`. |
|
|
124
124
|
| "make it live" | only after they have tried the preview: \`isomorph promote --operation <ref> --app-root . --confirm-tested --json\`. Report the production link, or that operator approval is pending. |
|
|
@@ -69,14 +69,11 @@ export async function runChecks(root, options) {
|
|
|
69
69
|
// failure ended the run as `fetch failed` (CLI_FAILED) with no report written.
|
|
70
70
|
const skipped = (why) => record({ name: "integrations", status: "not_run", detail: why });
|
|
71
71
|
const lock = await readKitLock(root).catch(() => undefined);
|
|
72
|
-
const { declaration, errors } = await readDeclaration(root);
|
|
73
72
|
if (!lock?.appId)
|
|
74
73
|
skipped("the app is not linked yet; `isomorph dev` (signed in) or `isomorph integrations request` links it");
|
|
75
|
-
else if (errors.length)
|
|
76
|
-
skipped(`.isomorph/integrations.json is invalid: ${errors[0]}`);
|
|
77
74
|
else {
|
|
78
75
|
try {
|
|
79
|
-
integrations = await testIntegrationReads(lock.appId, options.governance,
|
|
76
|
+
integrations = await testIntegrationReads(lock.appId, options.governance, await readDeclaration(root), output);
|
|
80
77
|
if (!integrations.length)
|
|
81
78
|
output("No READY development grants with read operations; real integrations were not exercised.");
|
|
82
79
|
}
|
|
@@ -252,8 +249,8 @@ function indentLines(detail) { return detail.split("\n").map((line, index) => in
|
|
|
252
249
|
*/
|
|
253
250
|
export async function writeGateFiles(root, files, output) {
|
|
254
251
|
for (const [path, content] of Object.entries(files)) {
|
|
255
|
-
if (!/^\.isomorph\/checks\/[A-Za-z0-9._-]
|
|
256
|
-
throw new CliError("LOCAL_RUNTIME_FAILED", `The kit gate named a file
|
|
252
|
+
if (!/^\.isomorph\/(checks\/[A-Za-z0-9._-]+|integrations\.json)$/.test(path))
|
|
253
|
+
throw new CliError("LOCAL_RUNTIME_FAILED", `The kit gate named a file it does not own: ${path}.`);
|
|
257
254
|
const target = join(root, path);
|
|
258
255
|
if (content === null) {
|
|
259
256
|
await rm(target, { force: true });
|
|
@@ -63,9 +63,7 @@ async function runDeploy(rootArg, governance, output, tenantId, options) {
|
|
|
63
63
|
// that is blank too; this only says so before the checks spend a minute.
|
|
64
64
|
if (!profile.name.trim() || !profile.description.trim())
|
|
65
65
|
throw new CliError("PROFILE_INCOMPLETE", "The app needs a name and a description in .isomorph/app.json before it can deploy.", undefined, "Edit `name` and `description` in .isomorph/app.json (show them to the person first), then run this again.");
|
|
66
|
-
const
|
|
67
|
-
if (errors.length && !errors[0].startsWith(".isomorph/integrations.json is missing"))
|
|
68
|
-
throw new CliError("DECLARATION_INVALID", `.isomorph/integrations.json is invalid: ${errors[0]}`);
|
|
66
|
+
const declaration = await readDeclaration(root);
|
|
69
67
|
// Link first: the app's identity lands in `.isomorph/kit.lock.json`, which is
|
|
70
68
|
// one of the deployable files, so a report pinned after the link stays fresh
|
|
71
69
|
// through the checks and the package is built from the linked tree.
|
|
@@ -53,13 +53,12 @@ Shared-read, owner-write: the same, plus \`CREATE POLICY posts_read ON posts FOR
|
|
|
53
53
|
- \`APP_NOT_FOUND\`: clear \`appId\` and \`tenantId\` in \`.isomorph/kit.lock.json\` to relink.`,
|
|
54
54
|
integrations: `# Company systems (Slack, Gmail, warehouse)
|
|
55
55
|
|
|
56
|
-
##
|
|
56
|
+
## Call shape
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
One call, this exact shape, never a wrapper function; \`connection\`, \`operation\`, \`resource\`, \`mode\` and \`input.columns\` are catalog literals (logical names, never IDs or variables). \`isomorph check\` derives \`.isomorph/integrations.json\` from them (committed, never edited); \`dev\`/\`deploy\` file the request.
|
|
59
59
|
|
|
60
|
-
\`\`\`
|
|
61
|
-
"
|
|
62
|
-
"sales-warehouse": { "kind": "database", "operations": { "warehouse.view.read": { "identity": "app", "resources": { "weekly_sales": { "columns": ["week", "total"] } } } } }
|
|
60
|
+
\`\`\`ts
|
|
61
|
+
await isomorph.integrations.execute("sales-warehouse", { operation: "warehouse.view.read", resource: "weekly_sales", input: { columns: ["week", "total"], limit: 100 } });
|
|
63
62
|
\`\`\`
|
|
64
63
|
|
|
65
64
|
## Catalog
|
|
@@ -70,16 +69,6 @@ Never guess a connection, channel, table, view, mailbox or column: a guess is re
|
|
|
70
69
|
|
|
71
70
|
\`isomorph integrations request <connection> --reason "<why>" --app-root . --json\` files the one request IT approves once for every environment (\`isomorph dev\` and \`isomorph deploy\` file it too); submit it yourself, in the same turn. \`isomorph integrations status --app-root . --json\`: READY, use it; PENDING, say "IT has to approve this; the app works meanwhile". \`RESOURCE_NOT_APPROVED\`: IT adds the channel, view or mailbox in the console (Controls & integrations); then run the same request again.
|
|
72
71
|
|
|
73
|
-
## Call shape
|
|
74
|
-
|
|
75
|
-
Always this exact shape, never a wrapper function: the kit gate reads the direct call.
|
|
76
|
-
|
|
77
|
-
\`\`\`ts
|
|
78
|
-
const report = await isomorph.integrations.execute("sales-warehouse", {
|
|
79
|
-
operation: "warehouse.view.read", resource: "weekly_sales", input: { columns: ["week", "total"], limit: 100 }
|
|
80
|
-
});
|
|
81
|
-
\`\`\`
|
|
82
|
-
|
|
83
72
|
## Consent
|
|
84
73
|
|
|
85
74
|
Only user-identity operations need it, built in unasked: the control calls \`isomorph.integrations.connect(connection)\`, follows \`authorizationUrl\` on \`consent_required\`, and returns to a connected state. Never on page load; app-identity operations never call connect.
|
|
@@ -90,7 +79,7 @@ A send (\`slack.message.post\`, \`gmail.message.send\`) runs only from an explic
|
|
|
90
79
|
|
|
91
80
|
## Slack modes
|
|
92
81
|
|
|
93
|
-
\`"
|
|
82
|
+
\`execute("company-slack", { …, mode: "app" })\` posts as the bot under \`slack.displayName\`/\`iconEmoji\` in \`.isomorph/app.json\`; \`mode: "user"\` posts as the person after consent (\`USER_RECONNECT_REQUIRED\`: offer Connect again). One mode per connection (\`MODE_REQUIRED\`); an unapproved mode is \`MODE_NOT_GRANTED\`, never swapped. \`RESOURCE_NOT_APPROVED\` on a post: the bot is not in the channel; \`/invite @Isomorph AI\`.`,
|
|
94
83
|
ai: `# Governed AI
|
|
95
84
|
|
|
96
85
|
AI goes through \`isomorph.ai\` only: one \`isomorph.ai.chat({ messages, maxTokens })\` call on the app's one client (\`src/isomorph.client.ts\`), never through a wrapper function (the gate reads only the direct call), behind a control the person presses — never on load, in an effect or a timer. Never add an OpenAI/Anthropic/Gemini key, SDK or URL: the governed AI gateway holds the key and IT sees every call. Send \`messages\` and \`maxTokens\` and nothing else; a refusal with \`unsupported_request_capability\` names a field the company's AI route does not accept — remove that field. A refusal with \`AI_NOT_ENABLED\` means IT has not enabled an AI provider yet: say so in one line and keep the app working without AI.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
-
import { linkIdempotencyKey,
|
|
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
5
|
export class GovernanceClient {
|
|
@@ -164,12 +164,7 @@ function settled(requests) {
|
|
|
164
164
|
* so governance is not called.
|
|
165
165
|
*/
|
|
166
166
|
export async function fileDeclaredRequests(root, client, tenantId, bundle) {
|
|
167
|
-
const
|
|
168
|
-
if (errors.length) {
|
|
169
|
-
if (errors[0].startsWith(".isomorph/integrations.json is missing"))
|
|
170
|
-
return undefined;
|
|
171
|
-
throw new CliError("DECLARATION_INVALID", `.isomorph/integrations.json is invalid: ${errors[0]}`);
|
|
172
|
-
}
|
|
167
|
+
const declaration = await readDeclaration(root);
|
|
173
168
|
const connections = Object.keys(declaration.connections);
|
|
174
169
|
if (!connections.length)
|
|
175
170
|
return undefined;
|
|
@@ -183,9 +178,7 @@ export async function fileDeclaredRequests(root, client, tenantId, bundle) {
|
|
|
183
178
|
* for up to 30 s; PENDING is reported as pending, never as ready.
|
|
184
179
|
*/
|
|
185
180
|
export async function requestIntegrations(root, client, tenantId, bundle, options) {
|
|
186
|
-
const
|
|
187
|
-
if (errors.length)
|
|
188
|
-
throw new CliError("DECLARATION_INVALID", `.isomorph/integrations.json is invalid: ${errors[0]}`);
|
|
181
|
+
const declaration = await readDeclaration(root);
|
|
189
182
|
const appId = await ensureLinkedApp(root, client, tenantId, bundle);
|
|
190
183
|
const submitted = settled(await submitRequests(client, appId, declaration, root, options.connection, options.reason, options.operations)).map(item => ({ ...item, state: item.outcome.state }));
|
|
191
184
|
const sleep = options.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
|
|
@@ -352,9 +345,7 @@ export function renderGrantGroup(group, nowMs = Date.now()) {
|
|
|
352
345
|
* each a separate cycle.
|
|
353
346
|
*/
|
|
354
347
|
export async function assertDeployReady(root, client, tenantId, bundle, environment, output, callsAi) {
|
|
355
|
-
const
|
|
356
|
-
if (errors.length && !errors[0].startsWith(".isomorph/integrations.json is missing"))
|
|
357
|
-
throw new CliError("DECLARATION_INVALID", `.isomorph/integrations.json is invalid: ${errors[0]}`);
|
|
348
|
+
const declaration = await readDeclaration(root);
|
|
358
349
|
const appId = await ensureLinkedApp(root, client, tenantId, bundle);
|
|
359
350
|
const preflight = await client.deployPreflight(appId, { environment, callsAi, declaration, cliVersion: CLI_VERSION });
|
|
360
351
|
if (preflight.ready && !blockersOf(preflight.blockers).length) {
|
|
@@ -386,25 +377,25 @@ export function deployBlocked(value, environment, output) {
|
|
|
386
377
|
/** The code readers already know for a refusal that is all of one kind. */
|
|
387
378
|
const BLOCKER_CODES = { integration: "INTEGRATIONS_NOT_READY", ai: "AI_NOT_READY", cli: "CLI_UPGRADE_REQUIRED" };
|
|
388
379
|
/**
|
|
389
|
-
* Splits the
|
|
390
|
-
* declaration's own `identity` (the gate
|
|
391
|
-
*
|
|
392
|
-
* that carries `slack.message.post` also carries the
|
|
393
|
-
* presentation, so IT sees "posts as"
|
|
380
|
+
* Splits the operations the app calls on one connection by identity mode — the
|
|
381
|
+
* derived declaration's own `identity` (the gate derived it from the call's
|
|
382
|
+
* `mode` or the operation's only identity and checked it against the closed
|
|
383
|
+
* set). The app-mode part that carries `slack.message.post` also carries the
|
|
384
|
+
* connection's presentation (from `.isomorph/app.json`), so IT sees "posts as"
|
|
385
|
+
* on the request. A connection or operation the app does not call cannot be
|
|
386
|
+
* asked for: the declaration is the source, so the fix is the call.
|
|
394
387
|
*/
|
|
395
388
|
export function requestScope(declaration, connection, only) {
|
|
396
389
|
const declared = declaration.connections[connection];
|
|
397
390
|
if (!declared)
|
|
398
|
-
throw new CliError("
|
|
391
|
+
throw new CliError("CONNECTION_NOT_CALLED", `The app does not call ${connection}; add the isomorph.integrations.execute call first, then run \`isomorph check\`.`);
|
|
399
392
|
const names = (only?.length ? only : Object.keys(declared.operations));
|
|
400
393
|
const unknown = names.filter(name => !declared.operations[name]);
|
|
401
394
|
if (unknown.length)
|
|
402
|
-
throw new CliError("
|
|
395
|
+
throw new CliError("OPERATION_NOT_CALLED", `The app does not call ${unknown.join(", ")} on ${connection}; add the call first, then run \`isomorph check\`.`);
|
|
403
396
|
const byMode = new Map();
|
|
404
397
|
for (const name of names) {
|
|
405
|
-
const
|
|
406
|
-
const declaredMode = declared.operations[name].identity;
|
|
407
|
-
const mode = allowed.includes(declaredMode) ? declaredMode : allowed[0];
|
|
398
|
+
const mode = declared.operations[name].identity;
|
|
408
399
|
const entry = byMode.get(mode) ?? { operations: [], resources: new Map() };
|
|
409
400
|
entry.operations.push(name);
|
|
410
401
|
for (const resource of requestResources(declared.operations[name]))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const PUBLISHED_KIT_BUNDLE = {
|
|
2
2
|
"schema": "isomorph.kit-bundle/1.0",
|
|
3
|
-
"kitVersion": "0.
|
|
3
|
+
"kitVersion": "0.6.0",
|
|
4
4
|
"sdk": {
|
|
5
5
|
"package": "@isomorph.ai/app-sdk",
|
|
6
6
|
"version": "1.2.1",
|
|
@@ -8,21 +8,21 @@ export const PUBLISHED_KIT_BUNDLE = {
|
|
|
8
8
|
"url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:607e3896aa4116d82d0473677d38a68fecd4855a250cd44e038780290c335c67"
|
|
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:7f7ae24c24d81cf2951ce8ae9033e252373c17f3a31299e05714103068273232",
|
|
12
|
+
"sessionFixture": "public.ecr.aws/y6t4p3i8/harbour-session-fixture@sha256:14803bd23b64c0ba5aa68022716c339a772ce0664c739dabda3457ec27978bb2"
|
|
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:42705c1b74d45f08b4f926e98b7ae15bcc8688d26d1424a74cf2d03472c9b332",
|
|
17
|
+
"sha256": "42705c1b74d45f08b4f926e98b7ae15bcc8688d26d1424a74cf2d03472c9b332"
|
|
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:853563fb277f49eb8862ee06227d5faba8c82cb8de304357083c19d9f616fa81",
|
|
21
|
+
"sha256": "853563fb277f49eb8862ee06227d5faba8c82cb8de304357083c19d9f616fa81"
|
|
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:8709b4922c8c5e3ba19662999df4beb97d01fe8a8a85b32d6025088234714a50",
|
|
25
|
+
"sha256": "8709b4922c8c5e3ba19662999df4beb97d01fe8a8a85b32d6025088234714a50"
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"brief": {
|
|
@@ -5,30 +5,18 @@ import { basename, dirname, join, parse, resolve } from "node:path";
|
|
|
5
5
|
import { scanWorkspace } from "../../../src/analyzer.js";
|
|
6
6
|
import { isKitBundle } from "./kit-bundle.js";
|
|
7
7
|
import { CliError } from "./output.js";
|
|
8
|
-
/**
|
|
9
|
-
* Closed set: each operation has one connection kind and the identity modes it
|
|
10
|
-
* may run under — exactly one, except slack.message.post, which the app
|
|
11
|
-
* declares (`app` posts as the company's Slack app under its IT-approved name,
|
|
12
|
-
* `user` posts as the signed-in person after their consent). Gmail is
|
|
13
|
-
* user-mode only (two reads and one send as the signed-in person).
|
|
14
|
-
*/
|
|
15
|
-
export const OPERATIONS = {
|
|
16
|
-
"slack.channel.history": { identities: ["user"], kind: "saas" },
|
|
17
|
-
"slack.message.post": { identities: ["app", "user"], kind: "saas" },
|
|
18
|
-
"gmail.thread.list": { identities: ["user"], kind: "saas" },
|
|
19
|
-
"gmail.message.read": { identities: ["user"], kind: "saas" },
|
|
20
|
-
"gmail.message.send": { identities: ["user"], kind: "saas" },
|
|
21
|
-
"warehouse.view.read": { identities: ["app"], kind: "database" }
|
|
22
|
-
};
|
|
23
8
|
/** Explicit reads `isomorph check --integrations` may run; every other operation is a send and never runs during checks. */
|
|
24
9
|
export const READ_OPERATIONS = ["slack.channel.history", "gmail.thread.list", "warehouse.view.read"];
|
|
25
10
|
/** Reads whose input needs an identifier from a prior read (a message id): listed as reads, exercised only through the list. */
|
|
26
11
|
export const DEPENDENT_READ_OPERATIONS = ["gmail.message.read"];
|
|
27
12
|
/**
|
|
28
|
-
* Reads the
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
13
|
+
* Reads the declaration `isomorph check` derived from the app's
|
|
14
|
+
* `isomorph.integrations.execute` calls and wrote to `.isomorph/integrations.json`
|
|
15
|
+
* (data plane ADR 0016). Nobody edits the file: its closed rules are the kit
|
|
16
|
+
* gate's `declaration` check, so a defect is named once, at the call site, by
|
|
17
|
+
* `isomorph check` and by CodeBuild alike. A missing file is the empty
|
|
18
|
+
* declaration (`init` writes one); a file that is not JSON is not the gate's
|
|
19
|
+
* and the fix is to run the check.
|
|
32
20
|
*/
|
|
33
21
|
export function emptyDeclaration() { return { schema: "isomorph.app-integrations/2.0", connections: {} }; }
|
|
34
22
|
export function resourceNames(operation) { return Array.isArray(operation.resources) ? operation.resources : Object.keys(operation.resources); }
|
|
@@ -42,20 +30,20 @@ export async function readDeclaration(root) {
|
|
|
42
30
|
raw = await readFile(kitPaths(root).declaration, "utf8");
|
|
43
31
|
}
|
|
44
32
|
catch {
|
|
45
|
-
return
|
|
33
|
+
return emptyDeclaration();
|
|
46
34
|
}
|
|
47
35
|
let parsed;
|
|
48
36
|
try {
|
|
49
37
|
parsed = JSON.parse(raw);
|
|
50
38
|
}
|
|
51
39
|
catch {
|
|
52
|
-
|
|
40
|
+
parsed = undefined;
|
|
53
41
|
}
|
|
54
42
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
55
|
-
|
|
43
|
+
throw new CliError("DECLARATION_UNREADABLE", ".isomorph/integrations.json is not the file `isomorph check` writes.", undefined, "Run `isomorph check --app-root .`: it derives the file from the app's isomorph.integrations.execute calls.");
|
|
56
44
|
const record = parsed;
|
|
57
45
|
const connections = record.connections && typeof record.connections === "object" && !Array.isArray(record.connections) ? record.connections : {};
|
|
58
|
-
return {
|
|
46
|
+
return { schema: record.schema, connections };
|
|
59
47
|
}
|
|
60
48
|
export function isKitLock(value) {
|
|
61
49
|
const record = value;
|
|
@@ -156,7 +144,8 @@ export async function readAppProfile(root) {
|
|
|
156
144
|
problems.push("audience must be a list of email addresses (an empty list means only you)");
|
|
157
145
|
if (problems.length)
|
|
158
146
|
throw new CliError("APP_PROFILE_INVALID", `.isomorph/app.json is invalid: ${problems.join("; ")}.`, undefined, "Fix the file, then run this again.");
|
|
159
|
-
|
|
147
|
+
const slack = record.slack && typeof record.slack === "object" && typeof record.slack.displayName === "string" ? { slack: { displayName: record.slack.displayName, ...(typeof record.slack.iconEmoji === "string" ? { iconEmoji: record.slack.iconEmoji } : {}) } } : {};
|
|
148
|
+
return { schema: "isomorph.app/1.0", name: record.name.trim(), description: (record.description ?? "").trim(), audience: record.audience.map(entry => entry.trim()), ...slack };
|
|
160
149
|
}
|
|
161
150
|
/** Stable per-project identity for the kit-managed local state. */
|
|
162
151
|
export function projectName(root, suffix = "") {
|
|
@@ -26,7 +26,13 @@ export function safeError(error) {
|
|
|
26
26
|
const paths = error instanceof CliError && error.paths?.length ? error.paths : undefined;
|
|
27
27
|
// Structured, so it is neither bounded nor redacted: a command's choices are its commands, and a refusal's blockers are what the reader acts on.
|
|
28
28
|
const details = error instanceof CliError && error.details && Object.keys(error.details).length ? error.details : undefined;
|
|
29
|
-
|
|
29
|
+
// A refusal another layer authored — the gate, governance, the pipeline — is
|
|
30
|
+
// the whole answer and is already bounded where it was written; cutting it
|
|
31
|
+
// here deleted the table name from a pipeline refusal on 2026-09-16 (the cut
|
|
32
|
+
// fell exactly on "No journey was generated for `notes`"). Only this CLI's
|
|
33
|
+
// own prose keeps the cap.
|
|
34
|
+
const message = (layer === "cli" ? redact(raw) : scrub(raw)) || NO_DETAIL;
|
|
35
|
+
return { code, layer, message, ...(hint ? { remediationHint: hint } : {}), ...(paths ? { paths } : {}), ...(details ? { details } : {}) };
|
|
30
36
|
}
|
|
31
37
|
/** Anything that must never reach a terminal, a log or an envelope. */
|
|
32
38
|
const SECRETS = /https?:\/\/\S+|Bearer\s+\S+|(?:x-harbour|authorization|content-type)[^\n]*/gi;
|
|
@@ -70,8 +76,12 @@ const ELISION = " \u2026 ";
|
|
|
70
76
|
* phrasings, it deletes words the server actually said, and it is the kind of
|
|
71
77
|
* meaning-guessing the classifier deleted above was deleted for.
|
|
72
78
|
*/
|
|
79
|
+
/** Redacted and whitespace-folded, never bounded: what a server-authored refusal reaches the reader as. */
|
|
80
|
+
function scrub(value) {
|
|
81
|
+
return value.replace(SECRETS, "").replace(/\s+/g, " ").trim();
|
|
82
|
+
}
|
|
73
83
|
function redact(value) {
|
|
74
|
-
const said = value
|
|
84
|
+
const said = scrub(value);
|
|
75
85
|
if (said.length <= DETAIL_BOUND)
|
|
76
86
|
return said;
|
|
77
87
|
const keep = DETAIL_BOUND - ELISION.length;
|
|
@@ -136,14 +136,11 @@ function kitFiles(root) {
|
|
|
136
136
|
// Name, description and audience: what `isomorph deploy` prints before it starts and records
|
|
137
137
|
// with the deployment. The folder name and an empty audience are only a starting point.
|
|
138
138
|
".isomorph/app.json": renderAppProfile(defaultAppProfile(root)),
|
|
139
|
-
// No connection
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
// beside the `isomorph` skill (guide.ts). They are not in this file: it is read
|
|
145
|
-
// with JSON.parse here and again by the pipeline's source intake, so it cannot
|
|
146
|
-
// carry comments.
|
|
139
|
+
// No connection: the starter calls no company system. The kit gate derives
|
|
140
|
+
// this file from the app's isomorph.integrations.execute calls on every
|
|
141
|
+
// `isomorph check` (data plane ADR 0016); it is committed and packaged for
|
|
142
|
+
// governance, never edited. `init` writes the empty one so a fresh app has
|
|
143
|
+
// a declaration before its first check.
|
|
147
144
|
".isomorph/integrations.json": `${JSON.stringify(emptyDeclaration(), null, 2)}\n`
|
|
148
145
|
};
|
|
149
146
|
}
|
|
@@ -196,7 +193,7 @@ Created by \`isomorph init\`: a Vite + React app that runs on Isomorph. Sign-in,
|
|
|
196
193
|
|
|
197
194
|
The kit's rules are in the \`isomorph\` skill that \`isomorph agent-setup\` installs for Claude Code (\`~/.claude/skills/isomorph/\`) and Codex (\`~/.codex/skills/isomorph/\`): \`core.md\` (identity, data, files, realtime, commands, error codes), \`integrations.md\` (Slack, Gmail, warehouse), \`ai.md\` and \`jobs.md\`. The managed block in CLAUDE.md / AGENTS.md points there.
|
|
198
195
|
|
|
199
|
-
\`.isomorph/checks/\` is regenerated on each check and gitignored — put your own checks in \`tests/<name>.mjs\`. \`.isomorph/integrations.json\`
|
|
196
|
+
\`.isomorph/checks/\` is regenerated on each check and gitignored — put your own checks in \`tests/<name>.mjs\`. \`.isomorph/integrations.json\` is derived by \`isomorph check\` from the app's \`isomorph.integrations.execute\` calls and committed, never edited. \`.isomorph/app.json\` holds the app's name, description and audience.
|
|
200
197
|
`;
|
|
201
198
|
const VITE_CONFIG = `import { defineConfig } from "vite";
|
|
202
199
|
import react from "@vitejs/plugin-react";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isomorph.ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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.6.0"
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
}
|