run402 4.80.0 → 4.80.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.
Files changed (59) hide show
  1. package/README.md +22 -4
  2. package/gitvault-surface.json +1 -1
  3. package/lib/command-manifest.mjs +4 -1
  4. package/lib/deploy-v2.mjs +18 -2
  5. package/lib/doctor.mjs +164 -30
  6. package/lib/functions.mjs +153 -42
  7. package/lib/gitvault-scaffold.mjs +19 -7
  8. package/lib/harness-context.mjs +6 -2
  9. package/lib/image.mjs +3 -1
  10. package/lib/init.mjs +5 -3
  11. package/lib/logs.mjs +125 -96
  12. package/lib/repos.mjs +40 -8
  13. package/lib/up.mjs +67 -21
  14. package/package.json +1 -1
  15. package/sdk/dist/actions.d.ts +27 -9
  16. package/sdk/dist/actions.d.ts.map +1 -1
  17. package/sdk/dist/errors.d.ts +1 -1
  18. package/sdk/dist/errors.d.ts.map +1 -1
  19. package/sdk/dist/errors.js.map +1 -1
  20. package/sdk/dist/index.d.ts +1 -1
  21. package/sdk/dist/index.d.ts.map +1 -1
  22. package/sdk/dist/index.js +1 -1
  23. package/sdk/dist/index.js.map +1 -1
  24. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  25. package/sdk/dist/namespaces/deploy.js +35 -3
  26. package/sdk/dist/namespaces/deploy.js.map +1 -1
  27. package/sdk/dist/namespaces/deploy.types.d.ts +15 -8
  28. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  29. package/sdk/dist/namespaces/deploy.types.js +1 -1
  30. package/sdk/dist/namespaces/deploy.types.js.map +1 -1
  31. package/sdk/dist/namespaces/functions.d.ts +17 -1
  32. package/sdk/dist/namespaces/functions.d.ts.map +1 -1
  33. package/sdk/dist/namespaces/functions.js +131 -2
  34. package/sdk/dist/namespaces/functions.js.map +1 -1
  35. package/sdk/dist/namespaces/functions.types.d.ts +62 -0
  36. package/sdk/dist/namespaces/functions.types.d.ts.map +1 -1
  37. package/sdk/dist/namespaces/gitvault.d.ts +38 -0
  38. package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
  39. package/sdk/dist/namespaces/gitvault.js +122 -14
  40. package/sdk/dist/namespaces/gitvault.js.map +1 -1
  41. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  42. package/sdk/dist/node/actions-node.js +176 -16
  43. package/sdk/dist/node/actions-node.js.map +1 -1
  44. package/sdk/dist/node/client-detect.d.ts +16 -1
  45. package/sdk/dist/node/client-detect.d.ts.map +1 -1
  46. package/sdk/dist/node/client-detect.js +26 -9
  47. package/sdk/dist/node/client-detect.js.map +1 -1
  48. package/sdk/dist/node/deploy-manifest.d.ts +42 -0
  49. package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
  50. package/sdk/dist/node/deploy-manifest.js +175 -1
  51. package/sdk/dist/node/deploy-manifest.js.map +1 -1
  52. package/sdk/dist/node/index.d.ts +2 -2
  53. package/sdk/dist/node/index.d.ts.map +1 -1
  54. package/sdk/dist/node/index.js +1 -1
  55. package/sdk/dist/node/index.js.map +1 -1
  56. package/sdk/dist/scoped.d.ts +2 -1
  57. package/sdk/dist/scoped.d.ts.map +1 -1
  58. package/sdk/dist/scoped.js +3 -0
  59. package/sdk/dist/scoped.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"actions-node.d.ts","sourceRoot":"","sources":["../../src/node/actions-node.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAIV,kBAAkB,EAClB,sBAAsB,EACtB,aAAa,EAGb,kCAAkC,EAClC,wBAAwB,EACxB,mBAAmB,EACnB,cAAc,EAGf,MAAM,eAAe,CAAC;AAoBvB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAU1C,OAAO,KAAK,EAAkB,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,KAAK,EAAY,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAIrE,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAEhE,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAmKD;;;GAGG;AAEH,qBAAa,WAAY,YAAW,aAAa;;IAE7C,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBADJ,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,kBAAuB;IAG1C,GAAG,CACP,KAAK,EAAE,kCAAkC,EACzC,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAC;IACzC,GAAG,CACP,KAAK,EAAE,wBAAwB,EAC/B,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;IACvC,GAAG,CACP,KAAK,EAAE,mBAAmB,EAC1B,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC;IAmCxC,EAAE,CACN,KAAK,GAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,CAAM,EAC7C,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC;CA2hE/C"}
