run402 4.82.0 → 4.84.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 (65) hide show
  1. package/cli.mjs +1 -1
  2. package/gitvault-surface.json +1 -1
  3. package/lib/deploy-v2.mjs +41 -71
  4. package/lib/doctor-source-scan.mjs +2 -547
  5. package/lib/doctor.mjs +28 -8
  6. package/lib/sdk-errors.mjs +7 -0
  7. package/lib/status.mjs +1 -1
  8. package/lib/up.mjs +62 -21
  9. package/lib/up.test.mjs +16 -0
  10. package/lib/update-check.mjs +11 -6
  11. package/lib/update-check.test.mjs +18 -5
  12. package/package.json +1 -1
  13. package/sdk/dist/actions.d.ts +16 -3
  14. package/sdk/dist/actions.d.ts.map +1 -1
  15. package/sdk/dist/config.d.ts +1 -1
  16. package/sdk/dist/config.d.ts.map +1 -1
  17. package/sdk/dist/config.js.map +1 -1
  18. package/sdk/dist/index.d.ts +2 -0
  19. package/sdk/dist/index.d.ts.map +1 -1
  20. package/sdk/dist/index.js +2 -0
  21. package/sdk/dist/index.js.map +1 -1
  22. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  23. package/sdk/dist/namespaces/deploy.js +2 -1
  24. package/sdk/dist/namespaces/deploy.js.map +1 -1
  25. package/sdk/dist/namespaces/deploy.types.d.ts +6 -0
  26. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  27. package/sdk/dist/namespaces/edge-evidence.d.ts +22 -0
  28. package/sdk/dist/namespaces/edge-evidence.d.ts.map +1 -0
  29. package/sdk/dist/namespaces/edge-evidence.js +38 -0
  30. package/sdk/dist/namespaces/edge-evidence.js.map +1 -0
  31. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  32. package/sdk/dist/namespaces/projects.js +9 -7
  33. package/sdk/dist/namespaces/projects.js.map +1 -1
  34. package/sdk/dist/node/actions-node.d.ts +18 -0
  35. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  36. package/sdk/dist/node/actions-node.js +139 -91
  37. package/sdk/dist/node/actions-node.js.map +1 -1
  38. package/sdk/dist/node/app-scope.d.ts +24 -0
  39. package/sdk/dist/node/app-scope.d.ts.map +1 -0
  40. package/sdk/dist/node/app-scope.js +47 -0
  41. package/sdk/dist/node/app-scope.js.map +1 -0
  42. package/sdk/dist/node/deploy-manifest.d.ts +6 -0
  43. package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
  44. package/sdk/dist/node/deploy-manifest.js +61 -0
  45. package/sdk/dist/node/deploy-manifest.js.map +1 -1
  46. package/sdk/dist/node/index.d.ts +6 -1
  47. package/sdk/dist/node/index.d.ts.map +1 -1
  48. package/sdk/dist/node/index.js +8 -2
  49. package/sdk/dist/node/index.js.map +1 -1
  50. package/sdk/dist/node/manifest-export.d.ts +5 -0
  51. package/sdk/dist/node/manifest-export.d.ts.map +1 -0
  52. package/sdk/dist/node/manifest-export.js +78 -0
  53. package/sdk/dist/node/manifest-export.js.map +1 -0
  54. package/sdk/dist/node/preflight.d.ts +65 -0
  55. package/sdk/dist/node/preflight.d.ts.map +1 -0
  56. package/sdk/dist/node/preflight.js +23 -0
  57. package/sdk/dist/node/preflight.js.map +1 -0
  58. package/sdk/dist/node/source-scan.d.ts +73 -0
  59. package/sdk/dist/node/source-scan.d.ts.map +1 -0
  60. package/sdk/dist/node/source-scan.js +552 -0
  61. package/sdk/dist/node/source-scan.js.map +1 -0
  62. package/sdk/dist/rest-diagnostics.d.ts +15 -0
  63. package/sdk/dist/rest-diagnostics.d.ts.map +1 -0
  64. package/sdk/dist/rest-diagnostics.js +19 -0
  65. package/sdk/dist/rest-diagnostics.js.map +1 -0
