@isomorph.ai/cli 0.4.2 → 0.5.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/README.md +9 -6
- package/dist/packages/harbour-cli/src/agent-setup.js +1 -1
- package/dist/packages/harbour-cli/src/check.js +42 -103
- package/dist/packages/harbour-cli/src/cli.js +54 -34
- package/dist/packages/harbour-cli/src/deploy.js +128 -330
- package/dist/packages/harbour-cli/src/guide.js +2 -2
- package/dist/packages/harbour-cli/src/integrations.js +66 -10
- package/dist/packages/harbour-cli/src/kit-bundle.manifest.js +12 -12
- package/dist/packages/harbour-cli/src/kit.js +5 -3
- package/dist/packages/harbour-cli/src/local-runtime.js +9 -8
- package/dist/packages/harbour-cli/src/operations.js +101 -71
- package/dist/packages/harbour-cli/src/output.js +11 -2
- package/dist/packages/harbour-cli/src/package.js +101 -0
- package/dist/packages/harbour-cli/src/starter.js +44 -22
- package/dist/packages/harbour-cli/src/upload.js +8 -2
- package/dist/src/analyzer.js +3 -17
- package/dist/src/secret-paths.js +6 -6
- package/package.json +2 -2
- package/dist/packages/harbour-cli/src/app-schema.js +0 -150
- package/dist/packages/harbour-cli/src/remote-mcp-client.js +0 -135
- package/dist/packages/harbour-cli/src/retained-checks.js +0 -443
- package/dist/packages/harbour-cli/src/source-inventory.js +0 -239
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
-
import { extname, join, relative, sep } from "node:path";
|
|
3
|
-
/**
|
|
4
|
-
* What the app's own source performs, read the way the pipeline reads it.
|
|
5
|
-
*
|
|
6
|
-
* Two inventories: every `<table> <verb>` the browser code performs
|
|
7
|
-
* (write_probe.go `inventorySDKTableVerbs`) and every SDK capability it calls
|
|
8
|
-
* (the kit lane's derivation). They are what the kit gate demands evidence
|
|
9
|
-
* for, so `retained-checks.ts` generates the retained journeys from exactly
|
|
10
|
-
* them — a generated journey can then only ever cover what the gate asks
|
|
11
|
-
* about, with no template in between.
|
|
12
|
-
*
|
|
13
|
-
* The gate itself — operation coverage in both directions, the cross-user
|
|
14
|
-
* denial, the database gate — is not here. It is the data plane's
|
|
15
|
-
* `transformbuild.RunKitGate`, and `isomorph check` runs it (check.ts) in the
|
|
16
|
-
* app's local session through the pinned gateway image's `gate` subcommand.
|
|
17
|
-
*/
|
|
18
|
-
/** Capabilities the kit gate demands evidence for, in the pipeline's order. */
|
|
19
|
-
export const COVERED_CAPABILITIES = ["data", "files", "actions", "telemetry", "realtime", "ai"];
|
|
20
|
-
/** Every capability the kit lane derives from the browser SDK surface. */
|
|
21
|
-
const SDK_CAPABILITIES = ["data", "files", "actions", "realtime", "telemetry", "integrations", "ai"];
|
|
22
|
-
const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs"]);
|
|
23
|
-
const SKIPPED_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".isomorph"]);
|
|
24
|
-
/**
|
|
25
|
-
* TypeScript call sites carry explicit type arguments between a method name and
|
|
26
|
-
* its call — `isomorph.data.from<Note>("notes")`, `.select<Row>(` — and the
|
|
27
|
-
* starter's own list is one of them. Matching only the bare `.from(` made every
|
|
28
|
-
* typed call site invisible, so a freshly generated starter inventoried
|
|
29
|
-
* `notes: delete, insert, update` with no SELECT: the gate was reading the
|
|
30
|
-
* starter's own capabilities wrong. This is the pipeline's own
|
|
31
|
-
* `appSDKTypeArguments` (transformbuild/source_inspection.go), one nesting
|
|
32
|
-
* level of generics, so both scanners see the same call sites.
|
|
33
|
-
*/
|
|
34
|
-
const TYPE_ARGUMENTS = String.raw `(?:<[^<>()]*(?:<[^<>()]*>[^<>()]*)*>)?\s*`;
|
|
35
|
-
const TABLE_CHAIN_SOURCE = String.raw `\.from${TYPE_ARGUMENTS}\(\s*["'\`]([A-Za-z0-9_]+)["'\`]\s*\)`;
|
|
36
|
-
const TABLE_CHAIN = new RegExp(TABLE_CHAIN_SOURCE, "g");
|
|
37
|
-
/** The same chain, unanchored and non-global: where THIS chain's verbs stop. */
|
|
38
|
-
const NEXT_TABLE_CHAIN = new RegExp(TABLE_CHAIN_SOURCE);
|
|
39
|
-
const VERB_CALL = new RegExp(String.raw `\.(select|insert|update|delete|upsert)\s*${TYPE_ARGUMENTS}\(`, "g");
|
|
40
|
-
/** `createClient()` bindings name the identifier a capability call must be made on. */
|
|
41
|
-
const CLIENT_BINDING = /(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::[^=]+)?=\s*(?:await\s+)?createClient\s*\(/g;
|
|
42
|
-
/** `<anything>.<namespace>.<method>(` — the receiver is deliberately not constrained; see capabilityCallSurface. */
|
|
43
|
-
const CAPABILITY_CALL = new RegExp(String.raw `\.\s*(${SDK_CAPABILITIES.join("|")})\s*\.\s*[A-Za-z_$][A-Za-z0-9_$]*\s*${TYPE_ARGUMENTS}\(`, "g");
|
|
44
|
-
async function sourceFiles(root) {
|
|
45
|
-
const found = [];
|
|
46
|
-
const walk = async (directory) => {
|
|
47
|
-
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
48
|
-
for (const entry of entries) {
|
|
49
|
-
const path = join(directory, entry.name);
|
|
50
|
-
if (entry.isDirectory()) {
|
|
51
|
-
if (!SKIPPED_DIRECTORIES.has(entry.name))
|
|
52
|
-
await walk(path);
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (SOURCE_EXTENSIONS.has(extname(entry.name)))
|
|
56
|
-
found.push(path);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
await walk(root);
|
|
60
|
-
return found.sort();
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Every `<table> <verb>` the browser code performs, with the files performing
|
|
64
|
-
* it. The verb chain is what follows THIS `.from(...)` up to the end of its
|
|
65
|
-
* statement or the next `.from(` — never a later chain's verbs.
|
|
66
|
-
*
|
|
67
|
-
* This reading is what the gate demands evidence for, so `retained-checks.ts`
|
|
68
|
-
* generates from exactly it: a generated journey can then only ever cover what
|
|
69
|
-
* the gate asks about, in both directions, with no template in between.
|
|
70
|
-
*/
|
|
71
|
-
export async function sourceTableUsage(root) {
|
|
72
|
-
const inventory = new Map();
|
|
73
|
-
for (const path of await sourceFiles(root)) {
|
|
74
|
-
const text = await readFile(path, "utf8");
|
|
75
|
-
for (const match of text.matchAll(TABLE_CHAIN)) {
|
|
76
|
-
const start = match.index ?? 0;
|
|
77
|
-
if (text.slice(0, start).trimEnd().endsWith(".storage"))
|
|
78
|
-
continue;
|
|
79
|
-
let tail = text.slice(start + match[0].length);
|
|
80
|
-
const semicolon = tail.indexOf(";");
|
|
81
|
-
if (/[;\n]/.test(tail) && semicolon >= 0)
|
|
82
|
-
tail = tail.slice(0, semicolon);
|
|
83
|
-
const next = tail.search(NEXT_TABLE_CHAIN);
|
|
84
|
-
if (next > 0)
|
|
85
|
-
tail = tail.slice(0, next);
|
|
86
|
-
tail = tail.slice(0, 200);
|
|
87
|
-
const table = match[1];
|
|
88
|
-
const usage = inventory.get(table) ?? { verbs: new Set(), files: [] };
|
|
89
|
-
for (const verb of tail.matchAll(VERB_CALL))
|
|
90
|
-
usage.verbs.add(verb[1]);
|
|
91
|
-
const file = appPath(root, path);
|
|
92
|
-
if (!usage.files.includes(file))
|
|
93
|
-
usage.files.push(file);
|
|
94
|
-
inventory.set(table, usage);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return inventory;
|
|
98
|
-
}
|
|
99
|
-
/** `<table> <verb>` alone, as the coverage gate compares it. */
|
|
100
|
-
export async function sourceTableVerbs(root) {
|
|
101
|
-
return new Map([...await sourceTableUsage(root)].map(([table, usage]) => [table, usage.verbs]));
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* The capabilities the kit lane declares for this app, with the files declaring
|
|
105
|
-
* them: a namespace called as `<client>.<namespace>.<method>(` on an identifier
|
|
106
|
-
* bound to `createClient()`. TypeScript type arguments may sit between the
|
|
107
|
-
* method and its call.
|
|
108
|
-
*/
|
|
109
|
-
export async function capabilityUsage(root) {
|
|
110
|
-
const files = await sourceFiles(root);
|
|
111
|
-
const texts = (await Promise.all(files.map(path => readFile(path, "utf8").catch(() => "")))).map(maskNonCode);
|
|
112
|
-
const clients = new Set();
|
|
113
|
-
for (const text of texts)
|
|
114
|
-
for (const match of text.matchAll(CLIENT_BINDING))
|
|
115
|
-
clients.add(match[1]);
|
|
116
|
-
const used = new Map();
|
|
117
|
-
if (!clients.size)
|
|
118
|
-
return used;
|
|
119
|
-
const namespaces = SDK_CAPABILITIES.join("|");
|
|
120
|
-
for (const client of clients) {
|
|
121
|
-
const call = new RegExp(`\\b${client}\\s*\\.\\s*(${namespaces})\\s*\\.\\s*[A-Za-z_$][A-Za-z0-9_$]*\\s*(?:<[^<>()]*>\\s*)?\\(`, "g");
|
|
122
|
-
for (const [index, text] of texts.entries()) {
|
|
123
|
-
const file = appPath(root, files[index]);
|
|
124
|
-
for (const match of text.matchAll(call)) {
|
|
125
|
-
const seen = used.get(match[1]) ?? [];
|
|
126
|
-
if (!seen.includes(file))
|
|
127
|
-
seen.push(file);
|
|
128
|
-
used.set(match[1], seen);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return used;
|
|
133
|
-
}
|
|
134
|
-
/** The capability names alone, as the coverage gate compares them. */
|
|
135
|
-
export async function declaredCapabilities(root) {
|
|
136
|
-
return new Set((await capabilityUsage(root)).keys());
|
|
137
|
-
}
|
|
138
|
-
/** A source file as the app itself names it: relative to the root, `/`-separated on every platform. */
|
|
139
|
-
const appPath = (root, path) => relative(root, path).split(sep).join("/");
|
|
140
|
-
/**
|
|
141
|
-
* Every capability namespace the app's own source calls a method on, whatever
|
|
142
|
-
* object the call is made on: `isomorph.files.list(`, `client().files.list(`,
|
|
143
|
-
* `(isomorph as {files: F}).files.list(`.
|
|
144
|
-
*
|
|
145
|
-
* This is deliberately looser than `declaredCapabilities`, and the two answer
|
|
146
|
-
* different questions. The forward gate asks "was this capability exercised?"
|
|
147
|
-
* and must be precise about what the app really uses. The reverse gate below
|
|
148
|
-
* asks "is this check exercising something the app no longer has?" — a
|
|
149
|
-
* question whose wrong answer refuses a good app — so it needs a lower bound on
|
|
150
|
-
* what is unused, never an upper bound on what is used.
|
|
151
|
-
*
|
|
152
|
-
* The pipeline's own evidence for a capability is `<client>.<namespace>.<method>(`
|
|
153
|
-
* with the client traced through the module graph (transformbuild
|
|
154
|
-
* analyzeAppSDKClientCalls / analyzeAppSDKNamespaceWrappers). Dropping the
|
|
155
|
-
* receiver makes this a superset of that set — every call site the pipeline can
|
|
156
|
-
* trace, plus ones it cannot — so a capability this cannot see is one the kit
|
|
157
|
-
* lane cannot declare either, and the reverse gate refuses only trees the
|
|
158
|
-
* pipeline already refuses. Comments and string bodies are masked first,
|
|
159
|
-
* exactly as the pipeline masks them, so a quoted example never counts.
|
|
160
|
-
*/
|
|
161
|
-
export async function capabilityCallSurface(root) {
|
|
162
|
-
const surface = new Set();
|
|
163
|
-
for (const path of await sourceFiles(root))
|
|
164
|
-
for (const capability of capabilitiesCalled(await readFile(path, "utf8").catch(() => "")))
|
|
165
|
-
surface.add(capability);
|
|
166
|
-
return surface;
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* The pipeline's reading masks comments and string bodies before it looks for
|
|
170
|
-
* call shapes (transformbuild maskJavaScriptNonCode); this reading does the
|
|
171
|
-
* same, so a call shape quoted in a comment — the starter's own examples —
|
|
172
|
-
* or inside a string never counts as a use. Offsets are preserved (masked
|
|
173
|
-
* bytes become spaces), so nothing that reports a position drifts.
|
|
174
|
-
*/
|
|
175
|
-
export function maskNonCode(source) {
|
|
176
|
-
const out = source.split("");
|
|
177
|
-
let state = "code";
|
|
178
|
-
for (let i = 0; i < source.length; i += 1) {
|
|
179
|
-
const c = source[i], n = source[i + 1];
|
|
180
|
-
if (state === "code") {
|
|
181
|
-
if (c === "/" && n === "/") {
|
|
182
|
-
state = "line";
|
|
183
|
-
out[i] = " ";
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
if (c === "/" && n === "*") {
|
|
187
|
-
state = "block";
|
|
188
|
-
out[i] = " ";
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
if (c === "'")
|
|
192
|
-
state = "single";
|
|
193
|
-
else if (c === "\"")
|
|
194
|
-
state = "double";
|
|
195
|
-
else if (c === "`")
|
|
196
|
-
state = "template";
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
if (state === "line") {
|
|
200
|
-
if (c === "\n")
|
|
201
|
-
state = "code";
|
|
202
|
-
else
|
|
203
|
-
out[i] = " ";
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
if (state === "block") {
|
|
207
|
-
if (c === "*" && n === "/") {
|
|
208
|
-
state = "code";
|
|
209
|
-
out[i] = " ";
|
|
210
|
-
out[i + 1] = " ";
|
|
211
|
-
i += 1;
|
|
212
|
-
}
|
|
213
|
-
else if (c !== "\n")
|
|
214
|
-
out[i] = " ";
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
// A string body: keep the quotes (the shape around them stays readable), mask what is inside.
|
|
218
|
-
const quote = state === "single" ? "'" : state === "double" ? "\"" : "`";
|
|
219
|
-
if (c === "\\") {
|
|
220
|
-
out[i] = " ";
|
|
221
|
-
if (n !== undefined && n !== "\n") {
|
|
222
|
-
out[i + 1] = " ";
|
|
223
|
-
i += 1;
|
|
224
|
-
}
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (c === quote) {
|
|
228
|
-
state = "code";
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
if (c !== "\n")
|
|
232
|
-
out[i] = " ";
|
|
233
|
-
}
|
|
234
|
-
return out.join("");
|
|
235
|
-
}
|
|
236
|
-
/** The same reading of one piece of text, so a retained check is judged by exactly the rule the app's own source is read with (retained-checks.ts). */
|
|
237
|
-
export function capabilitiesCalled(text) {
|
|
238
|
-
return new Set([...maskNonCode(text).matchAll(CAPABILITY_CALL)].map(match => match[1]));
|
|
239
|
-
}
|