1
+ {"version":3,"file":"actions-node.d.ts","sourceRoot":"","sources":["../../src/node/actions-node.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAIV,kBAAkB,EAClB,sBAAsB,EACtB,aAAa,EAGb,kCAAkC,EAClC,wBAAwB,EACxB,mBAAmB,EACnB,cAAc,EAGf,MAAM,eAAe,CAAC;AAoBvB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAU1C,OAAO,KAAK,EAAkB,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,KAAK,EAAY,aAAa,EAAE,MAAM,uBAAuB,CAAC;AASrE,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAEhE,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAmKD;;;GAGG;AAEH,qBAAa,WAAY,YAAW,aAAa;;IAE7C,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBADJ,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,kBAAuB;IAG1C,GAAG,CACP,KAAK,EAAE,kCAAkC,EACzC,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAC;IACzC,GAAG,CACP,KAAK,EAAE,wBAAwB,EAC/B,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;IACvC,GAAG,CACP,KAAK,EAAE,mBAAmB,EAC1B,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC;IAmCxC,EAAE,CACN,KAAK,GAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,CAAM,EAC7C,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC;CAgqE/C"}
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { execFile } from "node:child_process";
3
- import { lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile, } from "node:fs/promises";
3
+ import { lstat, mkdir, mkdtemp, readdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { basename, dirname, isAbsolute, join, relative, resolve as resolvePath } from "node:path";
6
6
  import { setTimeout as delay } from "node:timers/promises";
@@ -8,7 +8,7 @@ import { promisify } from "node:util";
8
8
  import { Run402Action } from "../actions.js";
9
9
  import { RUN402_APP_SCHEMA_ID, RUN402_APP_MANIFEST_FILENAME, compileRun402AppInstallGraph, createRun402AppUpResult, } from "../app-up.js";
10
10
  import { LocalError, Run402DeployError } from "../errors.js";
11
- import { loadDeployManifest, normalizeDeployManifest } from "./deploy-manifest.js";
11
+ import { assertLocalFileReferencesExist, loadDeployManifest, manifestNotFoundError, normalizeDeployManifest, } from "./deploy-manifest.js";
12
12
  import { declaredAgentName, detectClientName } from "./client-detect.js";
13
13
  function deployEventDetails(event) {
14
14
  const raw = event;
@@ -400,9 +400,13 @@ export class NodeActions {
400
400
  * principal-display-name (first-deploy-agent-dx): make sure the deploying
401
401
  * principal has a display name before the deploy that will be credited to
402
402
  * it, then join the project's room under that name. Explicit
403
- * `identityName` wins; otherwise the detected client name is set and
404
- * reported as `detected`. Best-effort end to end: a whoami, PATCH, or
405
- * room hiccup never fails the deploy it reports `source: "unavailable"`.
403
+ * `identityName` / `RUN402_AGENT_NAME` wins and may rename an already-named
404
+ * principal; otherwise a detected client name is set only when no name
405
+ * exists yet, and reported as `detected`. The detected client is always
406
+ * reported (`detected` + `detection`), even when it was not applied, so an
407
+ * agent whose principal is already named sees why. Best-effort end to
408
+ * end: a whoami, PATCH, or room hiccup never fails the deploy — it reports
409
+ * `source: "unavailable"`.
406
410
  */
407
411
  async #ensureIdentity(input, projectId, run) {
408
412
  if (this.#targetKind() === "core") {
@@ -422,9 +426,20 @@ export class NodeActions {
422
426
  let source = "existing";
423
427
  // Order: explicit input, then the runtime's own declaration
424
428
  // (RUN402_AGENT_NAME), then a specifically detected client. A generic
425
- // guess is never persisted as a name.
429
+ // guess is never persisted as a name. Detection always runs so the
430
+ // result can say what was seen even when it was not applied.
426
431
  const explicitName = input.identityName?.trim() || declaredAgentName() || null;
427
- const detectedName = explicitName ? null : detectClientName();
432
+ const detectedName = detectClientName();
433
+ const detectionReport = (applied) => {
434
+ const reason = applied
435
+ ? "applied"
436
+ : !detectedName
437
+ ? "nothing_detected"
438
+ : explicitName
439
+ ? "explicit_name_wins"
440
+ : "name_already_set";
441
+ return { detected: detectedName, detection: { applied, reason } };
442
+ };
428
443
  if (!displayName || (explicitName && explicitName !== displayName)) {
429
444
  const desired = explicitName ?? detectedName;
430
445
  if (!desired) {
@@ -439,12 +454,12 @@ export class NodeActions {
439
454
  }
440
455
  run.skipStep({
441
456
  action: "identity.name.set",
442
- description: "No display name is known for this principal; nothing was written (set RUN402_AGENT_NAME or run `run402 org whoami --set-name <name>`)",
457
+ description: "No display name is known for this principal; nothing was written (set RUN402_AGENT_NAME=<name>, RUN402_CLIENT=<client>, or run `run402 org whoami --set-name <name>`)",
443
458
  mutation: false,
444
459
  auto: true,
445
- details: { source: "undetected" },
460
+ details: { source: "undetected", detected: null },
446
461
  });
447
- return { display_name: null, source: "undetected", presence };
462
+ return { display_name: null, source: "undetected", ...detectionReport(false), presence };
448
463
  }
449
464
  source = explicitName ? "explicit" : "detected";
450
465
  const step = run.addStep({
@@ -452,7 +467,7 @@ export class NodeActions {
452
467
  description: `Set this principal's display name to ${desired}`,
453
468
  mutation: true,
454
469
  auto: true,
455
- details: { display_name: desired, source },
470
+ details: { display_name: desired, source, detected: detectedName },
456
471
  });
457
472
  await run.approve(step, ["identity.name.set"], `Name this principal "${desired}" (promotion credit and room presence use it; change any time with run402 org whoami --set-name).`);
458
473
  run.setState(step, "running");
@@ -463,9 +478,20 @@ export class NodeActions {
463
478
  }
464
479
  catch (err) {
465
480
  run.setState(step, "failed", { error: err instanceof Error ? err.message : String(err) });
466
- return { display_name: current, source: "unavailable" };
481
+ return { display_name: current, source: "unavailable", ...detectionReport(false) };
467
482
  }
468
483
  }
484
+ else if (detectedName && detectedName !== displayName) {
485
+ // A client was detected but the principal already carries a name: say
486
+ // so, write nothing (an existing name is never overwritten by a guess).
487
+ run.skipStep({
488
+ action: "identity.name.set",
489
+ description: `Detected client "${detectedName}" but the principal is already named "${displayName}"; nothing written — set RUN402_AGENT_NAME=<name> or run \`run402 org whoami --set-name <name>\` to rename`,
490
+ mutation: false,
491
+ auto: true,
492
+ details: { source: "existing", display_name: displayName, detected: detectedName, reason: "name_already_set" },
493
+ });
494
+ }
469
495
  let presence = null;
470
496
  try {
471
497
  const room = await this.sdk.rooms.forProject(projectId);
@@ -478,7 +504,7 @@ export class NodeActions {
478
504
  catch {
479
505
  presence = null;
480
506
  }
481
- return { display_name: displayName, source, presence };
507
+ return { display_name: displayName, source, ...detectionReport(source === "detected"), presence };
482
508
  }
483
509
  async #verifyDeployManifestOnly(input, manifest, workspaceDir, run) {
484
510
  const checks = manifest.verify?.http ?? [];
@@ -557,11 +583,28 @@ export class NodeActions {
557
583
  },
558
584
  },
559
585
  };
560
- throw run.error("No deploy manifest found. Add run402.deploy.json or app.json, or pass --manifest for executable configs.", "UP_MANIFEST_REQUIRED", {
586
+ // `up` never walks into subdirectories, but an agent standing in a
587
+ // parent workspace (a monorepo root, a checkout that holds the app one
588
+ // level down) usually is one `--dir` away from the manifest it meant.
589
+ // Name what is one directory down so the hop is executable.
590
+ const nearby = await findNearbyManifests(workspaceDir);
591
+ const nearbyActions = nearby.map((entry) => ({
592
+ type: "run_in_directory",
593
+ command: `run402 up --check --dir ${shellArg(entry.relative_dir)}`,
594
+ argv: ["run402", "up", "--check", "--dir", entry.relative_dir],
595
+ path: entry.path,
596
+ dir: entry.relative_dir,
597
+ why: "A manifest exists one directory down; up does not walk into subdirectories.",
598
+ }));
599
+ throw run.error(nearby.length > 0
600
+ ? `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
+ : "No deploy manifest found. Add run402.json, run402.deploy.json or app.json, or pass --manifest for executable configs.", "UP_MANIFEST_REQUIRED", {
561
602
  dir: workspaceDir,
562
603
  candidates: MANIFEST_CANDIDATES,
563
604
  executable_candidates: EXECUTABLE_MANIFEST_CANDIDATES,
605
+ nearby_manifests: nearby,
564
606
  next_actions: [
607
+ ...nearbyActions,
565
608
  {
566
609
  type: "create_manifest",
567
610
  why: "Write a deploy manifest, then re-run. This minimal site manifest deploys as-is; add database/functions/routes slices later.",
@@ -621,11 +664,23 @@ export class NodeActions {
621
664
  }
622
665
  throw err;
623
666
  }
667
+ // The release slice is normally normalized only after the build
668
+ // ran. With no build commands declared nothing on disk changes
669
+ // between now and then, so the slice's filesystem references can be
670
+ // checked right here — in `--check` as much as before a real apply.
671
+ // A declared build may produce the referenced files (there is no
672
+ // output-dir field to narrow by), so the pre-build check is skipped
673
+ // then and the post-build normalization performs it instead.
674
+ const buildProducesFiles = (appSpec.build?.commands ?? []).length > 0;
675
+ if (!buildProducesFiles) {
676
+ await this.#assertAppReleaseFileReferences(appSpec, manifestPath, run);
677
+ }
624
678
  run.setState(step, "succeeded", {
625
679
  manifest_kind: "app",
626
680
  manifest_path: manifestPath,
627
681
  app_id: appSpec.app.id,
628
682
  graph_digest: appGraph.graph_digest,
683
+ file_references: buildProducesFiles ? "deferred_to_post_build" : "checked",
629
684
  });
630
685
  return {
631
686
  manifestKind: "app",
@@ -645,6 +700,7 @@ export class NodeActions {
645
700
  if (!hasDeployableContent(loaded.spec)) {
646
701
  throw run.error("Deploy manifest contains no deployable sections.", "MANIFEST_EMPTY", { manifest_path: manifestPath });
647
702
  }
703
+ await this.#assertManifestFileReferences(loaded.spec, manifestPath, run);
648
704
  run.setState(step, "succeeded", {
649
705
  manifest_kind: "release",
650
706
  manifest_path: manifestPath,
@@ -722,6 +778,73 @@ export class NodeActions {
722
778
  },
723
779
  };
724
780
  }
781
+ /**
782
+ * `stat` every local filesystem reference of a normalized spec and fail
783
+ * with the action-shaped `MANIFEST_FILE_MISSING` envelope (code, message
784
+ * naming the first missing file, `details.missing[]`, one `create_file`
785
+ * next action per file) before any plan or mutation.
786
+ */
787
+ async #assertManifestFileReferences(spec, manifestPath, run) {
788
+ try {
789
+ await assertLocalFileReferencesExist(spec, { manifestPath });
790
+ }
791
+ catch (err) {
792
+ throw this.#retypeManifestFileError(err, run);
793
+ }
794
+ }
795
+ /**
796
+ * Normalize an app release slice and re-shape a `MANIFEST_FILE_MISSING`
797
+ * (a missing migration `sql_path`) into the action envelope; every other
798
+ * normalization error passes through untouched.
799
+ */
800
+ async #normalizeWithTypedFileErrors(fn, run) {
801
+ try {
802
+ return await fn();
803
+ }
804
+ catch (err) {
805
+ if (err instanceof LocalError && err.code === "MANIFEST_FILE_MISSING") {
806
+ throw this.#retypeManifestFileError(err, run);
807
+ }
808
+ throw err;
809
+ }
810
+ }
811
+ #retypeManifestFileError(err, run) {
812
+ if (!(err instanceof LocalError) || err.code !== "MANIFEST_FILE_MISSING")
813
+ return err;
814
+ const details = err.details && typeof err.details === "object" && !Array.isArray(err.details)
815
+ ? err.details
816
+ : {};
817
+ return run.error(err.message, "MANIFEST_FILE_MISSING", {
818
+ ...details,
819
+ next_actions: err.nextActions ?? [],
820
+ });
821
+ }
822
+ /**
823
+ * Pre-build filesystem check for an app manifest's release slice. Templates
824
+ * are materialized against an empty environment (only path-bearing fields
825
+ * matter here) and the slice is normalized exactly as the post-build path
826
+ * would; a `MANIFEST_FILE_MISSING` surfaces, any other normalization error
827
+ * is left for the real apply path to report with its full context.
828
+ */
829
+ async #assertAppReleaseFileReferences(appSpec, manifestPath, run) {
830
+ if (!appSpec.release || typeof appSpec.release !== "object")
831
+ return;
832
+ const releaseInput = materializeTemplates(appSpec.release, {});
833
+ let normalized;
834
+ try {
835
+ normalized = await normalizeDeployManifest(releaseInput, {
836
+ project: "prj_up_preflight_placeholder",
837
+ baseDir: dirname(manifestPath),
838
+ });
839
+ }
840
+ catch (err) {
841
+ if (err instanceof LocalError && err.code === "MANIFEST_FILE_MISSING") {
842
+ throw this.#retypeManifestFileError(err, run);
843
+ }
844
+ return;
845
+ }
846
+ await this.#assertManifestFileReferences(normalized.spec, manifestPath, run);
847
+ }
725
848
  #planAppUpResult(input, manifest, run, opts) {
726
849
  if (!manifest.appGraph) {
727
850
  throw run.error("Internal error: app manifest did not produce an install graph.", "RUN402_ACTION_INTERNAL");
@@ -957,13 +1080,14 @@ export class NodeActions {
957
1080
  ...env,
958
1081
  "input.name": input.name ?? manifest.appSpec.project.name ?? "",
959
1082
  });
960
- const normalized = await normalizeDeployManifest(releaseInput, {
1083
+ const normalized = await this.#normalizeWithTypedFileErrors(() => normalizeDeployManifest(releaseInput, {
961
1084
  project: resolved.projectId,
962
1085
  baseDir: dirname(manifest.manifestPath),
963
- });
1086
+ }), run);
964
1087
  if (!hasDeployableContent(normalized.spec)) {
965
1088
  throw run.error("App release contains no deployable sections.", "MANIFEST_EMPTY", { manifest_path: manifest.manifestPath });
966
1089
  }
1090
+ await this.#assertManifestFileReferences(normalized.spec, manifest.manifestPath, run);
967
1091
  const deployStep = run.addStep({
968
1092
  action: "deploy.apply",
969
1093
  description: "Apply app release",
@@ -2007,9 +2131,14 @@ class ActionRun {
2007
2131
  };
2008
2132
  }
2009
2133
  error(message, code, details) {
2134
+ // `details.next_actions` stays where consumers already read it; hoist a
2135
+ // copy to the envelope's top-level `next_actions` so `err.nextActions` /
2136
+ // the CLI's `next_actions` field carry it too.
2137
+ const nextActions = Array.isArray(details?.next_actions) ? details.next_actions : undefined;
2010
2138
  return withActionDetails(new LocalError(message, "running Run402 action", {
2011
2139
  code,
2012
2140
  details: { action: this.input.type, ...(details ?? {}), steps: this.steps },
2141
+ ...(nextActions ? { next_actions: nextActions } : {}),
2013
2142
  }), this);
2014
2143
  }
2015
2144
  emit(step) {
@@ -2494,6 +2623,34 @@ async function findManifest(workspaceDir) {
2494
2623
  }
2495
2624
  return null;
2496
2625
  }
2626
+ const NEARBY_MANIFEST_SCAN_LIMIT = 200;
2627
+ /**
2628
+ * Manifests one directory down from `workspaceDir` (immediate children
2629
+ * only; `node_modules`, `.git` and every other dot-directory skipped; the
2630
+ * scan reads at most {@link NEARBY_MANIFEST_SCAN_LIMIT} entries). Surfaced in
2631
+ * `UP_MANIFEST_REQUIRED` so an agent standing in a parent directory learns
2632
+ * where `up` would have found a manifest instead of a bare ENOENT.
2633
+ */
2634
+ async function findNearbyManifests(workspaceDir) {
2635
+ let entries;
2636
+ try {
2637
+ entries = await readdir(workspaceDir, { withFileTypes: true });
2638
+ }
2639
+ catch {
2640
+ return [];
2641
+ }
2642
+ const found = [];
2643
+ for (const entry of entries.slice(0, NEARBY_MANIFEST_SCAN_LIMIT)) {
2644
+ if (!entry.isDirectory())
2645
+ continue;
2646
+ if (entry.name === "node_modules" || entry.name.startsWith("."))
2647
+ continue;
2648
+ const manifestPath = await findManifest(join(workspaceDir, entry.name));
2649
+ if (manifestPath)
2650
+ found.push({ path: manifestPath, relative_dir: entry.name });
2651
+ }
2652
+ return found.sort((a, b) => a.relative_dir.localeCompare(b.relative_dir));
2653
+ }
2497
2654
  async function findExecutableManifest(workspaceDir) {
2498
2655
  for (const candidate of EXECUTABLE_MANIFEST_CANDIDATES) {
2499
2656
  const path = join(workspaceDir, candidate);
@@ -2513,6 +2670,9 @@ async function loadRun402JsonManifest(path) {
2513
2670
  raw = await readFile(path, "utf-8");
2514
2671
  }
2515
2672
  catch (err) {
2673
+ if (err?.code === "ENOENT") {
2674
+ throw manifestNotFoundError(path, "loading Run402 manifest");
2675
+ }
2516
2676
  throw new LocalError(`Failed to read Run402 manifest '${path}': ${err.message}`, "loading Run402 manifest", err);
2517
2677
  }
2518
2678
  try {