@@ -1,3 +1,7 @@
1
+ import { serializeDeployManifest, manifestExportUnsupported } from "./manifest-export.js";
2
+ import { describeLocalPreflight } from "./preflight.js";
3
+ import { resolveApplicationScope, materializeTemplates } from "./app-scope.js";
4
+ import { scanDeploymentSources } from "./source-scan.js";
1
5
  import { createHash } from "node:crypto";
2
6
  import { execFile } from "node:child_process";
3
7
  import { lstat, mkdir, mkdtemp, readdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
@@ -191,9 +195,23 @@ export class NodeActions {
191
195
  }
192
196
  }
193
197
  async #runUpFromWorkspace(input, run, source, startedAt) {
194
- const workspaceDir = source.workspaceDir;
198
+ let workspaceDir = source.workspaceDir;
195
199
  const manifest = await this.#discoverAndValidateManifest(input, workspaceDir, source.metadata, run);
200
+ workspaceDir = dirname(manifest.manifestPath);
201
+ const target = !input.verifyOnly ? await resolveDeploymentTarget({
202
+ appRoot: workspaceDir, manifestPath: manifest.manifestPath, projectId: input.projectId,
203
+ manifestProjectId: manifest.manifestProjectId ?? manifest.appSpec?.project.id, name: input.name,
204
+ targetKind: this.#targetKind(), apiBase: this.sdk.apiBase,
205
+ allowUnresolved: run.executionMode === "check" || (run.executionMode === "printSpec" || run.executionMode === "printManifest") || run.dryRun,
206
+ }) : null;
207
+ if (!input.verifyOnly && process.env.RUN402_DEPLOY_SKIP_SCAN !== "1") {
208
+ const scan = scanDeploymentSources(manifest.releaseSpec ?? {}, workspaceDir);
209
+ if (scan.errors.length)
210
+ throw run.error("Source scan blocked this application.", "R402_AUTH_PREFLIGHT_FAILED", scan);
211
+ }
196
212
  if (manifest.manifestKind === "app") {
213
+ if (run.executionMode === "printManifest")
214
+ manifestExportUnsupported("app", "app resources and build intent cannot be faithfully represented as a release manifest");
197
215
  if (input.verifyOnly) {
198
216
  return this.#verifyAppManifestOnly(input, manifest, workspaceDir, run, startedAt);
199
217
  }
@@ -202,22 +220,23 @@ export class NodeActions {
202
220
  startedAt,
203
221
  status: block ? "blocked" : "planned",
204
222
  dryRun: run.dryRun || run.executionMode === "check" || run.executionMode === "printSpec",
205
- projectId: input.projectId ?? (run.dryRun ? "prj_planned" : null),
223
+ projectId: target?.project_id ?? null,
206
224
  diagnostics: block?.diagnostics,
207
225
  nextActions: block?.nextActions,
208
226
  blockedNodeId: block?.nodeId,
209
227
  });
210
228
  if (run.executionMode === "check" || run.executionMode === "printSpec" || run.dryRun) {
211
229
  return run.result({
212
- project_id: input.projectId ?? "prj_planned",
230
+ project_id: target?.project_id ?? null,
213
231
  manifest_path: manifest.manifestPath,
214
232
  app_graph: manifest.appGraph,
215
233
  app_result: appResult,
234
+ preflight: describeLocalPreflight({ appRoot: workspaceDir, manifestPath: manifest.manifestPath, target: target, authoring: manifest.appSpec?.release, spec: manifest.releaseSpec ?? undefined, buildDeferred: !!manifest.appSpec?.build?.commands?.length, apiBase: this.sdk.apiBase, profile: this.opts.profile }),
216
235
  });
217
236
  }
