@isomorph.ai/cli 0.3.3 → 0.3.4
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 +23 -0
- package/dist/packages/harbour-cli/src/cli.js +12 -4
- package/dist/packages/harbour-cli/src/kit.js +34 -3
- package/dist/packages/harbour-cli/src/productionise.js +15 -3
- package/dist/packages/harbour-cli/src/starter.js +1 -1
- package/dist/packages/harbour-cli/src/version.js +1 -1
- package/package.json +1 -1
|
@@ -7,6 +7,28 @@ import { CliError } from "./output.js";
|
|
|
7
7
|
import { DEPENDENT_READ_OPERATIONS, READ_OPERATIONS, kitPaths, readDeclaration, readKitLock, resourceNames, sourceDigest } from "./kit.js";
|
|
8
8
|
import { LocalRuntime, allocatePorts, ensureSdk, freePort, nodePackageCommand, readDevLock, runCommand, runningOrigin } from "./local-runtime.js";
|
|
9
9
|
import { CLI_VERSION } from "./version.js";
|
|
10
|
+
/**
|
|
11
|
+
* What actually failed, in the refusal itself.
|
|
12
|
+
*
|
|
13
|
+
* The report is already in hand at the call site, and a refusal that spends its
|
|
14
|
+
* message on a file path costs the reader a whole turn to learn anything —
|
|
15
|
+
* `productionise` already names the failing checks when it refuses to deploy
|
|
16
|
+
* (`nameList(failed)`), and `check` did not. The detail of the first failure is
|
|
17
|
+
* carried too, capped, because "which check" is a smaller answer than "why".
|
|
18
|
+
*/
|
|
19
|
+
export function checksFailedMessage(report) {
|
|
20
|
+
const failed = report.checks.filter(check => check.status === "fail");
|
|
21
|
+
if (failed.length === 0)
|
|
22
|
+
return "One or more checks failed.";
|
|
23
|
+
const names = failed.map(check => check.name).join(", ");
|
|
24
|
+
const first = failed.find(check => check.detail)?.detail?.replace(/\s+/g, " ").trim();
|
|
25
|
+
const detail = first ? ` ${first.length > DETAIL_CAP ? `${first.slice(0, DETAIL_CAP - 1)}\u2026` : first}` : "";
|
|
26
|
+
return `${failed.length === 1 ? "Check" : "Checks"} failed: ${names}.${detail}`;
|
|
27
|
+
}
|
|
28
|
+
/** The bound on the reason carried into the refusal; the rest stays in the report. */
|
|
29
|
+
const DETAIL_CAP = 200;
|
|
30
|
+
/** Where the rest of it is, and the one command that re-runs them. */
|
|
31
|
+
export const CHECKS_FAILED_HINT = "Full detail is in .isomorph/local/check-report.json; fix what it reports, then run `isomorph check --app-root .` again.";
|
|
10
32
|
/** The name the gate reports operation coverage under — the pipeline's rule for it. */
|
|
11
33
|
export const FLOW_CHECK = "flow.check-failed";
|
|
12
34
|
/**
|
|
@@ -78,6 +100,7 @@ export async function runChecks(root, options) {
|
|
|
78
100
|
schema: "isomorph.check-report/1.0",
|
|
79
101
|
createdAt: new Date().toISOString(),
|
|
80
102
|
sourceDigest: source.digest,
|
|
103
|
+
sourceFiles: source.entries,
|
|
81
104
|
toolchain: { node: process.version, cliVersion: CLI_VERSION, bundle: { kitVersion: bundle.kitVersion, sdkTarballSha256: bundle.sdk.tarballSha256, appGateway: bundle.images.appGateway, sessionFixture: bundle.images.sessionFixture, briefFingerprint: bundle.brief.fingerprint } },
|
|
82
105
|
checks,
|
|
83
106
|
gate,
|
|
@@ -12,7 +12,7 @@ import { initKit } from "./starter.js";
|
|
|
12
12
|
import { agentPaths, agentSetup, cliVersionLines } from "./agent-setup.js";
|
|
13
13
|
import { startDev } from "./dev.js";
|
|
14
14
|
import { ensureSdk, LocalRuntime, readDevLock, releaseDevLock, runCommand } from "./local-runtime.js";
|
|
15
|
-
import { runChecks } from "./check.js";
|
|
15
|
+
import { CHECKS_FAILED_HINT, checksFailedMessage, runChecks } from "./check.js";
|
|
16
16
|
import { assertAiReady, companySystemsLine, GovernanceClient, groupGrants, IDENTITY_WORDS, integrationsCatalog, integrationsStatus, renderGrantGroup, renderIntegrationsCatalog, requestIntegrations } from "./integrations.js";
|
|
17
17
|
import { runJob } from "./jobs.js";
|
|
18
18
|
const args = process.argv.slice(2);
|
|
@@ -112,8 +112,12 @@ const exitAfterWriting = (code, out, err = "") => { process.stderr.write(err, ()
|
|
|
112
112
|
const USAGE_REFUSALS = ["INTEGRATIONS_NOT_READY", "AI_NOT_READY", "NOT_A_MEMBER", "TENANT_AMBIGUOUS", "NOT_A_START_LINK", "PLATFORM_UNREACHABLE"];
|
|
113
113
|
if (command === "--version" || command === "version") {
|
|
114
114
|
process.stdout.write(`${CLI_VERSION}\n`);
|
|
115
|
+
// `-h` was matched anywhere in argv while `--help` was only recognised as the
|
|
116
|
+
// first token, so `isomorph productionise --help` fell through to argument
|
|
117
|
+
// validation and exited 2 with the usage block as an error. Both spellings now
|
|
118
|
+
// mean the same thing wherever they appear.
|
|
115
119
|
}
|
|
116
|
-
else if (!command || command === "help" || command === "--help" || args.includes("-h")) {
|
|
120
|
+
else if (!command || command === "help" || command === "--help" || args.includes("-h") || args.includes("--help")) {
|
|
117
121
|
process.stdout.write(usage);
|
|
118
122
|
}
|
|
119
123
|
else if (command === "agent-setup") {
|
|
@@ -202,8 +206,12 @@ else {
|
|
|
202
206
|
governance = new GovernanceClient(config.apiUrl, token, config.tenantId);
|
|
203
207
|
}
|
|
204
208
|
const report = await runChecks(target, { run: runCommand, bundle, output: progress, governance });
|
|
205
|
-
if (!report.passed)
|
|
206
|
-
|
|
209
|
+
if (!report.passed) {
|
|
210
|
+
const message = checksFailedMessage(report);
|
|
211
|
+
exitAfterWriting(1, json
|
|
212
|
+
? `${JSON.stringify({ ...summaryEnvelope(report), status: "FAILED", error: { code: "CHECKS_FAILED", message, remediationHint: CHECKS_FAILED_HINT } })}\n`
|
|
213
|
+
: `${message}\n${CHECKS_FAILED_HINT}\n`);
|
|
214
|
+
}
|
|
207
215
|
else
|
|
208
216
|
emit(summaryEnvelope(report));
|
|
209
217
|
}
|
|
@@ -136,14 +136,45 @@ export function projectName(root, suffix = "") {
|
|
|
136
136
|
export function linkIdempotencyKey(tenantId, root) {
|
|
137
137
|
return createHash("sha256").update(`${tenantId}${resolve(root)}`).digest("hex");
|
|
138
138
|
}
|
|
139
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* sha256 over the tracked app files (same boundary as productionise), so any
|
|
141
|
+
* edit changes it — and, per file, the hash of its contents, so a stale check
|
|
142
|
+
* can say WHICH path moved rather than only that something did.
|
|
143
|
+
*/
|
|
140
144
|
export async function sourceDigest(root) {
|
|
141
145
|
const graph = await scanWorkspace(root, { sourceBoundary: "root" });
|
|
142
146
|
const files = [...graph.deploymentScope.includedFiles].sort();
|
|
143
147
|
const hash = createHash("sha256");
|
|
148
|
+
const entries = {};
|
|
144
149
|
for (const path of files) {
|
|
150
|
+
const content = await readFile(join(root, path));
|
|
145
151
|
hash.update(path).update("\0");
|
|
146
|
-
hash.update(
|
|
152
|
+
hash.update(content).update("\0");
|
|
153
|
+
entries[path] = createHash("sha256").update(content).digest("hex");
|
|
147
154
|
}
|
|
148
|
-
return { digest: `sha256:${hash.digest("hex")}`, files };
|
|
155
|
+
return { digest: `sha256:${hash.digest("hex")}`, files, entries };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* What moved between the tree the checks ran on and the tree being deployed.
|
|
159
|
+
*
|
|
160
|
+
* A builder hit this three times in one session: their agent wrote scratch files
|
|
161
|
+
* into a gitignored folder inside the app root, which is still inside the
|
|
162
|
+
* deployment boundary, so every write invalidated the checks. The refusal named
|
|
163
|
+
* the code but not the path, so nothing pointed at the folder. Naming the paths
|
|
164
|
+
* is the whole fix.
|
|
165
|
+
*/
|
|
166
|
+
export function describeSourceChange(before, after) {
|
|
167
|
+
if (!before)
|
|
168
|
+
return "";
|
|
169
|
+
const added = Object.keys(after).filter(path => !(path in before)).sort();
|
|
170
|
+
const removed = Object.keys(before).filter(path => !(path in after)).sort();
|
|
171
|
+
const changed = Object.keys(after).filter(path => path in before && before[path] !== after[path]).sort();
|
|
172
|
+
const some = (paths, verb) => {
|
|
173
|
+
if (!paths.length)
|
|
174
|
+
return "";
|
|
175
|
+
const shown = paths.slice(0, 3).join(", ");
|
|
176
|
+
const rest = paths.length - 3;
|
|
177
|
+
return `${verb} ${shown}${rest > 0 ? ` and ${rest} more` : ""}`;
|
|
178
|
+
};
|
|
179
|
+
return [some(added, "added"), some(changed, "changed"), some(removed, "removed")].filter(Boolean).join("; ");
|
|
149
180
|
}
|
|
@@ -10,7 +10,7 @@ import { confirmAudience, confirmProfile, follow, getAppSetup, pickSourceFailure
|
|
|
10
10
|
import { CLI_VERSION } from "./version.js";
|
|
11
11
|
import { isProhibitedSecretPath } from "../../../src/secret-paths.js";
|
|
12
12
|
import { assertAiReady, assertPreviewIntegrationsReady } from "./integrations.js";
|
|
13
|
-
import { kitPaths, readKitLock, recordKitAppId, sourceDigest } from "./kit.js";
|
|
13
|
+
import { describeSourceChange, kitPaths, readKitLock, recordKitAppId, sourceDigest } from "./kit.js";
|
|
14
14
|
import { FLOW_CHECK, preflightKitGate, readReport } from "./check.js";
|
|
15
15
|
/** What the maker types at the save prompt; `--confirm-save` supplies this same literal, so the
|
|
16
16
|
* `isomorph.stage-approval/1.0` attestation governance receives is unchanged, byte for byte. */
|
|
@@ -302,8 +302,20 @@ async function assertChecksPassedForTree(root, output) {
|
|
|
302
302
|
if (!report)
|
|
303
303
|
throw new CliError("CHECKS_NOT_RUN", `This app keeps ${retained.length} check${plural} that say whether it still works, and ${retained.length === 1 ? "it has" : "they have"} never been run against this code. Isomorph does not deploy an app whose own checks have not passed.`, undefined, RUN_THE_CHECKS);
|
|
304
304
|
const tree = await sourceDigest(root);
|
|
305
|
-
if (report.sourceDigest !== tree.digest)
|
|
306
|
-
|
|
305
|
+
if (report.sourceDigest !== tree.digest) {
|
|
306
|
+
// Name the paths. Without them the refusal is true but unactionable: a
|
|
307
|
+
// builder whose agent wrote scratch files into a gitignored folder inside
|
|
308
|
+
// the app root hit this three times before finding the folder, because
|
|
309
|
+
// everything inside the root is inside the deployment boundary whether git
|
|
310
|
+
// tracks it or not. Reports written before CLI 0.3.4 carry no file hashes,
|
|
311
|
+
// so the sentence degrades to what it always said.
|
|
312
|
+
// The paths lead, and the whole sentence stays inside output.ts's
|
|
313
|
+
// DETAIL_BOUND. That bound keeps both ENDS and elides the middle, precisely
|
|
314
|
+
// because a filename once sat at index 266 and was never printed — so a
|
|
315
|
+
// path placed mid-sentence is put in the one position the bounding deletes.
|
|
316
|
+
const moved = describeSourceChange(report.sourceFiles, tree.entries);
|
|
317
|
+
throw new CliError("CHECKS_STALE", `The app's code has changed since its checks last ran${moved ? ` (${moved})` : ""}. Isomorph does not deploy an app whose checks have not passed for this exact code; everything inside the app folder counts, including files git ignores.`, undefined, RUN_THE_CHECKS);
|
|
318
|
+
}
|
|
307
319
|
const checks = Array.isArray(report.checks) ? report.checks : [];
|
|
308
320
|
const failed = checks.filter(check => check.status === "fail").map(check => check.name);
|
|
309
321
|
if (failed.length || report.passed !== true)
|
|
@@ -18,7 +18,7 @@ export async function initKit(root, bundle, options = {}) {
|
|
|
18
18
|
const existingPackage = await readJson(join(root, "package.json"));
|
|
19
19
|
const emptyDir = !existingPackage && !(await exists(join(root, "src")));
|
|
20
20
|
if (existingPackage && !isSupportedApp(existingPackage))
|
|
21
|
-
throw new CliError("APP_UNSUPPORTED", "isomorph init supports an empty directory or an existing Vite + React app (package.json must depend on vite and react).");
|
|
21
|
+
throw new CliError("APP_UNSUPPORTED", "isomorph init supports an empty directory or an existing Vite + React app (package.json must depend on vite and react).", undefined, "Run `isomorph init --app-root <new-empty-folder>` to start a Vite + React app, then move this app's code into it.");
|
|
22
22
|
// An app from a CLI older than 0.2.0 is moved to this layout by `--upgrade`
|
|
23
23
|
// and by nothing else: a plain `init` over it would write a second, empty kit
|
|
24
24
|
// beside the one it has. Refused before anything, the user-level skill included, is written.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.3.
|
|
1
|
+
export const CLI_VERSION = "0.3.4";
|