@isomorph.ai/cli 0.3.1-rc.2 → 0.3.2-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/harbour-cli/src/agent-setup.js +1 -1
- package/dist/packages/harbour-cli/src/cli.js +3 -2
- package/dist/packages/harbour-cli/src/integrations.js +7 -0
- package/dist/packages/harbour-cli/src/productionise.js +45 -3
- package/dist/packages/harbour-cli/src/upload.js +1 -1
- package/dist/packages/harbour-cli/src/version.js +1 -1
- package/package.json +1 -1
|
@@ -220,7 +220,7 @@ The kit's rules are the files beside this one: \`core.md\` before the first edit
|
|
|
220
220
|
- "does it work?", and before you report anything as working → open the dev link in your own browser, press the control you built or changed, and read what the app shows. A green \`isomorph check\` is not that proof: fixtures answer AI and company systems, so their refusals appear only when the control is really pressed. Test rendering, navigation, fixtures and approved reads automatically; a Send in development is real: reuse explicit authorization for that bounded test, or ask once if none exists (IT approval alone is not permission to send). If you have no browser, say that the button itself is untested.
|
|
221
221
|
- "I need Slack / Gmail / the warehouse / company data" → read \`integrations.md\` beside this skill first; then, in the same turn, read the catalog, declare only what the app calls, submit the access request yourself, and say READY or PENDING in one line.
|
|
222
222
|
- "summarise", "draft", "explain", "AI" → read \`ai.md\` beside this skill first; then one call behind a control they press.
|
|
223
|
-
- "ship it", "put it online", "let my team try it" → \`isomorph check --app-root . --json\` first, unasked; fix everything it finds: the same gates rerun in the cloud, minutes per attempt. Before sharing or deploying the private preview, show the exact proposed app name, description and audience that will be passed to Isomorph (say "only you" when the audience is empty); ask for one confirmation or correction covering all three; do not run \`productionise\` until they confirm. \`isomorph productionise --app-root . --name "<app name>" --description "<description>" --emails "<comma-separated audience>" --confirm-save --json\` (omit \`--emails\` for "only you"; \`--confirm-save\` is the approval they just gave). It records the confirmed setup before uploading or deploying the app, files the access request for each declared connection itself and refuses with \`INTEGRATIONS_NOT_READY\` naming what IT still has to approve, or \`AI_NOT_READY\` and the one IT step: relay that line only; the app works without them until then. \`result.deployment.protectedUrl\` is the private preview, for them and the confirmed audience after company sign-in. If your tool cuts it off, \`${continueCommand("<ref>")}\` continues the same deployment; never start another one to find out what happened; \`TRANSFORMING\` means building and checking.
|
|
223
|
+
- "ship it", "put it online", "let my team try it" → \`isomorph check --app-root . --json\` first, unasked; fix everything it finds: the same gates rerun in the cloud, minutes per attempt. Before sharing or deploying the private preview, show the exact proposed app name, description and audience that will be passed to Isomorph (say "only you" when the audience is empty); ask for one confirmation or correction covering all three; do not run \`productionise\` until they confirm. \`isomorph productionise --app-root . --name "<app name>" --description "<description>" --emails "<comma-separated audience>" --confirm-save --json\` (omit \`--emails\` for "only you"; \`--confirm-save\` is the approval they just gave). It records the confirmed setup before uploading or deploying the app, files the access request for each declared connection itself and refuses with \`INTEGRATIONS_NOT_READY\` naming what IT still has to approve, or \`AI_NOT_READY\` and the one IT step: relay that line only; the app works without them until then. Pass \`--session-notes\`: what in Isomorph cost you time, what you believed, what was true; \`""\` if nothing did. \`result.deployment.protectedUrl\` is the private preview, for them and the confirmed audience after company sign-in. If your tool cuts it off, \`${continueCommand("<ref>")}\` continues the same deployment; never start another one to find out what happened; \`TRANSFORMING\` means building and checking.
|
|
224
224
|
- "make it live for everyone", "go to production" → only after they have tried the preview, which \`--confirm-tested\` states. Before \`isomorph promote --operation <ref> --confirm-tested --json\`, show the exact app name, description and production audience again; ask for one confirmation or correction covering all three; never promote a profile or audience the person has not just seen and confirmed. Report the production link, or that operator approval is pending.
|
|
225
225
|
- "stop it" → \`isomorph stop --app-root .\` (data kept); \`isomorph dev --reset --app-root .\` deletes local data, only when they ask to start over.
|
|
226
226
|
- "every day at 2pm", "run this on a schedule", "send this automatically" → read \`jobs.md\` beside this skill first; then write the job, run it once with \`isomorph jobs run\`, and say the sentence it gives you.
|
|
@@ -26,6 +26,7 @@ const secretName = optionValue("--name");
|
|
|
26
26
|
const profileName = optionValue("--name");
|
|
27
27
|
const profileDescription = optionValue("--description");
|
|
28
28
|
const audienceEmails = optionValue("--emails");
|
|
29
|
+
const sessionNotes = optionValue("--session-notes");
|
|
29
30
|
const personal = args.includes("--personal");
|
|
30
31
|
const valueStdin = args.includes("--value-stdin");
|
|
31
32
|
/** The reply the save and promote prompts would otherwise wait to have typed, for a caller with no terminal. */
|
|
@@ -65,7 +66,7 @@ const usage = [
|
|
|
65
66
|
"Usage:",
|
|
66
67
|
" isomorph connect <work-email | company-start-url>",
|
|
67
68
|
" isomorph login | logout",
|
|
68
|
-
" isomorph productionise --app-root <path> [--include <relative-path>]... [--name <text> --description <text> [--emails a@co,b@co]] [--confirm-save] [--no-wait] [--max-wait <seconds>] [--json]",
|
|
69
|
+
" isomorph productionise --app-root <path> [--include <relative-path>]... [--name <text> --description <text> [--emails a@co,b@co]] [--session-notes <text>] [--confirm-save] [--no-wait] [--max-wait <seconds>] [--json]",
|
|
69
70
|
" isomorph status --operation <reference> [--wait] [--max-wait <seconds>] [--json]",
|
|
70
71
|
" isomorph retry --operation <reference> [--no-wait] [--max-wait <seconds>] [--json]",
|
|
71
72
|
" isomorph promote --operation <reference> [--app-root <path>] [--confirm-tested] [--no-wait] [--max-wait <seconds>] [--json] (--app-root: also confirm the company's AI setup for production when the app calls governed AI)",
|
|
@@ -264,7 +265,7 @@ else {
|
|
|
264
265
|
? { displayName: profileName, description: profileDescription, audienceEmails: audienceEmails ? audienceEmails.split(",").map(value => value.trim()).filter(Boolean) : [] }
|
|
265
266
|
: undefined;
|
|
266
267
|
await assertKitCurrent(appRoot(root));
|
|
267
|
-
const result = await productionise(root, client, progress, tenant, includePaths, { waitForDeployment: !noWait, waitOptions, integrations: { governance: new GovernanceClient(config.apiUrl, token, tenant), bundle: EMBEDDED_KIT_BUNDLE }, ...(confirmSave ? { readApproval: async () => SAVE_APPROVAL } : {}), ...(confirmedAppSetup ? { confirmedAppSetup } : {}) });
|
|
268
|
+
const result = await productionise(root, client, progress, tenant, includePaths, { waitForDeployment: !noWait, waitOptions, integrations: { governance: new GovernanceClient(config.apiUrl, token, tenant), bundle: EMBEDDED_KIT_BUNDLE }, ...(confirmSave ? { readApproval: async () => SAVE_APPROVAL } : {}), ...(confirmedAppSetup ? { confirmedAppSetup } : {}), ...(sessionNotes ? { sessionNotes } : {}) });
|
|
268
269
|
envelope = operationEnvelope(result.result, result.operationRef, CLI_VERSION);
|
|
269
270
|
}
|
|
270
271
|
else {
|
|
@@ -33,6 +33,13 @@ export class GovernanceClient {
|
|
|
33
33
|
aiReadiness(appId, environment) {
|
|
34
34
|
return this.call("POST", `/v1/development/apps/${encodeURIComponent(appId)}/ai/readiness`, { environment });
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* What the building agent says cost it time in Isomorph itself. Never blocks a
|
|
38
|
+
* deployment: the caller discards every failure, and an empty note files nothing.
|
|
39
|
+
*/
|
|
40
|
+
submitFeedback(appId, input) {
|
|
41
|
+
return this.call("POST", `/v1/development/apps/${encodeURIComponent(appId)}/feedback`, input);
|
|
42
|
+
}
|
|
36
43
|
async call(method, path, body) {
|
|
37
44
|
const url = `${this.apiUrl.replace(/\/$/, "")}${path}`;
|
|
38
45
|
const response = await this.fetchImpl(url, { method, headers: { authorization: `Bearer ${this.token}`, "x-harbour-tenant": this.tenantId, accept: "application/json", ...(body ? { "content-type": "application/json" } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}), redirect: "error" })
|
|
@@ -16,6 +16,33 @@ import { FLOW_CHECK, preflightKitGate, readReport } from "./check.js";
|
|
|
16
16
|
* `isomorph.stage-approval/1.0` attestation governance receives is unchanged, byte for byte. */
|
|
17
17
|
export const SAVE_APPROVAL = "Approved.";
|
|
18
18
|
export async function productionise(rootArg, client, output, tenantId, includePaths = [], options = {}) {
|
|
19
|
+
const deployment = await deploy(rootArg, client, output, tenantId, includePaths, options);
|
|
20
|
+
await reportSession(rootArg, deployment.operationRef, options);
|
|
21
|
+
return deployment;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The building agent's own account of what cost it time, filed once the
|
|
25
|
+
* deployment has resolved so the note is written with the outcome in hand.
|
|
26
|
+
*
|
|
27
|
+
* Every failure is swallowed by design. This is a side-channel: a governance
|
|
28
|
+
* outage, a missing Atlas token, or a runtime too old to have the feedback route
|
|
29
|
+
* must never fail, delay or alter a deployment the builder already asked for.
|
|
30
|
+
*/
|
|
31
|
+
async function reportSession(rootArg, operationRef, options) {
|
|
32
|
+
const notes = options.sessionNotes?.trim();
|
|
33
|
+
if (!notes || !options.integrations)
|
|
34
|
+
return;
|
|
35
|
+
try {
|
|
36
|
+
const appId = options.appId ?? (await readKitLock(resolve(rootArg)))?.appId;
|
|
37
|
+
if (!appId)
|
|
38
|
+
return;
|
|
39
|
+
await options.integrations.governance.submitFeedback(appId, {
|
|
40
|
+
operationRef, cliVersion: CLI_VERSION, kitBundleVersion: options.integrations.bundle.kitVersion, notes
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch { /* never surfaced: the deployment is what the builder asked for. */ }
|
|
44
|
+
}
|
|
45
|
+
async function deploy(rootArg, client, output, tenantId, includePaths = [], options = {}) {
|
|
19
46
|
const root = resolve(rootArg);
|
|
20
47
|
output(`Isomorph is checking ${basename(root)}.`);
|
|
21
48
|
if (root === parse(root).root || root === resolve(homedir()))
|
|
@@ -127,9 +154,7 @@ export async function productionise(rootArg, client, output, tenantId, includePa
|
|
|
127
154
|
throw new CliError("APP_IDENTITY_MISSING", "Isomorph did not return the app identity for this operation.", operationRef);
|
|
128
155
|
if (kitAppId && appId !== kitAppId)
|
|
129
156
|
throw new CliError("APP_IDENTITY_MISMATCH", "Isomorph returned a different app identity than the one linked in .isomorph/kit.lock.json.", operationRef);
|
|
130
|
-
|
|
131
|
-
await recordKitAppId(root, appId, tenantId);
|
|
132
|
-
pending = { ...pending, appId, sourceDigest: (await sourceDigest(root)).digest };
|
|
157
|
+
pending = { ...pending, appId };
|
|
133
158
|
await writePending(root, pending);
|
|
134
159
|
if (options.confirmedAppSetup && !pending.appSetupConfirmed) {
|
|
135
160
|
await confirmProfile(client, operationRef, options.confirmedAppSetup, output);
|
|
@@ -145,6 +170,17 @@ export async function productionise(rootArg, client, output, tenantId, includePa
|
|
|
145
170
|
if (prepared.nextAction?.tool && !matchesTool(prepared.nextAction.tool, "isomorph_complete_source_upload"))
|
|
146
171
|
throw new CliError("UNSUPPORTED_NEXT_STEP", "Isomorph returned an unsupported next step.", operationRef);
|
|
147
172
|
const archive = await archiveForManifest(root, prepared.acceptedManifest);
|
|
173
|
+
// The canonical identity lands in kit.lock so a later `integrations request` reuses it instead
|
|
174
|
+
// of linking a second app — written here, at the first moment it is safe to write it, and never
|
|
175
|
+
// earlier. `.isomorph/kit.lock.json` is one of the app's own deployable files; the package is
|
|
176
|
+
// built from bytes read at the top of this run, and `archiveForManifest` re-reads every file and
|
|
177
|
+
// compares its sha256 before uploading. Recording the id before that read made the CLI refuse
|
|
178
|
+
// its own edit — "The app changed while it was being packaged (.isomorph/kit.lock.json)" —
|
|
179
|
+
// deterministically on the first deployment of every new app, because `recordKitAppId` is a
|
|
180
|
+
// no-op once the id matches, so only a lock that has no id yet changes. Re-running succeeded and
|
|
181
|
+
// hid the cause (2026-09-15). Both reads are done by this line and the archive is in memory, so
|
|
182
|
+
// the app is still linked before anything can fail on the network.
|
|
183
|
+
await recordKitAppId(root, appId, tenantId);
|
|
148
184
|
const parts = await putMultipart(prepared.sourceUpload, archive.body, (sent, total) => output(`Isomorph is uploading the app package (${Math.floor(sent * 100 / total)}%).`));
|
|
149
185
|
pending = { ...pending, uploadCompletion: { intentId: prepared.sourceUpload.intentId, parts, clientSha256: archive.digest } };
|
|
150
186
|
await writePending(root, pending);
|
|
@@ -157,6 +193,12 @@ export async function productionise(rootArg, client, output, tenantId, includePa
|
|
|
157
193
|
}
|
|
158
194
|
if (!appId)
|
|
159
195
|
throw new CliError("APP_IDENTITY_MISSING", "The saved operation has no app identity.", operationRef);
|
|
196
|
+
// The lock now carries the app id, so the tree differs from the one this operation was opened
|
|
197
|
+
// against; re-pin the digest a resumed run compares with.
|
|
198
|
+
if (pending.sourceDigest !== (await sourceDigest(root)).digest) {
|
|
199
|
+
pending = { ...pending, sourceDigest: (await sourceDigest(root)).digest };
|
|
200
|
+
await writePending(root, pending);
|
|
201
|
+
}
|
|
160
202
|
if (!prior?.sourceSave)
|
|
161
203
|
await waitForIntake(client, operationRef);
|
|
162
204
|
let execution = prior?.sourceSave?.status && prior.sourceSave.status !== "AWAITING_APPROVAL" ? { status: prior.sourceSave.status } : await execute(client, operationRef, appId, graph);
|
|
@@ -27,7 +27,7 @@ export async function archiveForManifest(root, manifest) {
|
|
|
27
27
|
throw new Error("The app changed while it was being packaged.");
|
|
28
28
|
const content = new Uint8Array(await readFile(absolute));
|
|
29
29
|
if (content.byteLength !== entry.bytes || await sha256Bytes(content) !== entry.sha256)
|
|
30
|
-
throw new Error(`The app changed while it was being packaged (${entry.path}).`);
|
|
30
|
+
throw new Error(`The app changed while it was being packaged (${entry.path}). Run the same productionise command again: it continues the saved operation and packages the app as it is now.`);
|
|
31
31
|
files.push({ path: relative(rootAbsolute, absolute).split(sep).join("/"), content });
|
|
32
32
|
}
|
|
33
33
|
const body = new Uint8Array(deterministicZip(files.sort((left, right) => left.path.localeCompare(right.path))));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.3.
|
|
1
|
+
export const CLI_VERSION = "0.3.2-rc.1";
|