218
237
  if (block) {
219
238
  return run.result({
220
- project_id: input.projectId ?? "prj_planned",
239
+ project_id: target?.project_id ?? null,
221
240
  manifest_path: manifest.manifestPath,
222
241
  app_graph: manifest.appGraph,
223
242
  app_result: appResult,
@@ -228,14 +247,16 @@ export class NodeActions {
228
247
  if (input.verifyOnly) {
229
248
  return this.#verifyDeployManifestOnly(input, manifest, workspaceDir, run);
230
249
  }
231
- if (run.executionMode === "check" || run.executionMode === "printSpec") {
250
+ if (run.executionMode === "check" || (run.executionMode === "printSpec" || run.executionMode === "printManifest")) {
232
251
  if (!manifest.releaseSpec) {
233
252
  throw run.error("Internal error: release manifest did not produce a ReleaseSpec.", "RUN402_ACTION_INTERNAL");
234
253
  }
235
254
  return run.result({
236
- project_id: manifest.releaseSpec.project,
255
+ project_id: target?.project_id ?? null,
237
256
  manifest_path: manifest.manifestPath,
238
- ...(run.executionMode === "printSpec" ? { spec: manifest.releaseSpec } : {}),
257
+ preflight: describeLocalPreflight({ appRoot: workspaceDir, manifestPath: manifest.manifestPath, target: target, authoring: manifest.loaded?.manifest, spec: manifest.releaseSpec, apiBase: this.sdk.apiBase, profile: this.opts.profile }),
258
+ ...(run.executionMode === "printManifest" ? { manifest: serializeDeployManifest(manifest.loaded, workspaceDir, input.projectId) } : {}),
259
+ ...(run.executionMode === "printSpec" ? { spec: { ...manifest.releaseSpec, project: target?.project_id ?? undefined } } : {}),
239
260
  });
240
261
  }
241
262
  if (run.executionMode === "plan") {
@@ -272,7 +293,7 @@ export class NodeActions {
272
293
  schema_version: "run402.workspace-project.v1",
273
294
  project_id: resolved.projectId,
274
295
  ...(input.name ? { name: input.name } : {}),
275
- target: { kind: this.#targetKind() },
296
+ target: { kind: this.#targetKind(), api_base: this.sdk.apiBase },
276
297
  created_at: resolved.link?.created_at ?? new Date().toISOString(),
277
298
  updated_at: new Date().toISOString(),
278
299
  }, resolved.link, run);
@@ -283,14 +304,14 @@ export class NodeActions {
283
304
  mutation: run.executionMode !== "plan",
284
305
  auto: false,
285
306
  details: {
286
- project_id: resolved.projectId,
307
+ project_id: resolved.projectId === "prj_planned" ? null : resolved.projectId,
287
308
  manifest_path: manifest.manifestPath,
288
309
  },
289
310
  });
290
311
  if (run.dryRun) {
291
312
  run.setState(deployStep, "planned");
292
313
  return run.result({
293
- project_id: resolved.projectId,
314
+ project_id: resolved.projectId === "prj_planned" ? null : resolved.projectId,
294
315
  manifest_path: manifest.manifestPath,
295
316
  ...(resolved.shouldWriteLink ? { workspace_link_path: resolved.linkPath } : {}),
296
317
  });
@@ -401,7 +422,7 @@ export class NodeActions {
401
422
  * principal has a display name before the deploy that will be credited to
402
423
  * it, then join the project's room under that name. Explicit
403
424
  * `identityName` / `RUN402_AGENT_NAME` wins and may rename an already-named
404
- * principal; otherwise a detected client name is set only when no name
425
+ * agent principal; human and unknown principals are preserved. Otherwise a detected client name is set only when no name
405
426
  * exists yet, and reported as `detected`. The detected client is always
406
427
  * reported (`detected` + `detection`), even when it was not applied, so an
407
428
  * agent whose principal is already named sees why. Best-effort end to
@@ -412,15 +433,23 @@ export class NodeActions {
412
433
  if (this.#targetKind() === "core") {
413
434
  return { display_name: null, source: "unavailable" };
414
435
  }
436
+ const explicitName = input.identityName?.trim() || declaredAgentName() || null;
437
+ const detectedName = detectClientName();
415
438
  let current = null;
439
+ let principalId = null;
440
+ let principalType = null;
416
441
  try {
417
442
  const me = await this.sdk.orgs.whoami();
443
+ principalId = me.principal?.id ?? null;
444
+ principalType = me.principal?.type ?? null;
418
445
  // The gateway never seeds display_name from a wallet subject (and
419
446
  // nulled the ones it once did), so null here means exactly "unset".
420
447
  current = me.principal?.display_name ?? null;
421
448
  }
422
449
  catch {
423
- return { display_name: null, source: "unavailable" };
450
+ return { display_name: null, source: "unavailable", detected: detectedName,
451
+ principal: { id: null, type: null, display_name: null },
452
+ client: { detected: detectedName, declared_name: explicitName } };
424
453
  }
425
454
  let displayName = current;
426
455
  let source = "existing";
@@ -428,8 +457,13 @@ export class NodeActions {
428
457
  // (RUN402_AGENT_NAME), then a specifically detected client. A generic
429
458
  // guess is never persisted as a name. Detection always runs so the
430
459
  // result can say what was seen even when it was not applied.
431
- const explicitName = input.identityName?.trim() || declaredAgentName() || null;
432
- const detectedName = detectClientName();
460
+ if (principalType !== "agent") {
461
+ run.skipStep({ action: "identity.name.set", description: "Preserve authenticated human or unknown principal identity; client detection does not rename it.", mutation: false, auto: true, details: { principal_type: principalType } });
462
+ return { display_name: current, source: "existing",
463
+ principal: { id: principalId, type: principalType, display_name: current },
464
+ client: { detected: detectedName, declared_name: explicitName },
465
+ detected: detectedName, detection: { applied: false, reason: "principal_identity_preserved" } };
466
+ }
433
467
  const detectionReport = (applied) => {
434
468
  const reason = applied
435
469
  ? "applied"
@@ -438,7 +472,9 @@ export class NodeActions {
438
472
  : explicitName
439
473
  ? "explicit_name_wins"
440
474
  : "name_already_set";
441
- return { detected: detectedName, detection: { applied, reason } };
475
+ return { detected: detectedName, detection: { applied, reason },
476
+ principal: { id: principalId, type: principalType, display_name: displayName },
477
+ client: { detected: detectedName, declared_name: explicitName } };
442
478
  };
443
479
  if (!displayName || (explicitName && explicitName !== displayName)) {
444
480
  const desired = explicitName ?? detectedName;
@@ -588,14 +624,26 @@ export class NodeActions {
588
624
  // level down) usually is one `--dir` away from the manifest it meant.
589
625
  // Name what is one directory down so the hop is executable.
590
626
  const nearby = await findNearbyManifests(workspaceDir);
591
- const nearbyActions = nearby.map((entry) => ({
627
+ const selectorArgs = [...(input.projectId ? ["--project", input.projectId] : []), ...(input.name ? ["--name", input.name] : [])];
628
+ const candidateActions = await Promise.all(nearby.map(async (entry) => ({
629
+ binding: await nearbyBinding(entry.path),
630
+ cwd: workspaceDir,
631
+ env: { RUN402_API_BASE: this.sdk.apiBase, ...(this.opts.profile ? { RUN402_WALLET: this.opts.profile } : {}) },
592
632
  type: "run_in_directory",
593
- command: `run402 up --check --dir ${shellArg(entry.relative_dir)}`,
594
- argv: ["run402", "up", "--check", "--dir", entry.relative_dir],
633
+ command: ["run402", "up", "--check", "--dir", entry.relative_dir, ...selectorArgs].map(shellArg).join(" "),
634
+ argv: ["run402", "up", "--check", "--dir", entry.relative_dir, ...selectorArgs],
635
+ app_root: dirname(entry.path),
636
+ manifest_path: entry.path,
637
+ safe_to_auto_execute: true,
595
638
  path: entry.path,
596
639
  dir: entry.relative_dir,
597
640
  why: "A manifest exists one directory down; up does not walk into subdirectories.",
598
- }));
641
+ })));
642
+ const nearbyActions = nearby.length > 1 ? [{
643
+ type: "select_application", safe_to_auto_execute: false,
644
+ why: "Choose the intended application; directory order does not indicate a recommendation.",
645
+ candidates: candidateActions.map((entry) => ({ ...entry, recommended: false })),
646
+ }] : candidateActions;
599
647
  throw run.error(nearby.length > 0
600
648
  ? `No deploy manifest found in ${workspaceDir}, but ${nearby.length === 1 ? "one exists" : `${nearby.length} exist`} one directory down (${nearby.map((entry) => entry.relative_dir).join(", ")}). Re-run with --dir <that directory>, or add run402.json, run402.deploy.json or app.json here.`
601
649
  : "No deploy manifest found. Add run402.json, run402.deploy.json or app.json, or pass --manifest for executable configs.", "UP_MANIFEST_REQUIRED", {
@@ -603,7 +651,7 @@ export class NodeActions {
603
651
  candidates: MANIFEST_CANDIDATES,
604
652
  executable_candidates: EXECUTABLE_MANIFEST_CANDIDATES,
605
653
  nearby_manifests: nearby,
606
- next_actions: [
654
+ next_actions: nearby.length > 1 ? nearbyActions : [
607
655
  ...nearbyActions,
608
656
  {
609
657
  type: "create_manifest",
@@ -672,9 +720,7 @@ export class NodeActions {
672
720
  // output-dir field to narrow by), so the pre-build check is skipped
673
721
  // then and the post-build normalization performs it instead.
674
722
  const buildProducesFiles = (appSpec.build?.commands ?? []).length > 0;
675
- if (!buildProducesFiles) {
676
- await this.#assertAppReleaseFileReferences(appSpec, manifestPath, run);
677
- }
723
+ const checkedRelease = !buildProducesFiles ? await this.#assertAppReleaseFileReferences(appSpec, manifestPath, run) : undefined;
678
724
  run.setState(step, "succeeded", {
679
725
  manifest_kind: "app",
680
726
  manifest_path: manifestPath,
@@ -684,19 +730,19 @@ export class NodeActions {
684
730
  });
685
731
  return {
686
732
  manifestKind: "app",
733
+ manifestProjectId: appSpec.project.id,
687
734
  manifestPath,
688
- releaseSpec: null,
735
+ releaseSpec: checkedRelease ?? null,
689
736
  appGraph,
690
737
  appSpec,
691
738
  source,
692
739
  };
693
740
  }
694
741
  }
695
- const loaded = await loadDeployManifest(manifestPath, {
696
- ...(input.projectId
697
- ? { project: input.projectId }
698
- : { defaultProject: "prj_up_preflight_placeholder" }),
699
- });
742
+ const loaded = await this.#normalizeWithTypedFileErrors(() => loadDeployManifest(manifestPath, {
743
+ defaultProject: "prj_up_preflight_placeholder",
744
+ validateFiles: true,
745
+ }), run);
700
746
  if (!hasDeployableContent(loaded.spec)) {
701
747
  throw run.error("Deploy manifest contains no deployable sections.", "MANIFEST_EMPTY", { manifest_path: manifestPath });
702
748
  }
@@ -704,13 +750,14 @@ export class NodeActions {
704
750
  run.setState(step, "succeeded", {
705
751
  manifest_kind: "release",
706
752
  manifest_path: manifestPath,
707
- project_id: loaded.spec.project,
753
+ project_id: loaded.spec.project === "prj_up_preflight_placeholder" ? null : loaded.spec.project,
708
754
  idempotency_key: loaded.idempotencyKey ?? null,
709
755
  });
710
756
  return {
711
757
  manifestKind: "release",
712
758
  manifestPath,
713
759
  releaseSpec: loaded.spec,
760
+ loaded,
714
761
  idempotencyKey: loaded.idempotencyKey,
715
762
  manifestProjectId: loaded.spec.project === "prj_up_preflight_placeholder"
716
763
  ? undefined
@@ -787,6 +834,11 @@ export class NodeActions {
787
834
  async #assertManifestFileReferences(spec, manifestPath, run) {
788
835
  try {
789
836
  await assertLocalFileReferencesExist(spec, { manifestPath });
837
+ if (process.env.RUN402_DEPLOY_SKIP_SCAN !== "1") {
838
+ const scan = scanDeploymentSources(spec, dirname(manifestPath));
839
+ if (scan.errors.length)
840
+ throw run.error("Source scan blocked this application. Fix the reported findings, or explicitly override with RUN402_DEPLOY_SKIP_SCAN=1.", "R402_AUTH_PREFLIGHT_FAILED", scan);
841
+ }
790
842
  }
791
843
  catch (err) {
792
844
  throw this.#retypeManifestFileError(err, run);
@@ -834,6 +886,8 @@ export class NodeActions {
834
886
  try {
835
887
  normalized = await normalizeDeployManifest(releaseInput, {
836
888
  project: "prj_up_preflight_placeholder",
889
+ validateFiles: true,
890
+ manifestPath,
837
891
  baseDir: dirname(manifestPath),
838
892
  });
839
893
  }
@@ -844,6 +898,7 @@ export class NodeActions {
844
898
  return;
845
899
  }
846
900
  await this.#assertManifestFileReferences(normalized.spec, manifestPath, run);
901
+ return normalized.spec;
847
902
  }
848
903
  #planAppUpResult(input, manifest, run, opts) {
849
904
  if (!manifest.appGraph) {
@@ -987,7 +1042,7 @@ export class NodeActions {
987
1042
  })),
988
1043
  };
989
1044
  }
990
- if (run.executionMode !== "check" && run.executionMode !== "printSpec" && !run.dryRun) {
1045
+ if (run.executionMode !== "check" && run.executionMode !== "printSpec" && run.executionMode !== "printManifest" && !run.dryRun) {
991
1046
  const spendBlock = spendApprovalBlock(input, run);
992
1047
  if (spendBlock)
993
1048
  return spendBlock;
@@ -1013,7 +1068,7 @@ export class NodeActions {
1013
1068
  blockedNodeId: block.nodeId,
1014
1069
  });
1015
1070
  return run.result({
1016
- project_id: input.projectId ?? "prj_planned",
1071
+ project_id: input.projectId ?? null,
1017
1072
  manifest_path: manifest.manifestPath,
1018
1073
  app_graph: manifest.appGraph,
1019
1074
  app_result: blocked,
@@ -1041,7 +1096,7 @@ export class NodeActions {
1041
1096
  blockedNodeId: "project.ensure",
1042
1097
  });
1043
1098
  return run.result({
1044
- project_id: input.projectId ?? "prj_planned",
1099
+ project_id: input.projectId ?? null,
1045
1100
  manifest_path: manifest.manifestPath,
1046
1101
  app_graph: manifest.appGraph,
1047
1102
  app_result: blocked,
@@ -1060,7 +1115,7 @@ export class NodeActions {
1060
1115
  schema_version: "run402.workspace-project.v1",
1061
1116
  project_id: resolved.projectId,
1062
1117
  ...(input.name ? { name: input.name } : {}),
1063
- target: { kind: this.#targetKind() },
1118
+ target: { kind: this.#targetKind(), api_base: this.sdk.apiBase },
1064
1119
  created_at: resolved.link?.created_at ?? new Date().toISOString(),
1065
1120
  updated_at: new Date().toISOString(),
1066
1121
  }, resolved.link, run);
@@ -1793,7 +1848,7 @@ export class NodeActions {
1793
1848
  });
1794
1849
  run.setState(step, "running");
1795
1850
  const linkConflict = workspaceLinkConflict(link, input, this.#targetKind(), linkPath);
1796
- if (linkConflict && !input.projectId) {
1851
+ if (linkConflict) {
1797
1852
  throw run.error(linkConflict.message, "RUN402_WORKSPACE_LINK_CONFLICT", linkConflict.details);
1798
1853
  }
1799
1854
  let projectId = null;
@@ -1898,34 +1953,7 @@ export class NodeActions {
1898
1953
  shouldWriteLink: true,
1899
1954
  };
1900
1955
  }
1901
- const active = await this.sdk.projects.active();
1902
- if (active) {
1903
- const activeStep = run.addStep({
1904
- action: "project.resolve",
1905
- description: "Use active project for this workspace",
1906
- mutation: false,
1907
- auto: true,
1908
- details: { active_project_id: active },
1909
- });
1910
- await run.approve(activeStep, [], `Use active project ${active} and link this workspace to it.`);
1911
- run.setState(activeStep, run.dryRun ? "planned" : "succeeded", {
1912
- project_id: active,
1913
- source: "active",
1914
- });
1915
- run.setState(step, run.dryRun ? "planned" : "succeeded", {
1916
- project_id: active,
1917
- source: "active",
1918
- link_path: linkPath,
1919
- });
1920
- return {
1921
- projectId: active,
1922
- source: "active",
1923
- link,
1924
- linkPath,
1925
- shouldWriteLink: true,
1926
- };
1927
- }
1928
- throw run.error("No project is configured for this workspace. Pass --project, add project_id to the manifest, or pass --name to create one.", "RUN402_PROJECT_REQUIRED", { link_path: linkPath, manifest_path: manifest.manifestPath });
1956
+ throw projectSelectionRequired(workspaceDir, manifest.manifestPath);
1929
1957
  }
1930
1958
  async #resolveProjectForVerify(input, manifest, workspaceDir, run) {
1931
1959
  const linkPath = join(workspaceDir, ".run402", "project.json");
@@ -1988,7 +2016,7 @@ export class NodeActions {
1988
2016
  description: "Write workspace project link",
1989
2017
  mutation: true,
1990
2018
  auto: true,
1991
- details: { path, project_id: link.project_id, name: link.name ?? null },
2019
+ details: { path, project_id: run.dryRun && link.project_id === "prj_planned" ? null : link.project_id, name: link.name ?? null },
1992
2020
  });
1993
2021
  await run.approve(step, ["workspace.link.write"], `Write ${shortPath(path)}.`);
1994
2022
  if (run.dryRun) {
@@ -2124,7 +2152,7 @@ class ActionRun {
2124
2152
  return {
2125
2153
  action: this.input.type,
2126
2154
  mode: this.executionMode,
2127
- dry_run: this.dryRun || this.executionMode === "check" || this.executionMode === "printSpec" || this.executionMode === "plan",
2155
+ dry_run: this.dryRun || this.executionMode === "check" || (this.executionMode === "printSpec" || this.executionMode === "printManifest") || this.executionMode === "plan",
2128
2156
  target: this.target,
2129
2157
  steps: this.steps,
2130
2158
  result,
@@ -2182,20 +2210,6 @@ function appPublicOrigin(input, spec) {
2182
2210
  return null;
2183
2211
  return `https://${subdomain}.run402.com`;
2184
2212
  }
2185
- function materializeTemplates(value, values) {
2186
- if (typeof value === "string") {
2187
- return value.replace(/\$\{([^}]+)\}/g, (_match, key) => values[key] ?? "");
2188
- }
2189
- if (Array.isArray(value))
2190
- return value.map((item) => materializeTemplates(item, values));
2191
- if (value && typeof value === "object") {
2192
- return Object.fromEntries(Object.entries(value).map(([key, nested]) => [
2193
- key,
2194
- materializeTemplates(nested, values),
2195
- ]));
2196
- }
2197
- return value;
2198
- }
2199
2213
  function appMailboxSlug(logicalName, spec) {
2200
2214
  return spec.slug ?? logicalName.replace(/_/g, "-");
2201
2215
  }
@@ -2317,6 +2331,9 @@ async function classifyAppVerifyAttempt(input) {
2317
2331
  if (input.attempt.status === input.check.expect.status) {
2318
2332
  return { kind: "success" };
2319
2333
  }
2334
+ if ((input.attempt.status === 400 || input.attempt.status === 429) && input.attempt.edgeCode === null && !input.attempt.edgeHeader) {
2335
+ return { kind: "failed" };
2336
+ }
2320
2337
  const host = hostnameFromUrl(input.url);
2321
2338
  const freshness = host ? freshnessForHost(input.context, host) : { fresh: false, ageSeconds: null };
2322
2339
  if (freshness.fresh &&
@@ -2611,17 +2628,7 @@ function appBuildBlock(input, manifest) {
2611
2628
  return null;
2612
2629
  }
2613
2630
  async function findManifest(workspaceDir) {
2614
- for (const candidate of MANIFEST_CANDIDATES) {
2615
- const path = join(workspaceDir, candidate);
2616
- try {
2617
- await lstat(path);
2618
- return path;
2619
- }
2620
- catch {
2621
- // try next candidate
2622
- }
2623
- }
2624
- return null;
2631
+ return (await resolveApplicationScope({ dir: workspaceDir })).manifest_path;
2625
2632
  }
2626
2633
  const NEARBY_MANIFEST_SCAN_LIMIT = 200;
2627
2634
  /**
@@ -2732,7 +2739,9 @@ async function readWorkspaceProjectLink(path) {
2732
2739
  try {
2733
2740
  raw = await readFile(path, "utf-8");
2734
2741
  }
2735
- catch {
2742
+ catch (err) {
2743
+ if (err.code !== "ENOENT")
2744
+ throw err;
2736
2745
  return null;
2737
2746
  }
2738
2747
  let parsed;
@@ -2913,4 +2922,43 @@ function actionErrorDetails(err) {
2913
2922
  function assertNever(value) {
2914
2923
  throw new LocalError(`Unknown Run402 action ${value.type}`, "running Run402 action", { code: "RUN402_ACTION_UNKNOWN" });
2915
2924
  }
2925
+ /** Resolve only local destination intent. Approval and global active state are never selectors. */
2926
+ export async function resolveDeploymentTarget(input) {
2927
+ const linkPath = join(input.appRoot, ".run402", "project.json");
2928
+ const link = await readWorkspaceProjectLink(linkPath);
2929
+ const conflict = workspaceLinkConflict(link, input, input.targetKind ?? "unknown", linkPath);
2930
+ if (conflict)
2931
+ throw new LocalError(conflict.message, "resolving deployment target", { code: "RUN402_WORKSPACE_LINK_CONFLICT", details: conflict.details });
2932
+ if (link?.target?.api_base && input.apiBase && link.target.api_base.replace(/\/$/, "") !== input.apiBase.replace(/\/$/, "")) {
2933
+ throw new LocalError("Workspace link belongs to a different API target.", "resolving deployment target", { code: "RUN402_WORKSPACE_LINK_CONFLICT", details: { reason: "api_base_mismatch", link_path: linkPath } });
2934
+ }
2935
+ const selectors = [input.projectId, input.environmentProjectId, link?.project_id, input.manifestProjectId].filter(Boolean);
2936
+ if (new Set(selectors).size > 1) {
2937
+ throw new LocalError("Deployment project selectors disagree.", "resolving deployment target", { code: "RUN402_PROJECT_CONFLICT", details: { explicit_project_id: input.projectId ?? null, linked_project_id: link?.project_id ?? null, manifest_project_id: input.manifestProjectId ?? null } });
2938
+ }
2939
+ const projectId = input.projectId ?? input.environmentProjectId ?? link?.project_id ?? input.manifestProjectId ?? null;
2940
+ if (projectId && input.name && (!link || link.name !== input.name)) {
2941
+ throw new LocalError("--name is creation intent and cannot select or rename an existing project. Pass --project alone.", "resolving deployment target", { code: "RUN402_PROJECT_CONFLICT", details: { project_id: projectId, name: input.name } });
2942
+ }
2943
+ if (!projectId && !input.name && !input.allowUnresolved)
2944
+ throw projectSelectionRequired(input.appRoot, input.manifestPath);
2945
+ return { project_id: projectId, project_name: input.name ?? link?.name ?? null,
2946
+ source: input.projectId || input.environmentProjectId ? "explicit" : link ? "workspace_link" : input.manifestProjectId ? "manifest" : input.name ? "create" : "unresolved" };
2947
+ }
2948
+ function projectSelectionRequired(appRoot, manifestPath) {
2949
+ return new LocalError("No deployment destination is selected. Pass --project for an existing project or --name to create one; -y never selects the global active project.", "resolving deployment target", {
2950
+ code: "UP_PROJECT_REQUIRED", details: { app_root: appRoot, manifest_path: manifestPath ?? null, mutation_state: "not_started" },
2951
+ next_actions: [{ type: "select_project", safe_to_auto_execute: false, why: "Select the intended project explicitly, or name a new project, then retry in this application directory.", app_root: appRoot }],
2952
+ });
2953
+ }
2954
+ async function nearbyBinding(manifestPath) {
2955
+ try {
2956
+ const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
2957
+ const link = await readWorkspaceProjectLink(join(dirname(manifestPath), ".run402", "project.json"));
2958
+ return { app_name: manifest.app?.name ?? manifest.app?.id ?? null, manifest_project_id: typeof manifest.project === "string" ? manifest.project : manifest.project_id ?? manifest.project?.id ?? null, linked_project_id: link?.project_id ?? null, target: link?.target ?? null };
2959
+ }
2960
+ catch {
2961
+ return { status: "unknown", reason: "Local binding could not be read; select the application and run its check." };
2962
+ }
2963
+ }
2916
2964
  //# sourceMappingURL=actions-node.js.map