@isomorph.ai/cli 0.2.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.
Files changed (30) hide show
  1. package/README.md +55 -0
  2. package/dist/packages/harbour-cli/src/agent-setup.js +235 -0
  3. package/dist/packages/harbour-cli/src/app-schema.js +142 -0
  4. package/dist/packages/harbour-cli/src/auth.js +197 -0
  5. package/dist/packages/harbour-cli/src/check.js +317 -0
  6. package/dist/packages/harbour-cli/src/cli.js +321 -0
  7. package/dist/packages/harbour-cli/src/config.js +97 -0
  8. package/dist/packages/harbour-cli/src/dev.js +125 -0
  9. package/dist/packages/harbour-cli/src/forwarder.js +168 -0
  10. package/dist/packages/harbour-cli/src/integrations.js +385 -0
  11. package/dist/packages/harbour-cli/src/jobs.js +53 -0
  12. package/dist/packages/harbour-cli/src/kit-bundle.js +41 -0
  13. package/dist/packages/harbour-cli/src/kit-bundle.manifest.js +32 -0
  14. package/dist/packages/harbour-cli/src/kit.js +149 -0
  15. package/dist/packages/harbour-cli/src/local-runtime.js +526 -0
  16. package/dist/packages/harbour-cli/src/operations.js +382 -0
  17. package/dist/packages/harbour-cli/src/output.js +273 -0
  18. package/dist/packages/harbour-cli/src/productionise.js +508 -0
  19. package/dist/packages/harbour-cli/src/remote-mcp-client.js +121 -0
  20. package/dist/packages/harbour-cli/src/retained-checks.js +435 -0
  21. package/dist/packages/harbour-cli/src/source-inventory.js +239 -0
  22. package/dist/packages/harbour-cli/src/starter.js +557 -0
  23. package/dist/packages/harbour-cli/src/upload.js +163 -0
  24. package/dist/packages/harbour-cli/src/version.js +1 -0
  25. package/dist/src/analyzer.js +685 -0
  26. package/dist/src/contracts.js +82 -0
  27. package/dist/src/digest.js +26 -0
  28. package/dist/src/secret-paths.js +38 -0
  29. package/dist/src/source-intake.js +125 -0
  30. package/package.json +43 -0
