@withandeo/cli 0.19.13 → 0.19.15

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/main.js CHANGED
@@ -227,8 +227,10 @@ async function execute(topic, options, cwd, runtime) {
227
227
  return { value: await linkProject(cwd, options, runtime) };
228
228
  case "doctor":
229
229
  return await doctor(cwd, options, runtime);
230
- case "preview":
231
- return { value: await preview(cwd, options, runtime) };
230
+ case "preview": {
231
+ const value = await preview(cwd, options, runtime);
232
+ return { value, exitCode: value.status === "timeout" ? 3 : 0 };
233
+ }
232
234
  case "preview entries":
233
235
  return { value: await previewEntries(cwd, options, runtime) };
234
236
  case "preview open":
@@ -1468,6 +1470,12 @@ async function sourceMerge(cwd, options, runtime) {
1468
1470
  throw new CliError("argument_invalid", "Administrator authorization flags require --validation-mode administrator_authorized.");
1469
1471
  }
1470
1472
  const access = await sourceReadAccess(cwd, options, runtime);
1473
+ if (access.repository.assignedFeatureBranch) {
1474
+ if (headBranch && headBranch !== access.repository.assignedFeatureBranch)
1475
+ throw new CliError("agent_branch_scope_mismatch", "This run may merge only its assigned feature branch.");
1476
+ if (validationMode === "administrator_authorized")
1477
+ throw new CliError("agent_merge_mode_invalid", "Persistent agents use the native preview merge workflow under source.merge.");
1478
+ }
1471
1479
  const result = await access.client.sourceMerge(access.repository.id, { headBranch, title, changeId, idempotencyKey,
1472
1480
  expectedHead, expectedSnapshot: expectedSnapshot ? Number(expectedSnapshot) : undefined, dryRun,
1473
1481
  expectedBase, expectedPolicyRevision: policyRevision === undefined ? undefined : Number(policyRevision),
@@ -1487,7 +1495,7 @@ async function sourceMerge(cwd, options, runtime) {
1487
1495
  if (merge.stage === "awaiting_approval")
1488
1496
  return { ...result, merge };
1489
1497
  await runtime.sleep(Math.min(5000, timeout * 1000 - poll * 5000));
1490
- const current = merge.changeId ? await access.client.sourceChangeRequest(merge.changeId) : await access.client.sourceMergeStatus(merge.id);
1498
+ const current = merge.changeId && !access.repository.assignedFeatureBranch ? await access.client.sourceChangeRequest(merge.changeId) : await access.client.sourceMergeStatus(merge.id);
1491
1499
  if ("changeRequest" in current)
1492
1500
  requireLinkedSourceChange(access.projectId, current.changeRequest.projectId);
1493
1501
  if (!current.merge || current.merge.id !== merge.id)
@@ -2120,6 +2128,11 @@ async function preview(cwd, options, runtime, uploadOnly = false) {
2120
2128
  if (!offline && !context.link)
2121
2129
  throw new CliError("project_not_linked", "This app is not linked to an Andeo project.", "andeo link --json");
2122
2130
  const authorized = offline ? null : await authenticatedClient(cwd, context.link, options, runtime);
2131
+ if (authorized?.persistent) {
2132
+ if (uploadOnly)
2133
+ throw new CliError("agent_artifact_upload_unavailable", "Persistent agents create previews from their assigned managed source branch.", "andeo preview --json");
2134
+ return persistentSourcePreview(context, options, runtime, authorized.client);
2135
+ }
2123
2136
  if (authorized && context.link && !uploadOnly) {
2124
2137
  assertPreviewEntry(await authorized.client.previewEntries(context.link.projectId), options.flags.get("through"));
2125
2138
  }
@@ -2158,6 +2171,7 @@ async function preview(cwd, options, runtime, uploadOnly = false) {
2158
2171
  let delivery = await client.createDelivery(context.link.projectId, {
2159
2172
  submissionId: finalized.id,
2160
2173
  idempotencyKey: options.flags.get("delivery-key")
2174
+ ?? options.flags.get("idempotency-key")
2161
2175
  ?? generatedDeliveryKey(context.link.projectId, finalized.id),
2162
2176
  });
2163
2177
  if (options.booleans.has("no-wait")) {
@@ -2190,6 +2204,55 @@ async function preview(cwd, options, runtime, uploadOnly = false) {
2190
2204
  productionChanged: false,
2191
2205
  };
2192
2206
  }
2207
+ async function persistentSourcePreview(context, options, runtime, client) {
2208
+ const principal = await client.whoami();
2209
+ if (principal.actorKind !== "persistent_agent")
2210
+ throw new CliError("agent_authentication_failed", "The Cloud credential did not authenticate a persistent agent.");
2211
+ const projectId = context.link.projectId;
2212
+ if (!principal.projects.some(project => project.projectId === projectId))
2213
+ throw new CliError("project_not_found", "This run does not have access to the linked project.");
2214
+ if (options.flags.has("through"))
2215
+ throw new CliError("agent_preview_entry_unsupported", "Persistent previews use their isolated entry; --through is not supported.");
2216
+ const repository = (await client.sourceRepository(projectId)).repository;
2217
+ if (!repository?.assignedFeatureBranch || repository.status !== "active")
2218
+ throw new CliError("source_repository_missing", "An active run-assigned managed repository is required.");
2219
+ const source = await sourceState(context.root, runtime);
2220
+ if (source.dirty)
2221
+ throw new CliError("preview_committed_source_required", "Commit and push the exact source before requesting an isolated preview.", "andeo source push --json");
2222
+ const timeout = Number(options.flags.get("timeout-seconds") ?? "1200");
2223
+ if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 3600)
2224
+ throw new CliError("argument_invalid", "--timeout-seconds must be between 1 and 3600.");
2225
+ const idempotencyKey = options.flags.get("idempotency-key") ?? options.flags.get("delivery-key") ?? `source-preview-${createHash("sha256").update(JSON.stringify({ repositoryId: repository.id, headBranch: repository.assignedFeatureBranch, headSha: source.revision })).digest("hex")}`;
2226
+ if (!/^[A-Za-z0-9:_-]{8,128}$/u.test(idempotencyKey))
2227
+ throw new CliError("argument_invalid", "--idempotency-key must contain 8–128 letters, numbers, colons, underscores or hyphens.");
2228
+ const result = await client.createSourcePreview(repository.id, { headBranch: repository.assignedFeatureBranch, expectedHeadSha: source.revision, idempotencyKey });
2229
+ let preview = result.preview;
2230
+ const assertExact = () => {
2231
+ if (!/^sbl_[A-Za-z0-9_-]+$/u.test(preview.id) || preview.buildId !== preview.id || preview.headBranch !== repository.assignedFeatureBranch || preview.headSha !== source.revision)
2232
+ throw new CliError("source_preview_changed", "The returned preview does not match the exact assigned source.");
2233
+ };
2234
+ assertExact();
2235
+ const summary = () => ({ created: result.created, preview, projectId, sourceRevision: source.revision, releaseId: preview.releaseId, compositionId: preview.previewCompositionId,
2236
+ from: preview.id, productionChanged: false, nextCommand: `andeo preview open --from ${preview.id} --json` });
2237
+ if (options.booleans.has("no-wait"))
2238
+ return { ...summary(), status: preview.status };
2239
+ for (let poll = 0; poll < Math.ceil(timeout / 5) && !["succeeded", "preview_ready", "failed"].includes(preview.status); poll++) {
2240
+ await runtime.sleep(Math.min(5000, timeout * 1000 - poll * 5000));
2241
+ const previousId = preview.id;
2242
+ preview = (await client.sourcePreview(previousId)).preview;
2243
+ assertExact();
2244
+ if (preview.id !== previousId)
2245
+ throw new CliError("source_preview_changed", "The preview operation changed while waiting.");
2246
+ }
2247
+ if (preview.status === "failed")
2248
+ throw new CliError(preview.error?.code ?? "source_preview_failed", preview.error?.message ?? "The isolated preview build failed.", undefined, { from: preview.id });
2249
+ if (!["succeeded", "preview_ready"].includes(preview.status))
2250
+ return { ...summary(), status: "timeout", waitTimedOut: true };
2251
+ const session = await client.openSourcePreview(preview.id, options.flags.get("return-path") ?? context.config.returnPath);
2252
+ if (session.projectId !== projectId || session.from !== preview.id || session.sourceRevision !== source.revision)
2253
+ throw new CliError("source_preview_changed", "The opened preview does not match the exact linked source.");
2254
+ return { ...summary(), status: "succeeded", previewUrl: session.url, previewGrantId: session.grantId, previewExpiresAt: session.expiresAt, sourceVerification: session.sourceVerification };
2255
+ }
2193
2256
  async function deliveryStatus(cwd, options, runtime) {
2194
2257
  const context = await loadProjectContext(cwd);
2195
2258
  const { client } = await authenticatedClient(cwd, context.link, options, runtime);
@@ -2223,7 +2286,23 @@ async function previewOpen(cwd, options, runtime) {
2223
2286
  const from = options.flags.get("from");
2224
2287
  if (!from || !/^(dly|sbl)_[A-Za-z0-9_-]+$/.test(from))
2225
2288
  throw new CliError("preview_source_invalid", "Use --from with an exact dly_ delivery or sbl_ source-build ID.");
2226
- const { client } = await authenticatedClient(cwd, context.link, options, runtime);
2289
+ const access = await authenticatedClient(cwd, context.link, options, runtime);
2290
+ const { client } = access;
2291
+ if (access.persistent) {
2292
+ const principal = await client.whoami();
2293
+ if (principal.actorKind !== "persistent_agent")
2294
+ throw new CliError("agent_authentication_failed", "The Cloud credential did not authenticate a persistent agent.");
2295
+ if (!principal.projects.some(project => project.projectId === context.link.projectId))
2296
+ throw new CliError("project_not_found", "This run does not have access to the linked project.");
2297
+ if (!/^sbl_[A-Za-z0-9_-]+$/u.test(from))
2298
+ throw new CliError("preview_source_invalid", "Persistent preview opening requires an exact sbl_ source-build ID.");
2299
+ if (options.flags.has("through"))
2300
+ throw new CliError("agent_preview_entry_unsupported", "Persistent previews use their isolated entry; --through is not supported.");
2301
+ const session = await client.openSourcePreview(from, options.flags.get("return-path") ?? context.config.returnPath);
2302
+ if (session.projectId !== context.link.projectId || session.from !== from)
2303
+ throw new CliError("source_preview_changed", "The opened preview belongs to another linked source.");
2304
+ return session;
2305
+ }
2227
2306
  return client.openPreview(context.link.projectId, { from, returnPath: options.flags.get("return-path") ?? context.config.returnPath,
2228
2307
  entryProjectId: options.flags.get("through") });
2229
2308
  }
@@ -2238,7 +2317,7 @@ async function authenticatedClient(cwd, link, options, runtime) {
2238
2317
  let apiUrl = selectedApiUrl(options, link, runtime.environment, runtime.defaultApiUrl);
2239
2318
  const cloud = await cloudAgentClient(runtime.environment, cwd, runtime.fetcher, { tokenStdin: options.booleans.has("token-stdin"), profile: options.flags.has("profile"), apiUrl });
2240
2319
  if (cloud)
2241
- return { client: new AgentApiClient(apiUrl, "task-run", cloud.fetcher), apiUrl, source: "cloud-agent", accessTokenExpiresAt: cloud.accessTokenExpiresAt };
2320
+ return { client: new AgentApiClient(apiUrl, "task-run", cloud.fetcher), apiUrl, source: "cloud-agent", accessTokenExpiresAt: cloud.accessTokenExpiresAt, persistent: true };
2242
2321
  const networkSecret = readNetworkSecret(runtime.environment, apiUrl);
2243
2322
  if (networkSecret !== undefined) {
2244
2323
  if (options.booleans.has("token-stdin") || options.flags.has("profile")) {
@@ -2254,7 +2333,7 @@ async function authenticatedClient(cwd, link, options, runtime) {
2254
2333
  : runtime.environment.TENDER_ACCOUNTS_TOKEN
2255
2334
  ? "environment"
2256
2335
  : "dev-vars";
2257
- return { client: new AgentApiClient(apiUrl, explicit, runtime.fetcher), apiUrl, source };
2336
+ return { client: new AgentApiClient(apiUrl, explicit, runtime.fetcher), apiUrl, source, persistent: /^ta_run_agc_[a-f0-9]{24}\.[A-Za-z0-9_-]{43}$/u.test(explicit) };
2258
2337
  }
2259
2338
  const profile = await selectAuthProfile(cwd, options, runtime);
2260
2339
  if (!options.flags.has("api-url") && !runtime.environment.TENDER_ACCOUNTS_API_URL && !link) {
@@ -2872,6 +2951,7 @@ function assertAllowedOptions(topic, options) {
2872
2951
  "no-wait",
2873
2952
  "submission-key",
2874
2953
  "delivery-key",
2954
+ "idempotency-key",
2875
2955
  "timeout-seconds",
2876
2956
  "return-path",
2877
2957
  "through",
@@ -3156,7 +3236,7 @@ Account workflow entitlement is required for new starts. Run status is authorita
3156
3236
  check: "Usage: andeo check [--json]\n\nRun the app's declared validation command.",
3157
3237
  build: "Usage: andeo build [--json]\n\nRun validation and packaging, then verify the complete portable artifact.",
3158
3238
  upload: "Usage: andeo upload [--dry-run] [--submission-key KEY] [--json]\n\nValidate, package and retain an exact artifact for administrator review. Does not create a preview or publish production.",
3159
- preview: "Usage: andeo preview [--through PROJECT_ID] [--return-path /account] [--dry-run] [--no-wait] [--json]\n\nBuild an exact app preview. Published connections select its entry automatically. Use preview entries to inspect choices or preview open --from dly_... to reopen without rebuilding.",
3239
+ preview: "Usage: andeo preview [--through PROJECT_ID] [--return-path /account] [--dry-run] [--no-wait] [--idempotency-key KEY] [--json]\n\nBuild an exact app preview. Persistent Cloud agents with preview.build/open request an isolated build of their assigned pushed branch; no local artifact upload is used. Unsupported bindings fail closed. Other credentials use local packaging and published entry selection. Use preview open --from sbl_... (persistent) or dly_... to reopen without rebuilding.",
3160
3240
  production: "Usage: andeo production <releases|rollback> [options]",
3161
3241
  "production releases": "Usage: andeo production releases [--project PROJECT_ID] [--json]\n\nList immutable production releases and rollback eligibility.",
3162
3242
  "production rollback": "Usage: andeo production rollback [--project PROJECT_ID] --release RELEASE_ID --expected-release CURRENT_ID --expected-revision N --idempotency-key KEY [--json]\n\nMerchant administrators restore a previously successful app release without building or uploading code.",
@@ -3181,7 +3261,7 @@ Account workflow entitlement is required for new starts. Run status is authorita
3181
3261
  "source change": "Usage: andeo source change <create|list|show|close|resume|cancel|take-over> [options]\n\nCreate and inspect exact preview-backed changes, or recover an accepted merge intent.",
3182
3262
  "source change cancel": "Usage: andeo source change cancel --change scr_... [--json]\n\nWithdraw new work for an accepted intent. Already-issued effects require readback; source may already be merged.",
3183
3263
  "source change take-over": "Usage: andeo source change take-over --change scr_... --idempotency-key KEY [--json]\n\nAdministrator only. Records a new bounded authorization for the same exact inputs; then use source change resume.",
3184
- "source merge": "Usage: andeo source merge --head BRANCH --title TITLE --idempotency-key KEY [--dry-run] [--wait] [--timeout-seconds N] [--json]\n andeo source merge --change scr_... --idempotency-key KEY [--wait] [--json]\n andeo source merge --head BRANCH --title TITLE --validation-mode administrator_authorized --expected-base SHA --expected-head SHA --expected-policy-revision N --authorize --idempotency-key KEY [--continuation source_only] [--note TEXT] [--wait] [--json]\n andeo source merge <status|resume|cancel|take-over> --merge smi_... [options]\n\nRequest exact fast-forward landing. Native review remains the default. Administrator-authorized source-only mode requires saved policy and a current administrator; Andeo does not independently verify checks. Inspect refs and blockers with --dry-run before authorizing. Waiting is bounded and never cancels hosted work.\n\nExamples:\n andeo source merge --head feature/copy --title \"Update copy\" --idempotency-key account-copy-v1 --dry-run --json\n andeo source merge --head feature/copy --title \"Update copy\" --idempotency-key account-copy-v1 --wait --json",
3264
+ "source merge": "Usage: andeo source merge --head BRANCH --title TITLE --idempotency-key KEY [--dry-run] [--wait] [--timeout-seconds N] [--json]\n andeo source merge --change scr_... --idempotency-key KEY [--wait] [--json]\n andeo source merge --head BRANCH --title TITLE --validation-mode administrator_authorized --expected-base SHA --expected-head SHA --expected-policy-revision N --authorize --idempotency-key KEY [--continuation source_only] [--note TEXT] [--wait] [--json]\n andeo source merge <status|resume|cancel|take-over> --merge smi_... [options]\n\nRequest exact fast-forward landing. Persistent agents require source.merge for the assigned branch and review.prepare for implicit review creation. The grant delegates source approval without per-change administrator approval; a successful exact isolated preview is required. Landing may deploy under the saved publication policy. Human/legacy native review keeps its existing approval contract. Administrator-authorized source-only mode requires saved policy and a current administrator; Andeo does not independently verify checks. Inspect refs and blockers with --dry-run before authorizing. Waiting is bounded and never cancels hosted work.\n\nExamples:\n andeo source merge --head feature/copy --title \"Update copy\" --idempotency-key account-copy-v1 --dry-run --json\n andeo source merge --head feature/copy --title \"Update copy\" --idempotency-key account-copy-v1 --wait --json",
3185
3265
  "source merge status": "Usage: andeo source merge status --merge smi_... [--json]\n\nRead the retained source, validation, authorization, continuation and cleanup outcomes. Source-only merged is successful completion.",
3186
3266
  "source merge resume": "Usage: andeo source merge resume --merge smi_... [--json]\n\nResume the same exact eligible intent after revalidating authority. Never substitutes a new source snapshot.",
3187
3267
  "source merge cancel": "Usage: andeo source merge cancel --merge smi_... [--json]\n\nWithdraw new work. Already-issued effects require readback; cancellation cannot undo a merge.",
@@ -3207,7 +3287,7 @@ Account workflow entitlement is required for new starts. Run status is authorita
3207
3287
  delivery: "Usage: andeo delivery <status|retry> --delivery dly_... [--json]\n\nThese commands accept the developer-delivery ID returned by preview. A dwf_ ID is a production workflow managed by merchant administrators in Activity.",
3208
3288
  "delivery status": "Usage: andeo delivery status --delivery dly_... [--json]\n\nRead one developer preview delivery. Do not pass a dwf_ production workflow ID.",
3209
3289
  "preview entries": "Usage: andeo preview entries [--json]\n\nList eligible published entry apps and the selected default for this linked app.",
3210
- "preview open": "Usage: andeo preview open --from dly_...|sbl_... [--through PROJECT_ID] [--return-path /account] [--json]\n\nOpen the exact deployed preview without rebuilding or uploading. Private apps open through their published connections. Opening again renews access.",
3290
+ "preview open": "Usage: andeo preview open --from dly_...|sbl_... [--through PROJECT_ID] [--return-path /account] [--json]\n\nOpen the exact deployed preview without rebuilding or uploading. Persistent agents require preview.open and an exact permitted sbl_ source preview; --through is unavailable and access remains bounded by the run/derived grant. Other private apps open through their published connections.",
3211
3291
  "delivery retry": "Usage: andeo delivery retry --delivery dly_... [--reauthorize] [--json]\n\nRetry one requested or failed developer preview idempotently. --reauthorize explicitly authorizes a legacy preview for the same owner and linked project using the current CLI login; it cannot replace another login's recorded grant or publish production. Normal credential refresh needs no flag. A dwf_ production workflow is resumed by a merchant administrator from Activity.",
3212
3292
  skill: "Usage: andeo skill <print|install|status|sync> [options]",
3213
3293
  "skill status": "Usage: andeo skill status [--agent codex|claude] [--directory PATH] [--json]\n\nCheck the installed guide against the compatible public revision. Reports local_changes, unmanaged or not_installed without overwriting files. Offline and incompatible checks fail explicitly.",