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
package/lib/logs.mjs CHANGED
@@ -1,163 +1,192 @@
1
1
  /**
2
2
  * run402 logs — Top-level shortcut for fetching function logs by request id.
3
3
  *
4
- * Capability `astro-ssr-runtime` (Run402 v1.52). When an SSR response
5
- * returns 5xx, the response includes `x-run402-request-id: req_...` and
6
- * `x-run402-error-code: R402_SSR_RUNTIME_ERROR`. The agent (or user)
7
- * copies the request id and runs:
4
+ * Capability `astro-ssr-runtime` (Run402 v1.52). Every function response
5
+ * (routed and direct) carries `x-run402-request-id: req_...`; a 5xx from the
6
+ * SSR runtime adds `x-run402-error-code: R402_SSR_RUNTIME_ERROR`. The agent
7
+ * (or user) copies the request id and runs:
8
8
  *
9
9
  * run402 logs --request-id req_...
10
10
  *
11
- * Resolves project + function from env (RUN402_PROJECT_ID + --function)
12
- * OR explicit flags. For multi-function projects, you can omit --function
13
- * and the command scans every function in the project.
11
+ * No function name needed: the SDK's `functions.logsByRequestId` fans the
12
+ * read out across every function in the project (the gateway has no
13
+ * project-wide logs route). `--function <name>` (or a leading positional)
14
+ * narrows it to one; with `--function` alone the command is that function's
15
+ * plain tail.
14
16
  *
15
- * Delegates to `run402 functions logs` for single-function lookups.
17
+ * App output only by default Lambda's own INIT_START / START / END / REPORT
18
+ * lines are `origin: "platform"` and hidden unless `--platform` / `--all`.
16
19
  */
17
20
 
18
21
  import { getSdk } from "./sdk.mjs";
22
+ import { getActiveProjectId } from "./config.mjs";
19
23
  import { reportSdkError, fail } from "./sdk-errors.mjs";
24
+ import { assertKnownFlags, flagValue, normalizeArgv, parseIntegerFlag, positionalArgs } from "./argparse.mjs";
25
+ import {
26
+ FUNCTION_LOG_TAIL_MAX,
27
+ assertLogRequestIdFlag,
28
+ parseLogSinceFlag,
29
+ platformHiddenHint,
30
+ resolveLogOriginFlag,
31
+ } from "./functions.mjs";
20
32
 
21
33
  const HELP = `run402 logs — Fetch function logs by request id
22
34
 
23
35
  Usage:
24
- run402 logs --request-id <req_id> [--function <name>] [--project <id>] [--tail <n>]
36
+ run402 logs --request-id <id> [<function> | --function <name>] [--project <id>] [options]
37
+ run402 logs --function <name> [--project <id>] [options]
25
38
 
26
- Required:
27
- --request-id <req_id> The req_... id (from x-run402-request-id header)
39
+ Required (one of):
40
+ --request-id <id> A req_... id (the x-run402-request-id response header),
41
+ or a fnrun_... / fnatt_... durable-run id. Without
42
+ --function, every function in the project is scanned.
43
+ --function <name> Limit to one function. Alone (no --request-id) this
44
+ is that function's recent tail. A leading positional
45
+ <function> means the same thing.
28
46
 
29
47
  Optional:
30
- --function <name> Limit to one function (default: scan all functions in the project)
31
- --project <id> Project id (default: \$RUN402_PROJECT_ID)
32
- --tail <n> Max entries per function (default 100)
48
+ --project <id> Project id (default: \$RUN402_PROJECT_ID, else the active project)
49
+ --tail <n> Max entries per function BEFORE the origin filter (default 100, max 1000)
50
+ --since <ts> ISO timestamp or epoch ms; only entries at or after this
51
+ --app Only the function's own output (default). Lambda runtime
52
+ lines (INIT_START, START/END/REPORT RequestId, billed
53
+ duration) are hidden; "hidden.platform" counts them and a
54
+ "hint" appears when hiding them left the result empty.
55
+ --platform Only the Lambda runtime lines
56
+ --all Both (the raw CloudWatch stream)
33
57
 
34
58
  Output:
35
- Stdout is JSON { ok, request_id, project_id, scanned, entries, errors? }.
59
+ Stdout is JSON { ok, request_id, project_id, scanned, entries, errors?, origin, hidden?, hint? }.
60
+ Every entry carries "function" and "origin": "app" | "platform". "ok" is
61
+ false only when a function's log read failed (it is named in "errors").
36
62
 
37
63
  Examples:
38
64
  run402 logs --request-id req_abc123
39
65
  run402 logs --request-id req_abc123 --function ssr
40
- run402 logs --request-id req_abc123 --project prj_xyz
66
+ run402 logs ssr --request-id req_abc123 --all
67
+ run402 logs --request-id fnrun_abc123 --project prj_xyz
68
+ run402 logs --function checkout --tail 20
41
69
 
42
70
  Tip: the request id appears in:
43
- - The 'x-run402-request-id' response header on every SSR response
71
+ - The 'x-run402-request-id' response header on every function response
44
72
  - The 'requestId' field of any R402_SSR_RUNTIME_ERROR envelope
45
73
  - The 'request_id' field in deploy / cache invalidate result envelopes
74
+ - The 'samples' of 'run402 errors' (each sample names a runnable logs command)
46
75
  `;