@@ -0,0 +1,41 @@
1
+ import { PUBLISHED_KIT_BUNDLE } from "./kit-bundle.manifest.js";
2
+ /**
3
+ * The local stack's supporting services. Not part of the published bundle: they run only under `isomorph dev`.
4
+ * MinIO is the exact image the pipeline's validation cell runs (data plane `transformbuild.validationMinIOImage`),
5
+ * from MinIO's own registry by digest: Docker Hub's `minio/minio` repository disappeared on 2026-09-11
6
+ * (404, "pull access denied"), and `isomorph dev`/`check` pull before every start, so a tag there took the
7
+ * whole local kit down. A digest cannot be moved or removed from under an app.
8
+ */
9
+ export const LOCAL_SERVICE_IMAGES = {
10
+ postgres: "postgres:16-alpine",
11
+ minio: "quay.io/minio/minio@sha256:d249d1fb6966de4d8ad26c04754b545205ff15a62e4fd19ebd0f26fa5baacbc0"
12
+ };
13
+ /**
14
+ * The bundle this CLI was built with: `manifest.json` of the
15
+ * harbour-deployment-data-plane `publish-kit-bundle` artifact for the kit
16
+ * version pinned in package.json `harbour.kitBundle`, fetched at build time
17
+ * into `kit-bundle.manifest.ts` (the images and the tarball live in the kit's
18
+ * public registry, `public.ecr.aws/<alias>/`, pinned by digest so no login
19
+ * and no tag can substitute other bytes). `isomorph init` records it in the
20
+ * app's kit.lock; `isomorph init --upgrade` shows the digest diff against it;
21
+ * the hosted pipeline compares `sdk.tarballSha256` with the SDK it bakes.
22
+ */
23
+ export const EMBEDDED_KIT_BUNDLE = { ...PUBLISHED_KIT_BUNDLE, images: { ...LOCAL_SERVICE_IMAGES, ...PUBLISHED_KIT_BUNDLE.images } };
24
+ export function isKitBundle(value) {
25
+ const record = value;
26
+ 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.tarballSha256 === "string"
28
+ && record.images && typeof record.images.appGateway === "string" && typeof record.images.sessionFixture === "string"
29
+ && record.brief && typeof record.brief.fingerprint === "string" && record.declarationSchema === "isomorph.app-integrations/2.0");
30
+ }
31
+ /** Lines naming every digest that differs between two manifests, for `init --upgrade`. */
32
+ export function bundleDiff(before, after) {
33
+ const fields = [
34
+ ["kitVersion", before.kitVersion, after.kitVersion],
35
+ ["sdk.tarballSha256", before.sdk.tarballSha256, after.sdk.tarballSha256],
36
+ ["images.appGateway", before.images.appGateway, after.images.appGateway],
37
+ ["images.sessionFixture", before.images.sessionFixture, after.images.sessionFixture],
38
+ ["brief.fingerprint", before.brief.fingerprint, after.brief.fingerprint]
39
+ ];
40
+ return fields.filter(([, a, b]) => a !== b).map(([name, a, b]) => `${name}: ${a ?? "(none)"} -> ${b ?? "(none)"}`);
41
+ }
@@ -0,0 +1,32 @@
1
+ export const PUBLISHED_KIT_BUNDLE = {
2
+ "schema": "isomorph.kit-bundle/1.0",
3
+ "kitVersion": "0.2.0",
4
+ "sdk": {
5
+ "package": "@isomorph.ai/app-sdk",
6
+ "version": "1.2.0",
7
+ "tarballSha256": "6b41494883215527a73e05a1e07d022552dfe3402915e23950e357e5e9381c49",
8
+ "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:6b41494883215527a73e05a1e07d022552dfe3402915e23950e357e5e9381c49"
9
+ },
10
+ "images": {
11
+ "appGateway": "public.ecr.aws/y6t4p3i8/harbour-app-gateway@sha256:13c51a2396d1af396e744b1f6f09abce9fd54267418b07122af89d6797fd5809",
12
+ "sessionFixture": "public.ecr.aws/y6t4p3i8/harbour-session-fixture@sha256:7c06fe3d14f902eef93240f33a6c39e20b01459c108dcaa7bdaa892109f6853d"
13
+ },
14
+ "nativeRuntime": {
15
+ "darwinArm64": {
16
+ "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:217471d3f95de27f774da7765b6e01f814f770c531c4ae2e0f17474b2b909ada",
17
+ "sha256": "217471d3f95de27f774da7765b6e01f814f770c531c4ae2e0f17474b2b909ada"
18
+ },
19
+ "linuxX64": {
20
+ "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:a55f40569ae51d6a4088b8025ea8f720ef6876a352fe3f17b32ff9571098ffd8",
21
+ "sha256": "a55f40569ae51d6a4088b8025ea8f720ef6876a352fe3f17b32ff9571098ffd8"
22
+ },
23
+ "windowsX64": {
24
+ "url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:3f637182209f6063b85d781bddcf9b437c4bca7f3361df37e209f874a377aaa1",
25
+ "sha256": "3f637182209f6063b85d781bddcf9b437c4bca7f3361df37e209f874a377aaa1"
26
+ }
27
+ },
28
+ "brief": {
29
+ "fingerprint": "dd6aca0163116c0c5b73ca447094aba67c5ca58a9d285722acb12eef5fde4e9e"
30
+ },
31
+ "declarationSchema": "isomorph.app-integrations/2.0"
32
+ };
@@ -0,0 +1,149 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, parse, resolve } from "node:path";
5
+ import { scanWorkspace } from "../../../src/analyzer.js";
6
+ import { isKitBundle } from "./kit-bundle.js";
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
+ /** Explicit reads `isomorph check --integrations` may run; every other operation is a send and never runs during checks. */
24
+ export const READ_OPERATIONS = ["slack.channel.history", "gmail.thread.list", "warehouse.view.read"];
25
+ /** Reads whose input needs an identifier from a prior read (a message id): listed as reads, exercised only through the list. */
26
+ export const DEPENDENT_READ_OPERATIONS = ["gmail.message.read"];
27
+ /**
28
+ * Reads the committed declaration as JSON. The closed rules (schema, connection
29
+ * kinds, the operation set, identity modes, logical resource names) are the
30
+ * kit gate's `declaration` check — the pipeline's own — so a defect is named
31
+ * once, in the same words, by `isomorph check` and by CodeBuild.
32
+ */
33
+ export function emptyDeclaration() { return { schema: "isomorph.app-integrations/2.0", connections: {} }; }
34
+ export function resourceNames(operation) { return Array.isArray(operation.resources) ? operation.resources : Object.keys(operation.resources); }
35
+ export const requestResourceName = (resource) => typeof resource === "string" ? resource : resource.name;
36
+ export function requestResources(operation) {
37
+ return Array.isArray(operation.resources) ? operation.resources : Object.entries(operation.resources).map(([name, spec]) => ({ name, columns: [...spec.columns] }));
38
+ }
39
+ export async function readDeclaration(root) {
40
+ let raw;
41
+ try {
42
+ raw = await readFile(kitPaths(root).declaration, "utf8");
43
+ }
44
+ catch {
45
+ return { declaration: emptyDeclaration(), errors: [".isomorph/integrations.json is missing (run `isomorph init`)"] };
46
+ }
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(raw);
50
+ }
51
+ catch {
52
+ return { declaration: emptyDeclaration(), errors: [".isomorph/integrations.json is not valid JSON"] };
53
+ }
54
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
55
+ return { declaration: emptyDeclaration(), errors: [".isomorph/integrations.json must be a JSON object"] };
56
+ const record = parsed;
57
+ const connections = record.connections && typeof record.connections === "object" && !Array.isArray(record.connections) ? record.connections : {};
58
+ return { declaration: { schema: record.schema, connections }, errors: [] };
59
+ }
60
+ export function isKitLock(value) {
61
+ const record = value;
62
+ 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");
63
+ }
64
+ export async function readKitLock(root) {
65
+ let raw;
66
+ try {
67
+ raw = await readFile(kitPaths(root).lock, "utf8");
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ let parsed;
73
+ try {
74
+ parsed = JSON.parse(raw);
75
+ }
76
+ catch {
77
+ throw new CliError("KIT_LOCK_INVALID", ".isomorph/kit.lock.json is not valid JSON.");
78
+ }
79
+ if (!isKitLock(parsed))
80
+ throw new CliError("KIT_LOCK_INVALID", ".isomorph/kit.lock.json does not match isomorph.kit-lock/1.0.");
81
+ return parsed;
82
+ }
83
+ export async function writeKitLock(root, lock) {
84
+ const next = { ...lock, updatedAt: new Date().toISOString() };
85
+ await mkdir(dirname(kitPaths(root).lock), { recursive: true });
86
+ await writeFile(kitPaths(root).lock, `${JSON.stringify(next, null, 2)}\n`);
87
+ return next;
88
+ }
89
+ /** Records the app identity Isomorph settled on; a non-kit app (no kit.lock) is left alone. */
90
+ export async function recordKitAppId(root, appId, tenantId) {
91
+ const lock = await readKitLock(root);
92
+ if (!lock || (lock.appId === appId && lock.tenantId === tenantId))
93
+ return;
94
+ await writeKitLock(root, { ...lock, appId, tenantId });
95
+ }
96
+ export function newKitLock(bundle, tenantId = "", appId = "") {
97
+ const now = new Date().toISOString();
98
+ return { schema: "isomorph.kit-lock/1.0", appId, tenantId, bundle, createdAt: now, updatedAt: now };
99
+ }
100
+ // ---- Paths and identity --------------------------------------------------------
101
+ /** Where a kit app keeps its declaration, lock, retained checks, gate-written inventory and (ignored) local state. */
102
+ export const KIT_DIRECTORY = ".isomorph";
103
+ export function appRoot(rootArg) {
104
+ if (!rootArg)
105
+ throw new CliError("USAGE", "--app-root <path> is required.");
106
+ const root = resolve(rootArg);
107
+ if (root === parse(root).root || root === resolve(homedir()))
108
+ throw new CliError("PREFLIGHT_APP_ROOT", "The selected app root is unsafe.");
109
+ return root;
110
+ }
111
+ export function kitPaths(root) {
112
+ const kit = join(root, KIT_DIRECTORY);
113
+ const local = join(kit, "local");
114
+ return { kit, local, declaration: join(kit, "integrations.json"), lock: join(kit, "kit.lock.json"), checks: join(kit, "checks"), devLock: join(local, "dev.lock"), state: join(local, "state"), report: join(local, "check-report.json") };
115
+ }
116
+ /** The kit directory of an app set up by a CLI older than 0.2.0; `isomorph init --upgrade` moves it to `.isomorph/`. */
117
+ export const LEGACY_KIT_DIRECTORY = ".harbour";
118
+ /**
119
+ * Refuses an app that still carries the old kit directory and not the current
120
+ * one. Nothing reads the old layout — not `dev`, not `check`, not the
121
+ * deployment pipeline — so the one command that moves it is named here rather
122
+ * than discovered as a missing declaration, a missing lock and an app deployed
123
+ * without its kit.
124
+ */
125
+ export async function assertKitCurrent(root) {
126
+ const [legacy, current] = await Promise.all([isDirectory(join(root, LEGACY_KIT_DIRECTORY)), isDirectory(kitPaths(root).kit)]);
127
+ if (legacy && !current)
128
+ throw new CliError("KIT_UPGRADE_REQUIRED", "This app was set up by an older CLI and has not been moved to the current kit yet. Run `isomorph init --upgrade --app-root .` once, then run this again.");
129
+ }
130
+ const isDirectory = (path) => stat(path).then(info => info.isDirectory(), () => false);
131
+ /** Stable per-project identity for the kit-managed local state. */
132
+ export function projectName(root, suffix = "") {
133
+ return `isomorph-${createHash("sha256").update(resolve(root)).digest("hex").slice(0, 12)}${suffix}`;
134
+ }
135
+ /** Governance link idempotency key: sha256(tenantId + absolute app root). */
136
+ export function linkIdempotencyKey(tenantId, root) {
137
+ return createHash("sha256").update(`${tenantId}${resolve(root)}`).digest("hex");
138
+ }
139
+ /** sha256 over the tracked app files (same boundary as productionise), so any edit changes it. */
140
+ export async function sourceDigest(root) {
141
+ const graph = await scanWorkspace(root, { sourceBoundary: "root" });
142
+ const files = [...graph.deploymentScope.includedFiles].sort();
143
+ const hash = createHash("sha256");
144
+ for (const path of files) {
145
+ hash.update(path).update("\0");
146
+ hash.update(await readFile(join(root, path))).update("\0");
147
+ }
148
+ return { digest: `sha256:${hash.digest("hex")}`, files };
149
+ }