@isomorph.ai/cli 0.10.4 → 0.10.6
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 +14 -0
- package/dist/packages/harbour-cli/src/agent-setup.js +1 -1
- package/dist/packages/harbour-cli/src/cli.js +24 -12
- package/dist/packages/harbour-cli/src/guide.js +7 -4
- package/dist/packages/harbour-cli/src/integrations.js +32 -4
- package/dist/src/analyzer.js +5 -5
- package/package.json +3 -3
- package/dist/src/gitignore.js +0 -81
package/README.md
CHANGED
|
@@ -56,3 +56,17 @@ There are two ways onto Isomorph and `init` is where the folder decides. An empt
|
|
|
56
56
|
Built from the [governance control plane](https://github.com/Fourier-Labs-AI/harbour-governance-control-plane) repository: `npm ci`, `npm run build --prefix packages/harbour-cli`, tests in `tests/cli-kit.test.ts` and `tests/cli-packaging-contract.test.ts`. Merge is release (`docs/adr/0021`): a PR that bumps the version in `packages/harbour-cli/package.json` — the one file that carries it; `src/version.ts` reads it at runtime — is tagged `cli-v<version>` by `CLI release automation` the moment it lands on `main`, and `Release Isomorph CLI` publishes that version under `latest` through npm trusted publishing. A version that already exists on npm is never republished. A prerelease (`X.Y.Z-rc.N`) publishes under `candidate` and is never required. When the release embeds a new kit bundle, bump `harbour.kitBundle.version` in the same PR and run `npm run kit-bundle:sync` there: it regenerates the embedded manifest and the server's `src/kit-bundle-expectation.generated.ts` from one fetch. 0.2.0 and 0.2.1 (the rename) were published by hand from their merge commits before the trusted publisher existed for `@isomorph.ai/cli`, which is why their `cli-v` release runs show red.
|
|
57
57
|
|
|
58
58
|
Connect with `isomorph connect name@getlokal.com`: the CLI asks the platform which company admits the email's domain (`https://platform.isomorph.ai/start/getlokal.com.json` — the same policies that admit the person at sign-in) and saves that company's profile. Only the domain is sent, not the email. When no company admits the domain, or more than one does, the CLI prints the platform's sentence (ask IT, or run the printed `isomorph connect <link>`) and saves nothing; a company's console page (`platform.isomorph.ai/t/<company>`) is accepted as the link. `isomorph login` then prints the company it signed in to and what IT has set up there. Company sign-in and access checks still apply.
|
|
59
|
+
|
|
60
|
+
### Owner-authorised background integrations
|
|
61
|
+
|
|
62
|
+
Read `isomorph integrations status --app-root . --json` for `consents`: the caller's connected
|
|
63
|
+
accounts and per-environment background flags. Include background use of the account by the app
|
|
64
|
+
in the setup approval, naming the account and environment; reuse explicit approval already given.
|
|
65
|
+
Then run `isomorph integrations request <connection> --app-root . --allow-background preview --json`
|
|
66
|
+
and verify status. Omit the flag for ordinary requests. This records consent on the existing link;
|
|
67
|
+
it neither approves IT access nor sends a message. The existing console control can revoke it.
|
|
68
|
+
|
|
69
|
+
If the account needs connecting, use `integrations connect <connection> --app-root .
|
|
70
|
+
--return-url <dev-origin>/_harbour/integrations/oauth/complete --json` and follow `nextStep`.
|
|
71
|
+
Run `isomorph agent-setup` after updating the CLI to refresh the AI instructions; `init` also
|
|
72
|
+
installs them. No new endpoint, login, runtime or SDK change is needed.
|
|
@@ -123,7 +123,7 @@ Rules live beside this file: \`core.md\` before the first edit; \`integrations.m
|
|
|
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. |
|
|
125
125
|
| "stop it" | \`isomorph stop --app-root .\` (data kept); \`isomorph dev --app-root . --reset\` only when they ask to start over. |
|
|
126
|
-
| "every day at 2pm", "send this automatically" | read \`jobs.md\` beside this skill first (
|
|
126
|
+
| "every day at 2pm", "send this automatically" | read \`jobs.md\` beside this skill first (follow its in-conversation owner approval steps); write the job, run it once with \`isomorph jobs run\`, and report what was verified. |
|
|
127
127
|
| "call this API", "a key for it", "run it in the background" | read \`server.md\` beside this skill first; server code goes in \`actions/\` or \`jobs/\`, a key by its name only. |
|
|
128
128
|
|
|
129
129
|
A refusal names its layer, its reason and its fix: change that one thing, then run it again. A wait timeout is not a failure — work is still running: run the command it prints (\`${continueCommand("<ref>")}\`); never start a second deploy while one is running.
|
|
@@ -13,7 +13,7 @@ import { agentPaths, agentSetup } from "./agent-setup.js";
|
|
|
13
13
|
import { detachDev, startDev } from "./dev.js";
|
|
14
14
|
import { endProcess, installDependencies, LocalRuntime, readDevLock, releaseDevLock, runCommand } from "./local-runtime.js";
|
|
15
15
|
import { CHECKS_FAILED_HINT, checksFailedMessage, runChecks } from "./check.js";
|
|
16
|
-
import { assertDeployReady, companySystemsLine, GovernanceClient, groupGrants, IDENTITY_WORDS, integrationsCatalog, integrationsStatus, renderGrantGroup, renderIntegrationsCatalog, requestIntegrations } from "./integrations.js";
|
|
16
|
+
import { connectIntegration, assertDeployReady, 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);
|
|
19
19
|
// `productionise` (the command's name until 0.3.4) was an alias of `deploy` for one release; it is no longer a command and falls through to the usage error.
|
|
@@ -39,6 +39,7 @@ const out = optionValue("--out");
|
|
|
39
39
|
const testIntegrations = args.includes("--integrations");
|
|
40
40
|
const reason = optionValue("--reason");
|
|
41
41
|
const operations = optionValue("--operations");
|
|
42
|
+
const allowBackground = optionValue("--allow-background");
|
|
42
43
|
const scheduledAt = optionValue("--scheduled-at");
|
|
43
44
|
const realJob = args.includes("--real");
|
|
44
45
|
/** Seconds `deploy`, `status --wait`, `retry` and `promote` follow the operation before reporting it as still running (default: 30 minutes). */
|
|
@@ -47,6 +48,8 @@ const waitOptions = maxWaitSeconds ? { maxWaitMs: maxWaitSeconds * 1000 } : {};
|
|
|
47
48
|
let optionError;
|
|
48
49
|
if (maxWaitSeconds !== undefined && !(Number.isInteger(maxWaitSeconds) && maxWaitSeconds > 0))
|
|
49
50
|
optionError = "--max-wait takes a whole number of seconds.";
|
|
51
|
+
if (args.includes("--allow-background") && !["development", "preview", "production"].includes(allowBackground ?? ""))
|
|
52
|
+
optionError = "--allow-background takes development, preview or production.";
|
|
50
53
|
const explicitToken = process.env.ISOMORPH_TOKEN?.trim() ?? "";
|
|
51
54
|
const usage = [
|
|
52
55
|
"Usage:",
|
|
@@ -63,7 +66,8 @@ const usage = [
|
|
|
63
66
|
" isomorph retry [--app-root <path>] [--operation <reference>] [--no-wait] [--max-wait <seconds>] [--json]",
|
|
64
67
|
" isomorph promote [--app-root <path>] [--operation <reference>] [--confirm-tested] [--no-wait] [--max-wait <seconds>] [--json] (--confirm-tested: the person has tried the preview; the company's AI setup is confirmed for production when the app calls governed AI)",
|
|
65
68
|
" isomorph jobs run <name> --app-root <path> [--scheduled-at <UTC>] [--real] [--json] run one scheduled job now (local fixtures; --real uses approved company access)",
|
|
66
|
-
" isomorph integrations request <connection> --app-root <path> [--reason <text>] [--operations a,b] [--json] one request per connection, for every environment at once: IT approves it once (isomorph dev and deploy file it for you)",
|
|
69
|
+
" isomorph integrations request <connection> --app-root <path> [--reason <text>] [--operations a,b] [--allow-background <development|preview|production>] [--json] one request per connection, for every environment at once: IT approves it once (isomorph dev and deploy file it for you)",
|
|
70
|
+
" isomorph integrations connect <connection> --app-root <path> [--return-url <local-app-url>] [--json] connect the signed-in account; returns provider authorizationUrl when needed",
|
|
67
71
|
" isomorph integrations status --app-root <path> [--json]",
|
|
68
72
|
" isomorph integrations catalog --app-root <path> [--json] the company's connections as .isomorph/integrations.json names them: identifiers, allowed operations, approved channels/tables/views/mailboxes per environment (no app needed)",
|
|
69
73
|
"Run `isomorph connect <work-email>` once; sign in with `isomorph login` when Isomorph asks.",
|
|
@@ -110,11 +114,11 @@ else if (command === "agent-setup") {
|
|
|
110
114
|
}
|
|
111
115
|
else if (!["connect", "login", "logout", "deploy", "integrations", ...LOCAL_COMMANDS, ...OPERATION_COMMANDS].includes(command)
|
|
112
116
|
|| (command === "connect" && (!connectUrl || connectUrl.startsWith("--")))
|
|
113
|
-
|| (["deploy", ...OPERATION_COMMANDS].includes(command) && optionError)
|
|
117
|
+
|| (["deploy", "integrations", ...OPERATION_COMMANDS].includes(command) && optionError)
|
|
114
118
|
|| (command === "deploy" && !root)
|
|
115
119
|
|| (LOCAL_COMMANDS.includes(command) && !root)
|
|
116
120
|
|| (command === "jobs" && (subcommand !== "run" || !args[2] || args[2].startsWith("--")))
|
|
117
|
-
|| (command === "integrations" && (!root || !subcommand || !["request", "status", "catalog"].includes(subcommand) || (
|
|
121
|
+
|| (command === "integrations" && (!root || !subcommand || !["request", "status", "catalog", "connect"].includes(subcommand) || (["request", "connect"].includes(subcommand) && (!args[2] || args[2].startsWith("--")))))) {
|
|
118
122
|
exitAfterWriting(2, "", `${optionError ? `${optionError}\n` : ""}${usage}`);
|
|
119
123
|
}
|
|
120
124
|
else {
|
|
@@ -246,14 +250,20 @@ else {
|
|
|
246
250
|
let envelope;
|
|
247
251
|
if (command === "integrations") {
|
|
248
252
|
const target = appRoot(root);
|
|
249
|
-
const result = subcommand === "
|
|
250
|
-
? await
|
|
251
|
-
: subcommand === "
|
|
252
|
-
? await
|
|
253
|
-
:
|
|
253
|
+
const result = subcommand === "connect"
|
|
254
|
+
? await connectIntegration(target, governance, args[2], optionValue("--return-url"))
|
|
255
|
+
: subcommand === "status"
|
|
256
|
+
? await integrationsStatus(target, governance)
|
|
257
|
+
: subcommand === "catalog"
|
|
258
|
+
? await integrationsCatalog(governance)
|
|
259
|
+
: await requestIntegrations(target, governance, tenant, EMBEDDED_KIT_BUNDLE, { connection: args[2], ...(reason ? { reason } : {}), operations: operations?.split(",").map(value => value.trim()).filter(Boolean), allowBackground });
|
|
254
260
|
envelope = summaryEnvelope(result);
|
|
255
|
-
if (!json)
|
|
256
|
-
|
|
261
|
+
if (!json) {
|
|
262
|
+
if ("nextStep" in result)
|
|
263
|
+
progress([result.authorizationUrl, result.nextStep].filter(Boolean).join("\n"));
|
|
264
|
+
else
|
|
265
|
+
progress(subcommand === "status" ? renderIntegrationsStatus(result) : subcommand === "catalog" ? renderIntegrationsCatalog(result) : renderRequest(result));
|
|
266
|
+
}
|
|
257
267
|
}
|
|
258
268
|
else if (command === "deploy") {
|
|
259
269
|
const result = await deploy(root, governance, progress, tenant, { waitForDeployment: !noWait, waitOptions, bundle: EMBEDDED_KIT_BUNDLE, ...(sessionNotes ? { sessionNotes } : {}) });
|
|
@@ -299,6 +309,8 @@ function renderIntegrationsStatus(status) {
|
|
|
299
309
|
if (!status.linked)
|
|
300
310
|
return "This app is not linked yet; `isomorph dev` (signed in) or `isomorph integrations request` links it.";
|
|
301
311
|
const lines = [`App ${status.appId}`, ...groupGrants(status.grants).flatMap(group => renderGrantGroup(group).map(line => ` ${line}`))];
|
|
312
|
+
for (const consent of status.consents ?? [])
|
|
313
|
+
lines.push(` ${consent.connection} (${consent.environment}): ${consent.accountLabel ?? "your account"}, ${consent.status}, background ${consent.backgroundAllowed ? "allowed" : "off"}`);
|
|
302
314
|
for (const request of status.requests)
|
|
303
315
|
lines.push(` request ${request.requestId}: ${request.state}${request.reason ? ` (${request.reason})` : ""}`);
|
|
304
316
|
if (!status.grants.length && !status.requests.length)
|
|
@@ -306,7 +318,7 @@ function renderIntegrationsStatus(status) {
|
|
|
306
318
|
return lines.join("\n");
|
|
307
319
|
}
|
|
308
320
|
function renderRequest(result) {
|
|
309
|
-
return [`App ${result.appId}: ${result.connection}`, ...result.requests.map(item => ` ${IDENTITY_WORDS[item.identityMode]} — ${item.operations.join(", ")} on ${item.resources.map(requestResourceName).join(", ")}${item.presentation ? ` (posts as "${item.presentation.displayName}")` : ""}: ${item.state} — ${item.summary}`)].join("\n");
|
|
321
|
+
return [`App ${result.appId}: ${result.connection}`, ...result.requests.map(item => ` ${IDENTITY_WORDS[item.identityMode]} — ${item.operations.join(", ")} on ${item.resources.map(requestResourceName).join(", ")}${item.presentation ? ` (posts as "${item.presentation.displayName}")` : ""}: ${item.state} — ${item.summary}${item.backgroundConsent ? `; background allowed in ${item.backgroundConsent.environment}` : ""}`)].join("\n");
|
|
310
322
|
}
|
|
311
323
|
/**
|
|
312
324
|
* Who signed in and what IT has set up, for the line after `login`; undefined
|
|
@@ -91,13 +91,16 @@ const reply = await isomorph.ai.chat({ messages: [{ role: "user", content: \`Sum
|
|
|
91
91
|
The starter calls no AI; add the one call when the person asks. The check's generated ai journey makes a real governed call locally through the development route with your \`isomorph login\`; in the pipeline it is answered by a canned completion and reported as not tested.`,
|
|
92
92
|
jobs: `# Scheduled jobs
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
Jobs run as the app. Gmail and user-mode Slack require the current owner's connected account, an approved integration grant and explicit background permission in each environment. App-mode Slack needs no personal background consent.
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
Write \`jobs/<name>.ts\` with one literal UTC cron as \`schedule\` and a default async handler. Never use \`setInterval\`, an effect or a browser timer: the deployed app server owns the clock. Jobs can call approved company operations and \`isomorph.ai.chat\`; without a schedule they run only when enqueued (server.md). Use a deterministic idempotency key per business period and destination.
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
For personal background access:
|
|
99
|
+
1. Run \`isomorph integrations status --app-root . --json\`; inspect \`consents\` for the connected account and per-environment \`backgroundAllowed\`.
|
|
100
|
+
2. If connection is missing or needs reconnecting, run \`isomorph dev\`, then \`isomorph integrations connect <connection> --app-root . --return-url <origin>/_harbour/integrations/oauth/complete --json\`. Follow \`nextStep\`; the person completes provider consent if required.
|
|
101
|
+
3. Include use of this account by this app's background code in the setup approval, naming the account and environment. Reuse explicit approval already given; no separate approval step. Then run \`isomorph integrations request <connection> --app-root . --allow-background <development|preview|production> --json\` and verify status. Do not send the person to the Admin console. Omit the flag when background use was not authorised.
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
Test with \`isomorph jobs run <name> --app-root . --scheduled-at <matching-UTC-time> --json\`: local data, canned AI, company systems refused, nothing sent. Add \`--real\` only for an explicitly requested immediate test with development approval and background consent. Permission alone does not deploy the job; verify deployment and actual delivery before calling the schedule active.`,
|
|
101
104
|
server: `# Server code: actions, queued runs, outside APIs
|
|
102
105
|
|
|
103
106
|
Isomorph runs one server process for the app, started by \`isomorph dev\`, the check and the deployment. It serves \`actions/<name>.ts\` and \`jobs/<name>.ts\`; both use the same \`isomorph\` client and nothing else — no database driver, no \`process.env.DATABASE_URL\`, no HTTP framework.
|
|
@@ -29,6 +29,9 @@ export class GovernanceClient {
|
|
|
29
29
|
catalog() {
|
|
30
30
|
return this.call("GET", `/v1/development/integrations/catalog?tenantId=${encodeURIComponent(this.tenantId)}`);
|
|
31
31
|
}
|
|
32
|
+
connectIntegration(appId, connection, returnUrl) {
|
|
33
|
+
return this.call("POST", `/v1/development/apps/${encodeURIComponent(appId)}/integrations/connect`, { connection, ...(returnUrl ? { returnUrl } : {}) });
|
|
34
|
+
}
|
|
32
35
|
execute(appId, body) {
|
|
33
36
|
return this.call("POST", `/v1/development/apps/${encodeURIComponent(appId)}/integrations/execute`, body);
|
|
34
37
|
}
|
|
@@ -141,11 +144,14 @@ export const IDENTITY_WORDS = { app: "as the company bot", user: "as the signed-
|
|
|
141
144
|
/** A refusal about the leg rather than the ask — signed out, unreachable, an answer with no code — is nobody's ask and stops the run. */
|
|
142
145
|
const legFailure = (code) => code === "AUTH_REQUIRED" || code === "GOVERNANCE_UNREACHABLE" || code.startsWith("HTTP_");
|
|
143
146
|
/** One request per identity mode of the connection. A refusal of an ask is returned beside the others, not thrown, so a caller can report every connection at once. */
|
|
144
|
-
async function submitRequests(client, appId, declaration, root, connection, app, reason, only) {
|
|
147
|
+
async function submitRequests(client, appId, declaration, root, connection, app, reason, only, allowBackground) {
|
|
145
148
|
return Promise.all(requestScope(declaration, connection, only).map(async (part) => {
|
|
146
149
|
const ask = { connection, ...part };
|
|
147
150
|
try {
|
|
148
|
-
|
|
151
|
+
const outcome = await client.request(appId, { connection, identityMode: part.identityMode, operations: part.operations, resources: part.resources, ...(part.presentation ? { presentation: part.presentation } : {}), ...(reason?.trim() ? { reason: reason.trim() } : {}), app, ...(allowBackground && part.identityMode === "user" ? { allowBackground } : {}) });
|
|
152
|
+
if (allowBackground && part.identityMode === "user" && (outcome.backgroundConsent?.environment !== allowBackground || outcome.backgroundConsent.backgroundAllowed !== true))
|
|
153
|
+
throw new CliError("BACKGROUND_NOT_CONFIRMED", "The server did not confirm background permission. Check integrations status before reporting it enabled.");
|
|
154
|
+
return { ...ask, outcome };
|
|
149
155
|
}
|
|
150
156
|
catch (error) {
|
|
151
157
|
const refusal = unregisteredResourceGuidance(error, declaration, connection, root);
|
|
@@ -192,9 +198,11 @@ export async function fileDeclaredRequests(root, client, tenantId, bundle) {
|
|
|
192
198
|
*/
|
|
193
199
|
export async function requestIntegrations(root, client, tenantId, bundle, options) {
|
|
194
200
|
const declaration = await readDeclaration(root);
|
|
201
|
+
if (options.allowBackground !== undefined && (!ENVIRONMENTS.includes(options.allowBackground) || !requestScope(declaration, options.connection, options.operations).some(part => part.identityMode === "user")))
|
|
202
|
+
throw new CliError("INPUT_INVALID", "--allow-background requires user-mode access and one environment: development, preview or production.");
|
|
195
203
|
const app = reviewedProfileBody(await readReviewedAppProfile(root));
|
|
196
204
|
const appId = await ensureLinkedApp(root, client, tenantId, bundle);
|
|
197
|
-
const submitted = settled(await submitRequests(client, appId, declaration, root, options.connection, app, options.reason, options.operations)).map(item => ({ ...item, state: item.outcome.state }));
|
|
205
|
+
const submitted = settled(await submitRequests(client, appId, declaration, root, options.connection, app, options.reason, options.operations, options.allowBackground)).map(item => ({ ...item, state: item.outcome.state }));
|
|
198
206
|
const sleep = options.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
|
|
199
207
|
const pollMs = options.pollMs ?? 3_000;
|
|
200
208
|
let grants = [];
|
|
@@ -216,6 +224,7 @@ export async function requestIntegrations(root, client, tenantId, bundle, option
|
|
|
216
224
|
}
|
|
217
225
|
const groups = groupGrants(grants);
|
|
218
226
|
return { appId, connection: options.connection, requests: submitted.map(item => ({
|
|
227
|
+
...(item.outcome.backgroundConsent ? { backgroundConsent: item.outcome.backgroundConsent } : {}),
|
|
219
228
|
identityMode: item.identityMode, operations: item.operations, resources: item.resources, ...(item.presentation ? { presentation: item.presentation } : {}),
|
|
220
229
|
...(item.outcome.requestId ? { requestId: item.outcome.requestId } : {}), grantId: item.outcome.grantId, state: item.state,
|
|
221
230
|
readiness: item.state === "PENDING" ? "pending" : grants.find(grant => grant.grantId === item.outcome.grantId)?.readiness ?? (item.state === "READY" ? "ready" : "denied"),
|
|
@@ -463,5 +472,24 @@ export async function integrationsStatus(root, client) {
|
|
|
463
472
|
if (!lock?.appId)
|
|
464
473
|
return { linked: false, grants: [], requests: [] };
|
|
465
474
|
const listed = await client.list(lock.appId);
|
|
466
|
-
return { appId: lock.appId, linked: true, grants: listed.grants.map(grant => ({ connection: grant.connection, environment: grant.environment, identityMode: grant.identityMode, status: grant.status, operations: grant.operations, resources: grant.resources, expiresAt: grant.expiresAt, readiness: grant.readiness, provisioning: grant.provisioning, pendingRequestId: grant.pendingRequestId, failure: grant.failure })), requests: listed.requests };
|
|
475
|
+
return { appId: lock.appId, linked: true, grants: listed.grants.map(grant => ({ connection: grant.connection, environment: grant.environment, identityMode: grant.identityMode, status: grant.status, operations: grant.operations, resources: grant.resources, expiresAt: grant.expiresAt, readiness: grant.readiness, provisioning: grant.provisioning, pendingRequestId: grant.pendingRequestId, failure: grant.failure })), requests: listed.requests, consents: listed.consents ?? [] };
|
|
476
|
+
}
|
|
477
|
+
async function linkedApp(root) {
|
|
478
|
+
const lock = await readKitLock(root);
|
|
479
|
+
if (!lock?.appId)
|
|
480
|
+
throw new CliError("APP_NOT_LINKED", "Run isomorph dev or integrations request for this app first.");
|
|
481
|
+
return lock.appId;
|
|
482
|
+
}
|
|
483
|
+
export async function connectIntegration(root, client, connection, returnUrl) {
|
|
484
|
+
const appId = await linkedApp(root);
|
|
485
|
+
let result;
|
|
486
|
+
try {
|
|
487
|
+
result = await client.connectIntegration(appId, connection, returnUrl);
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
if (!returnUrl && error instanceof CliError && error.code === "VALIDATION_FAILED")
|
|
491
|
+
throw new CliError(error.code, error.message, undefined, "Start isomorph dev, then repeat with --return-url <origin>/_harbour/integrations/oauth/complete using the returned local origin.");
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
return { appId, connection, ...result, nextStep: result.status === "connected" ? "Account connected. Run integrations status, then integrations request with --allow-background for the approved environment; connecting never enables it." : "Open authorizationUrl in the browser and let the owner finish provider consent, then run this command again. Use --return-url with the running app's local URL if required." };
|
|
467
495
|
}
|
package/dist/src/analyzer.js
CHANGED
|
@@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { lstat, readFile, readdir, stat } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, join, relative, sep } from "node:path";
|
|
6
|
-
import
|
|
6
|
+
import ignore from "ignore";
|
|
7
7
|
import { sha256 } from "./digest.js";
|
|
8
8
|
import { isEnvExamplePath, isKitCommittedPath, isSourceIntakeExcludedPath } from "./secret-paths.js";
|
|
9
9
|
const ANALYZER_VERSION = "0.1.0";
|
|
@@ -91,18 +91,18 @@ function extension(path) {
|
|
|
91
91
|
* Nested ignore files are not read. A missing file adds no rules.
|
|
92
92
|
*/
|
|
93
93
|
async function loadGitIgnore(root) {
|
|
94
|
-
|
|
94
|
+
const filter = ignore();
|
|
95
95
|
try {
|
|
96
|
-
|
|
96
|
+
filter.add(await readFile(join(root, ".gitignore"), "utf8"));
|
|
97
97
|
}
|
|
98
98
|
catch { /* no .gitignore: nothing extra is ignored */ }
|
|
99
|
-
return
|
|
99
|
+
return filter;
|
|
100
100
|
}
|
|
101
101
|
function isGitIgnored(filter, root, absolute, isDirectory) {
|
|
102
102
|
const path = relative(root, absolute).split(sep).join("/");
|
|
103
103
|
if (!path)
|
|
104
104
|
return false;
|
|
105
|
-
return filter.ignores(path
|
|
105
|
+
return filter.ignores(isDirectory ? `${path}/` : path);
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
108
|
* Which directories filled the boundary, for the error a builder reads when
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isomorph.ai/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.6",
|
|
4
4
|
"description": "Isomorph development kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"dist/src/analyzer.js",
|
|
19
19
|
"dist/src/contracts.js",
|
|
20
20
|
"dist/src/digest.js",
|
|
21
|
-
"dist/src/gitignore.js",
|
|
22
21
|
"dist/src/secret-paths.js",
|
|
23
22
|
"dist/src/source-digest.js",
|
|
24
23
|
"dist/src/source-intake.js",
|
|
@@ -28,7 +27,8 @@
|
|
|
28
27
|
"node": ">=22.13.0"
|
|
29
28
|
},
|
|
30
29
|
"dependencies": {
|
|
31
|
-
"embedded-postgres": "18.4.0-beta.17"
|
|
30
|
+
"embedded-postgres": "18.4.0-beta.17",
|
|
31
|
+
"ignore": "^7.0.10"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"prebuild": "node scripts/kit-bundle.mjs verify",
|
package/dist/src/gitignore.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
export function parseGitIgnore(content) {
|
|
2
|
-
const rules = [];
|
|
3
|
-
for (const raw of content.split(/\r?\n/)) {
|
|
4
|
-
let line = raw.replace(/\s+$/, "");
|
|
5
|
-
if (!line || line.startsWith("#"))
|
|
6
|
-
continue;
|
|
7
|
-
let negated = false;
|
|
8
|
-
if (line.startsWith("!")) {
|
|
9
|
-
negated = true;
|
|
10
|
-
line = line.slice(1);
|
|
11
|
-
}
|
|
12
|
-
else if (line.startsWith("\\!") || line.startsWith("\\#"))
|
|
13
|
-
line = line.slice(1);
|
|
14
|
-
let directoryOnly = false;
|
|
15
|
-
if (line.endsWith("/")) {
|
|
16
|
-
directoryOnly = true;
|
|
17
|
-
line = line.slice(0, -1);
|
|
18
|
-
}
|
|
19
|
-
if (!line)
|
|
20
|
-
continue;
|
|
21
|
-
let anchored = line.startsWith("/");
|
|
22
|
-
if (anchored)
|
|
23
|
-
line = line.slice(1);
|
|
24
|
-
if (line.includes("/"))
|
|
25
|
-
anchored = true;
|
|
26
|
-
const regex = new RegExp(`^${anchored ? "" : "(?:.*/)?"}${globToRegex(line)}(?:/.*)?$`);
|
|
27
|
-
rules.push({ regex, negated, directoryOnly });
|
|
28
|
-
}
|
|
29
|
-
return {
|
|
30
|
-
ignores(path, isDirectory) {
|
|
31
|
-
const normalized = path.replace(/^\/+|\/+$/g, "");
|
|
32
|
-
if (!normalized)
|
|
33
|
-
return false;
|
|
34
|
-
let ignored = false;
|
|
35
|
-
for (const rule of rules) {
|
|
36
|
-
if (rule.directoryOnly && !isDirectory && !parentMatches(rule.regex, normalized))
|
|
37
|
-
continue;
|
|
38
|
-
if (rule.regex.test(normalized))
|
|
39
|
-
ignored = !rule.negated;
|
|
40
|
-
}
|
|
41
|
-
return ignored;
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
/** A directory-only pattern still ignores a file beneath a matching directory. */
|
|
46
|
-
function parentMatches(regex, path) {
|
|
47
|
-
const parts = path.split("/");
|
|
48
|
-
for (let depth = 1; depth < parts.length; depth += 1)
|
|
49
|
-
if (regex.test(parts.slice(0, depth).join("/")))
|
|
50
|
-
return true;
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
function globToRegex(glob) {
|
|
54
|
-
let out = "";
|
|
55
|
-
for (let index = 0; index < glob.length; index += 1) {
|
|
56
|
-
const char = glob[index];
|
|
57
|
-
if (char === "*") {
|
|
58
|
-
if (glob[index + 1] === "*") {
|
|
59
|
-
index += 1;
|
|
60
|
-
if (glob[index + 1] === "/") {
|
|
61
|
-
index += 1;
|
|
62
|
-
out += "(?:.*/)?";
|
|
63
|
-
}
|
|
64
|
-
else
|
|
65
|
-
out += ".*";
|
|
66
|
-
}
|
|
67
|
-
else
|
|
68
|
-
out += "[^/]*";
|
|
69
|
-
}
|
|
70
|
-
else if (char === "?")
|
|
71
|
-
out += "[^/]";
|
|
72
|
-
else if (char === "\\" && index + 1 < glob.length) {
|
|
73
|
-
index += 1;
|
|
74
|
-
out += escape(glob[index]);
|
|
75
|
-
}
|
|
76
|
-
else
|
|
77
|
-
out += escape(char);
|
|
78
|
-
}
|
|
79
|
-
return out;
|
|
80
|
-
}
|
|
81
|
-
function escape(char) { return /[.*+?^${}()|[\]\\/]/.test(char) ? `\\${char}` : char; }
|