47
76
 
77
+ const VALUE_FLAGS = ["--request-id", "--function", "--project", "--tail", "--since"];
78
+ const KNOWN_FLAGS = [...VALUE_FLAGS, "--app", "--platform", "--all", "--help", "-h"];
79
+
48
80
  export async function run(sub, args = []) {
49
- const all = [sub, ...args].filter(Boolean);
81
+ const all = normalizeArgv([sub, ...args].filter(Boolean));
50
82
  if (!all.length || all.includes("--help") || all.includes("-h")) {
51
83
  console.log(HELP);
52
84
  return;
53
85
  }
86
+ assertKnownFlags(all, KNOWN_FLAGS, VALUE_FLAGS);
54
87
 
55
- const requestId = pickFlagValue(all, "--request-id");
56
- const fnName = pickFlagValue(all, "--function");
57
- const projectIdArg = pickFlagValue(all, "--project");
58
- const tailArg = pickFlagValue(all, "--tail");
88
+ const requestId = flagValue(all, "--request-id") ?? undefined;
89
+ const functionFlag = flagValue(all, "--function") ?? undefined;
90
+ const projectIdArg = flagValue(all, "--project") ?? undefined;
91
+ const tailArg = flagValue(all, "--tail") ?? undefined;
92
+ const sinceArg = flagValue(all, "--since") ?? undefined;
93
+ const origin = resolveLogOriginFlag(all);
59
94
 
60
- if (!requestId) {
95
+ const positionals = positionalArgs(all, VALUE_FLAGS);
96
+ if (positionals.length > 1) {
61
97
  fail({
62
98
  code: "BAD_USAGE",
63
- message: "Missing --request-id <req_id>.",
64
- hint: "Pass the request id from the 'x-run402-request-id' response header.",
99
+ message: `Unexpected argument: ${positionals[1]}`,
100
+ hint: "run402 logs [<function>] --request-id <id> [--project <id>]",
101
+ details: { argument: positionals[1] },
65
102
  });
66
103
  }
67
- if (!requestId.startsWith("req_")) {
104
+ if (positionals.length === 1 && functionFlag !== undefined && positionals[0] !== functionFlag) {
68
105
  fail({
69
106
  code: "BAD_USAGE",
70
- message: `--request-id must look like 'req_...' (got: ${requestId})`,
107
+ message: `Function named twice: positional '${positionals[0]}' and --function '${functionFlag}'`,
108
+ details: { positional: positionals[0], flag: functionFlag },
71
109
  });
72
110
  }
111
+ const fnName = functionFlag ?? positionals[0];
73
112
 
74
- const projectId = projectIdArg ?? process.env.RUN402_PROJECT_ID;
75
- if (!projectId) {
113
+ if (!requestId && !fnName) {
76
114
  fail({
77
115
  code: "BAD_USAGE",
78
- message: "Missing project id.",
79
- hint: "Pass --project <id> or set RUN402_PROJECT_ID env var.",
116
+ message: "Missing --request-id <id> (or --function <name> for a plain tail).",
117
+ hint: "Pass the request id from the 'x-run402-request-id' response header.",
80
118
  });
81
119
  }
120
+ assertLogRequestIdFlag(requestId);
82
121
 
83
- const tail = tailArg ? parseInt(tailArg, 10) : 100;
84
- if (Number.isNaN(tail) || tail < 1 || tail > 5000) {
122
+ const projectId = projectIdArg ?? process.env.RUN402_PROJECT_ID ?? getActiveProjectId() ?? undefined;
123
+ if (!projectId) {
85
124
  fail({
86
125
  code: "BAD_USAGE",
87
- message: `--tail must be an integer between 1 and 5000 (got: ${tailArg})`,
126
+ message: "Missing project id.",
127
+ hint: "Pass --project <id>, set RUN402_PROJECT_ID, or 'run402 projects use <id>'.",
88
128
  });
89
129
  }
90
130
 
91
- const sdk = getSdk();
131
+ const tail = parseIntegerFlag("--tail", tailArg, { min: 1, max: FUNCTION_LOG_TAIL_MAX, def: 100 });
132
+ const since = parseLogSinceFlag(sinceArg);
92
133
 
134
+ const sdk = getSdk();
93
135
  try {
94
- let fnNames;
95
- if (fnName) {
96
- fnNames = [fnName];
97
- } else {
98
- // Scan every function in the project.
99
- const list = await sdk.functions.list(projectId);
100
- fnNames = (list?.functions ?? []).map((f) => f.name);
101
- if (fnNames.length === 0) {
102
- console.log(JSON.stringify({ ok: true, request_id: requestId, project_id: projectId, entries: [], scanned: [] }, null, 2));
103
- return;
104
- }
136
+ if (requestId) {
137
+ const result = await sdk.functions.logsByRequestId(projectId, requestId, {
138
+ tail,
139
+ since,
140
+ origin,
141
+ ...(fnName !== undefined && { functionName: fnName }),
142
+ });
143
+ emit({
144
+ ok: result.errors.length === 0,
145
+ request_id: result.request_id,
146
+ project_id: projectId,
147
+ scanned: result.scanned,
148
+ entries: result.entries,
149
+ errors: result.errors,
150
+ origin: result.origin,
151
+ hidden: result.hidden,
152
+ });
153
+ return;
105
154
  }
106
155
 
107
- // Query each function in parallel; aggregate entries. SDK returns
108
- // FunctionLogsResult = { logs: FunctionLogEntry[] }; unwrap to the array
109
- // so the aggregated JSON has a flat entries[] field.
110
- const results = await Promise.allSettled(
111
- fnNames.map((name) =>
112
- sdk.functions
113
- .logs(projectId, name, { requestId, tail })
114
- .then((result) => ({ name, entries: result?.logs ?? [] })),
115
- ),
116
- );
117
-
118
- const allEntries = [];
119
- const scanned = [];
120
- const errors = [];
121
- for (const r of results) {
122
- if (r.status === "fulfilled") {
123
- scanned.push(r.value.name);
124
- for (const e of r.value.entries) {
125
- allEntries.push({ function: r.value.name, ...e });
126
- }
127
- } else {
128
- errors.push({ name: "?", error: r.reason?.message ?? String(r.reason) });
129
- }
130
- }
131
-
132
- // Sort by timestamp ascending. FunctionLogEntry.timestamp is an ISO 8601
133
- // string; convert to epoch ms for comparison.
134
- allEntries.sort((a, b) => {
135
- const ta = a.timestamp ? Date.parse(a.timestamp) : 0;
136
- const tb = b.timestamp ? Date.parse(b.timestamp) : 0;
137
- return ta - tb;
156
+ // --function alone: that function's recent tail, same envelope.
157
+ const result = await sdk.functions.logs(projectId, fnName, { tail, since, origin });
158
+ emit({
159
+ ok: true,
160
+ request_id: null,
161
+ project_id: projectId,
162
+ scanned: [fnName],
163
+ entries: (result.logs ?? []).map((entry) => ({ function: fnName, ...entry })),
164
+ errors: [],
165
+ origin: result.origin ?? origin,
166
+ hidden: result.hidden,
138
167
  });
139
-
140
- console.log(
141
- JSON.stringify(
142
- {
143
- ok: errors.length === 0,
144
- request_id: requestId,
145
- project_id: projectId,
146
- scanned,
147
- entries: allEntries,
148
- ...(errors.length > 0 && { errors }),
149
- },
150
- null,
151
- 2,
152
- ),
153
- );
154
168
  } catch (err) {
155
169
  reportSdkError(err);
156
170
  }
157
171
  }
158
172
 
159
- function pickFlagValue(args, flag) {
160
- const idx = args.indexOf(flag);
161
- if (idx === -1) return undefined;
162
- return args[idx + 1];
173
+ function emit({ ok, request_id, project_id, scanned, entries, errors, origin, hidden }) {
174
+ const hint = platformHiddenHint(entries, hidden);
175
+ console.log(
176
+ JSON.stringify(
177
+ {
178
+ ok,
179
+ request_id,
180
+ project_id,
181
+ scanned,
182
+ entries,
183
+ ...(errors.length > 0 && { errors }),
184
+ origin,
185
+ ...(hidden && { hidden }),
186
+ ...(hint && { hint }),
187
+ },
188
+ null,
189
+ 2,
190
+ ),
191
+ );
163
192
  }
package/lib/repos.mjs CHANGED
@@ -63,7 +63,7 @@ Usage:
63
63
  run402 repos <verb> [options] — fifteen verbs, tiered by how often you reach for them:
64
64
 
65
65
  Common:
66
- run402 repos create [name] [--org <org_id>] [--dir <path>] [--tier <tier>] [--project <id>]
66
+ run402 repos create [name] [--org <org_id>] [--dir <path>] [--nested] [--tier <tier>] [--project <id>]
67
67
  [--byo <s3://bucket/prefix>] [--profile <name> | --ambient] [--region <r>] [--endpoint <url>] [--no-init]
68
68
  run402 repos view [--project <id>] [--repo <repo_id>] [--human]
69
69
  run402 repos list [--org <org_id>] [--human]
@@ -383,6 +383,19 @@ Options:
383
383
  --dir <path> create: the working tree to scaffold (default: cwd). Not
384
384
  a git repository yet? One is created — \`repos create\` is
385
385
  a from-a-directory-to-a-hosted-repo verb by definition.
386
+ A directory INSIDE another repository (a monorepo app) is
387
+ not: the vault is still allocated, but the remote scaffold
388
+ is skipped (\`remote.status\` "skipped", the enclosing
389
+ toplevel named) and the response carries a
390
+ \`create_nested_repo\` next_action — re-run with --nested.
391
+ --nested create: make the working tree its OWN repository even when
392
+ it lies inside another one — \`git init -b main\` there, the
393
+ remote added there, and exactly one line
394
+ (\`/<relative path>/\`) appended to the ENCLOSING
395
+ repository's local .git/info/exclude so it never shows the
396
+ app as untracked noise. Nothing else in the enclosing
397
+ repository is touched (no .gitignore, index, or submodule).
398
+ No-op on a directory that is already its own toplevel.
386
399
  --tier <tier> create: project tier (default: prototype) — new projects only
387
400
  --idempotency-key <key>
388
401
  create: re-running with the same key resolves to the
@@ -740,7 +753,14 @@ function slugifyRepoName(name) {
740
753
  async function remoteBasenameCandidate(dir) {
741
754
  try {
742
755
  const { hardenedGit } = await import("#sdk/node");
743
- await hardenedGit(dir, ["rev-parse", "--git-dir"]);
756
+ const { realpathSync } = await import("node:fs");
757
+ // Only a remote of `dir`'s OWN repository names this repo. A directory
758
+ // that merely lies inside another checkout (a monorepo app, whether or
759
+ // not it is about to become a nested repository via --nested) would
760
+ // otherwise inherit the ENCLOSING repository's origin basename — the
761
+ // monorepo's name, never the app's.
762
+ const toplevel = (await hardenedGit(dir, ["rev-parse", "--show-toplevel"])).text().trim();
763
+ if (realpathSync(toplevel) !== realpathSync(dir)) return null;
744
764
  for (const name of ["run402", "origin"]) {
745
765
  let url;
746
766
  try {
@@ -834,9 +854,15 @@ async function printCreateResult({ sdk, projectId, vault, adopted, name, verbose
834
854
  if (name) console.error(`repo name not claimed (non-fatal): ${err?.message ?? String(err)}`);
835
855
  }
836
856
 
837
- const pushAction = vault.remote
857
+ // A `push_repo` action names a remote that EXISTS — only a scaffolded
858
+ // result has one. A skipped scaffold (the working tree lies inside another
859
+ // repository) allocated the vault but added no remote; its own
860
+ // `next_actions` (the SDK's `create_nested_repo`) name the way out.
861
+ const remoteScaffolded = vault.remote != null && vault.remote.status !== "skipped";
862
+ const pushAction = remoteScaffolded
838
863
  ? nextAction("push_repo", { command: `git push -u ${vault.remote.name} HEAD`, why: "Publish the current branch to the encrypted Run402 remote." })
839
864
  : null;
865
+ const remoteSkippedActions = vault.remote?.status === "skipped" ? (vault.remote.next_actions ?? []) : [];
840
866
  const claimAction = address ? null : orgSlug ? claimRepoNameAction(projectId) : claimOrgSlugAction();
841
867
  // gitvault-byo-primary-bucket task 3.5: a BYO vault's "add a copy" remedy
842
868
  // names a SECOND customer-held location (D7) — the plain mirror hint
@@ -846,7 +872,7 @@ async function printCreateResult({ sdk, projectId, vault, adopted, name, verbose
846
872
  const mirrorAction = isByo
847
873
  ? nextAction("configure_mirror", { command: "run402 repos mirror <destination>", why: GITVAULT_BYO_UNMIRRORED_REMEDY_STATEMENT })
848
874
  : nextAction("configure_mirror", { command: "run402 repos mirror <destination>", why: GITVAULT_MIRROR_SETUP_HINT });
849
- const nextActions = [pushAction, mirrorAction, claimAction].filter(Boolean);
875
+ const nextActions = [pushAction, ...remoteSkippedActions, mirrorAction, claimAction].filter(Boolean);
850
876
 
851
877
  // Secret-bearing (recovery_receipt): built fresh every call, printed once,
852
878
  // and never spilled into any cache path — see spillIfLarge's own doc
@@ -873,8 +899,14 @@ async function printCreateResult({ sdk, projectId, vault, adopted, name, verbose
873
899
  if (address) console.error(`address: ${address}`);
874
900
  else if (!orgSlug) console.error("no named address yet — claim an org slug (run402 org slug <slug>) to get run402::<slug>/<name> addresses");
875
901
  else console.error(`no address claimed — run 'run402 repos rename <name> --project ${projectId}' to claim one`);
876
- if (vault.remote) console.error(`remote '${vault.remote.name}' -> ${vault.remote.url} (${vault.remote.reason})`);
902
+ if (remoteScaffolded) {
903
+ console.error(`remote '${vault.remote.name}' -> ${vault.remote.url} (${vault.remote.reason})`);
904
+ if (vault.remote.nested) console.error(`nested repository inside ${vault.remote.enclosing_toplevel}${vault.remote.excluded_in_enclosing ? " — excluded there via .git/info/exclude" : " — could not write its .git/info/exclude"}`);
905
+ } else if (vault.remote?.status === "skipped") {
906
+ console.error(`remote skipped: ${vault.remote.reason}`);
907
+ }
877
908
  if (pushAction) console.error(`next: ${pushAction.command}`);
909
+ for (const action of remoteSkippedActions) if (action?.command) console.error(`next: ${action.command}`);
878
910
  if (isByo) {
879
911
  console.error(`storage: byo (${vault.byo_destination}) — ${GITVAULT_BYO_HEADLINE_STATEMENT}`);
880
912
  console.error(GITVAULT_BYO_NO_PAYLOAD_COPY_STATEMENT);
@@ -903,7 +935,7 @@ async function createAdopt(projectId, dir, a) {
903
935
  }
904
936
  try {
905
937
  const byo = resolveByoOption(a);
906
- const vault = await sdk.gitvault.init({ org_id: orgId, project_id: projectId, repo_dir: dir, ...(byo ? { byo } : {}) });
938
+ const vault = await sdk.gitvault.init({ org_id: orgId, project_id: projectId, repo_dir: dir, ...(a.includes("--nested") ? { nested: true } : {}), ...(byo ? { byo } : {}) });
907
939
  await printCreateResult({ sdk, projectId, vault, adopted: true, name: null, verboseArgv: a });
908
940
  } catch (err) {
909
941
  reportSdkError(err);
@@ -986,7 +1018,7 @@ async function createProvision(name, dir, a) {
986
1018
 
987
1019
  try {
988
1020
  const byo = resolveByoOption(a);
989
- const vault = await sdk.gitvault.init({ org_id: effectiveOrgId, project_id: provisioned.project_id, repo_dir: dir, ...(byo ? { byo } : {}) });
1021
+ const vault = await sdk.gitvault.init({ org_id: effectiveOrgId, project_id: provisioned.project_id, repo_dir: dir, ...(a.includes("--nested") ? { nested: true } : {}), ...(byo ? { byo } : {}) });
990
1022
  await printCreateResult({ sdk, projectId: provisioned.project_id, vault, adopted: false, name, verboseArgv: a });
991
1023
  } catch (err) {
992
1024
  reportSdkError(err);
@@ -995,7 +1027,7 @@ async function createProvision(name, dir, a) {
995
1027
 
996
1028
  async function create(args) {
997
1029
  const a = normalizeArgv(args);
998
- assertKnownFlags(a, [...CREATE_VALUE_FLAGS, "--ambient", "--no-init", "--help", "-h", "-v", "--verbose"], CREATE_VALUE_FLAGS);
1030
+ assertKnownFlags(a, [...CREATE_VALUE_FLAGS, "--ambient", "--no-init", "--nested", "--help", "-h", "-v", "--verbose"], CREATE_VALUE_FLAGS);
999
1031
  const positionals = requirePositionalCount(a, CREATE_VALUE_FLAGS, {
1000
1032
  min: 0, max: 1, command: "run402 repos create [name]", missing: "",
1001
1033
  });
package/lib/up.mjs CHANGED
@@ -10,7 +10,7 @@ import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.
10
10
  const HELP = `run402 up — Provision/link/deploy the current app
11
11
 
12
12
  Usage:
13
- run402 up [repo-or-path] [--name <name>] [--project <id>] [--manifest <path>] [--dir <path>] [--tier <tier>] [-y|--yes] [--check|--print-spec|--plan|--require-plan <id>|--repo-only] [--verify] [--human|--json-stream] [--quiet]
13
+ run402 up [repo-or-path] [--name <name>] [--project <id>] [--manifest <path>] [--dir <path>] [--tier <tier>] [-y|--yes] [--check|--print-spec|--plan|--require-plan <id>|--repo-only] [--nested] [--verify] [--human|--json-stream] [--quiet]
14
14
  run402 up verify [repo-or-path] [--project <id>] [--manifest <path>] [--dir <path>] [--human|--json-stream]
15
15
 
16
16
  Options:
@@ -55,12 +55,24 @@ Options:
55
55
  bearing deploy against a project with a live release is
56
56
  rehearsed on a contained branch and committed only on a
57
57
  passing report; a first deploy has nothing to protect and
58
- commits directly (result.deploy.rehearsal says which).
58
+ commits directly, and a plan whose migrations are all
59
+ already applied with an identical checksum is not
60
+ rehearsed either (result.deploy.rehearsal says which:
61
+ no_live_release / migrations_unchanged / no_migrations).
59
62
  --repo-only Provision + scaffold the run402 remote + first push,
60
63
  and stop there — no deploy. The vault-only track
61
64
  (D8), composed through up instead of run402 repos
62
65
  create. Incompatible with --check/--print-spec/--plan/
63
66
  --require-plan/--verify.
67
+ --nested Give an app root that lies INSIDE another repository (a
68
+ monorepo workspace) its own nested repository: git init
69
+ -b main there, the run402 remote added there, the first
70
+ push made from there, and exactly one line
71
+ (/<relative path>/) appended to the ENCLOSING
72
+ repository's local .git/info/exclude. Nothing else in
73
+ the enclosing repository is touched (no .gitignore,
74
+ index, or submodule). No-op when the app root is already
75
+ its own repository.
64
76
 
65
77
  Repo composition (D4): against a local directory (not a git URL source), up
66
78
  composes git init (only when the app root is not already a repository) +
@@ -68,17 +80,25 @@ provision + a run402 remote scaffold (origin is never claimed) + a first
68
80
  gitvault push — one command, the fly-launch shape. The app root is the
69
81
  manifest's directory; an app root that lies INSIDE another repository is
70
82
  left untouched (result.repo.status "skipped", reason
71
- "inside_other_repository", toplevel named). The scaffold and first push are
72
- best-effort: a git or vault hiccup never turns an otherwise-successful deploy
73
- into a failure, and is reported under result.repo (default apply) or result
74
- (--repo-only) instead.
83
+ "inside_other_repository", toplevel named) and result.repo.next_actions
84
+ carries create_nested_repo re-run with --nested to give the app its own
85
+ encrypted remote (result.repo.gitvault.nested true, enclosing_toplevel,
86
+ excluded_in_enclosing). The scaffold and first push are best-effort: a git
87
+ or vault hiccup never turns an otherwise-successful deploy into a failure,
88
+ and is reported under result.repo (default apply) or result (--repo-only)
89
+ instead.
75
90
 
76
91
  Identity: up makes sure this principal has a display name before the deploy
77
- that will be credited to it an existing name is kept, otherwise the
78
- detected client name (claude-code, codex, cursor, grok, or agent) is set (approved)
79
- by -y, or asked once) and joins the project room under it. Change it any
80
- time with 'run402 org whoami --set-name <name>'. Reported under
81
- result.identity.
92
+ that will be credited to it, then joins the project room under it. An
93
+ existing name is kept; otherwise the detected client name (claude-code,
94
+ codex, cursor, or grok) is set (approved by -y, or asked once); when no
95
+ client is detected nothing is written and the room presence is 'agent' for
96
+ coordination only. RUN402_AGENT_NAME=<name> sets the name, overriding an
97
+ existing one; RUN402_CLIENT=<name> declares the client when it is not
98
+ auto-detected. A detected client that was not applied (the principal is
99
+ already named) is still reported. Change the name any time with
100
+ 'run402 org whoami --set-name <name>'. Reported under result.identity
101
+ (source, detected, detection).
82
102
  --json Emit one final JSON object on stdout (default; compatibility no-op).
83
103
  --human Emit the legacy human success/blocking summary on stdout.
84
104
  --json-stream Emit NDJSON progress events on stdout and a final result event.
@@ -165,6 +185,7 @@ export async function run(args = []) {
165
185
  "--json-stream",
166
186
  "--repo-only",
167
187
  "--no-rehearse",
188
+ "--nested",
168
189
  ],
169
190
  [
170
191
  "--name",
@@ -276,6 +297,7 @@ export async function run(args = []) {
276
297
  });
277
298
  }
278
299
  const repoOnly = parsed.includes("--repo-only");
300
+ const nested = parsed.includes("--nested");
279
301
  if (repoOnly && (dryRun || isNonApplyingMode(mode) || isApplyReviewedMode(mode) || verifyEdge)) {
280
302
  fail({
281
303
  code: "BAD_USAGE",
@@ -307,7 +329,7 @@ export async function run(args = []) {
307
329
 
308
330
  let result;
309
331
  if (repoOnly) {
310
- result = await runRepoOnly({ sdk, workDir, createdRepository, opts: parsed, tier, allowWarningCodes, idempotencyKey: flagValue(parsed, "--idempotency-key") ?? undefined });
332
+ result = await runRepoOnly({ sdk, workDir, createdRepository, nested, opts: parsed, tier, allowWarningCodes, idempotencyKey: flagValue(parsed, "--idempotency-key") ?? undefined });
311
333
  } else {
312
334
  result = await sdk.up({
313
335
  source,
@@ -345,7 +367,7 @@ export async function run(args = []) {
345
367
  if (composeRepo && mode === undefined) {
346
368
  const projectId = result?.result?.project_id ?? result?.result?.deploy?.project_id ?? null;
347
369
  if (projectId) {
348
- const repoResult = await composeRepoPushStep({ sdk, workDir, projectId, createdRepository });
370
+ const repoResult = await composeRepoPushStep({ sdk, workDir, projectId, createdRepository, nested });
349
371
  if (result.result) result.result.repo = repoResult;
350
372
  }
351
373
  }
@@ -364,9 +386,9 @@ export async function run(args = []) {
364
386
  } else if (mode === "printSpec") {
365
387
  console.log(JSON.stringify(result.result?.spec ?? null, null, 2));
366
388
  } else if (human && result?.result?.app_result) {
367
- console.log(formatAppUpHuman(result.result.app_result));
389
+ console.log([formatAppUpHuman(result.result.app_result), formatRepoSkipLine(result)].filter(Boolean).join("\n"));
368
390
  } else if (human && shouldRenderHumanSuccess(result)) {
369
- console.log(formatLegacyUpSuccess(result));
391
+ console.log([formatLegacyUpSuccess(result), formatRepoSkipLine(result)].filter(Boolean).join("\n"));
370
392
  } else {
371
393
  console.log(JSON.stringify(result, null, 2));
372
394
  }
@@ -637,12 +659,20 @@ async function gitInitIfNeeded(dir) {
637
659
  * fold-in follows (`gitvault-scaffold.mjs`): a git or vault hiccup here must
638
660
  * never turn an otherwise-successful `up` into a failure.
639
661
  */
640
- async function composeRepoPushStep({ sdk, workDir, projectId, createdRepository }) {
662
+ async function composeRepoPushStep({ sdk, workDir, projectId, createdRepository, nested = false }) {
641
663
  const { resolveOwningOrgId } = await import("./org-context.mjs");
642
664
  const { scaffoldGitvaultRemote } = await import("./gitvault-scaffold.mjs");
643
665
  const orgId = await resolveOwningOrgId(projectId);
644
- const scaffold = await scaffoldGitvaultRemote({ repoDir: workDir, projectId, orgId: orgId ?? undefined, createRepoIfMissing: false });
666
+ // `--nested`: an app root INSIDE another repository (a monorepo workspace)
667
+ // becomes its own repository with the encrypted remote — the enclosing
668
+ // checkout only gains one local `.git/info/exclude` line. Without it the
669
+ // scaffold is skipped there and the result's `next_actions` name
670
+ // `run402 up --nested` (the caller's own spelling of the way out).
671
+ const scaffold = await scaffoldGitvaultRemote({ repoDir: workDir, projectId, orgId: orgId ?? undefined, createRepoIfMissing: false, nested, nestedCommand: "run402 up --nested" });
645
672
  if (createdRepository && scaffold.gitvault) scaffold.gitvault.created_repository = true;
673
+ // A nested repository the scaffold itself just created is as fresh as one
674
+ // `up`'s own git init made: nothing committed, every file untracked.
675
+ const freshRepository = createdRepository || scaffold.gitvault?.created_repository === true;
646
676
  const out = { ...scaffold, first_push: null, first_push_error: null };
647
677
  if (scaffold.status !== "scaffolded") {
648
678
  // Nothing was scaffolded (the app root is inside another repository, or
@@ -666,7 +696,7 @@ async function composeRepoPushStep({ sdk, workDir, projectId, createdRepository
666
696
  project_id: projectId,
667
697
  org_id: orgId,
668
698
  repo_dir: workDir,
669
- ...(createdRepository ? { snapshot: { allowDirty: true } } : {}),
699
+ ...(freshRepository ? { snapshot: { allowDirty: true } } : {}),
670
700
  onVaultCreated: (created) => { vaultCreated = created; },
671
701
  });
672
702
  out.first_push = {
@@ -674,7 +704,7 @@ async function composeRepoPushStep({ sdk, workDir, projectId, createdRepository
674
704
  form: pushed.form,
675
705
  gitvault_commit: pushed.gitvault_commit,
676
706
  vault_created: vaultCreated,
677
- ...(createdRepository ? { captured_dirty: true, modified_captured: pushed.snapshot?.modified_captured ?? null, untracked_captured: pushed.snapshot?.untracked_captured ?? null } : {}),
707
+ ...(freshRepository ? { captured_dirty: true, modified_captured: pushed.snapshot?.modified_captured ?? null, untracked_captured: pushed.snapshot?.untracked_captured ?? null } : {}),
678
708
  };
679
709
  } catch (err) {
680
710
  out.first_push_error = { code: err?.body?.code ?? err?.code ?? "GITVAULT_PUSH_FAILED", message: err?.message ?? String(err) };
@@ -688,7 +718,7 @@ async function composeRepoPushStep({ sdk, workDir, projectId, createdRepository
688
718
  * create`'s own shape (task 2.6); `up --repo-only` is the "I'm already
689
719
  * inside `up`'s mental model" entry point to the same outcome.
690
720
  */
691
- async function runRepoOnly({ sdk, workDir, createdRepository, opts, tier, idempotencyKey }) {
721
+ async function runRepoOnly({ sdk, workDir, createdRepository, nested = false, opts, tier, idempotencyKey }) {
692
722
  // `sdk.up()`'s action graph auto-approves prerequisites (allowance, tier)
693
723
  // for a cold start; calling projects.provision directly bypasses that, so
694
724
  // this mirrors the SAME gate `run402 projects provision` itself uses —
@@ -697,7 +727,7 @@ async function runRepoOnly({ sdk, workDir, createdRepository, opts, tier, idempo
697
727
  if (!isCoreApiTarget() && !loadLiveControlPlaneSession()) allowanceAuthHeaders("/projects/v1");
698
728
  const name = flagValue(opts, "--name") ?? undefined;
699
729
  const provisioned = await sdk.projects.provision({ tier, name, idempotencyKey });
700
- const repo = await composeRepoPushStep({ sdk, workDir, projectId: provisioned.project_id, createdRepository });
730
+ const repo = await composeRepoPushStep({ sdk, workDir, projectId: provisioned.project_id, createdRepository, nested });
701
731
  return {
702
732
  action: "up",
703
733
  mode: "repo-only",
@@ -744,6 +774,22 @@ function shouldRenderHumanSuccess(result) {
744
774
  result?.result?.deploy?.release_id;
745
775
  }
746
776
 
777
+ /**
778
+ * One line for the human summary when the repo compose was skipped — the
779
+ * reason and, when the app root lies inside another repository, the exact
780
+ * `--nested` command that gives it an encrypted remote of its own.
781
+ */
782
+ function formatRepoSkipLine(result) {
783
+ const repo = result?.result?.repo;
784
+ // Only the actionable skip gets a human line: an app root inside another
785
+ // repository has a remedy (--nested). Other skips stay JSON-only.
786
+ if (repo?.status !== "skipped" || repo.reason !== "inside_other_repository") return null;
787
+ const where = repo.toplevel ? ` (inside ${repo.toplevel})` : "";
788
+ const nestedAction = (Array.isArray(repo.next_actions) ? repo.next_actions : []).find((action) => action?.type === "create_nested_repo");
789
+ const remedy = nestedAction?.command ? ` Give it its own encrypted remote with: ${nestedAction.command}` : "";
790
+ return `Encrypted remote skipped: ${repo.reason ?? "skipped"}${where}.${remedy}`;
791
+ }
792
+
747
793
  function formatLegacyUpSuccess(result) {
748
794
  const urls = result?.result?.deploy?.urls ?? {};
749
795
  const origin = urls.site ?? urls.subdomain ?? urls.deployment;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "4.80.0",
3
+ "version": "4.80.1",
4
4
  "description": "CLI for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.",
5
5
  "type": "module",
6
6
  "bin": {