run402 4.43.0 → 4.44.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 (2) hide show
  1. package/lib/repos.mjs +112 -13
  2. package/package.json +1 -1
package/lib/repos.mjs CHANGED
@@ -33,7 +33,7 @@ import { resolveGitvaultTarget } from "./gitvault-target.mjs";
33
33
  import { nextAction, claimOrgSlugAction, claimRepoNameAction } from "./next-actions.mjs";
34
34
  import { printKeystoreLocation } from "./gitvault.mjs";
35
35
  import { gitvaultRemoteUrlForRepo } from "#sdk";
36
- import { sdkStats, printVerboseStats } from "./stats.mjs";
36
+ import { sdkStats, printVerboseStats, isVerbose } from "./stats.mjs";
37
37
  import {
38
38
  normalizeArgv,
39
39
  hasHelp,
@@ -63,7 +63,7 @@ Then plain git, forever:
63
63
  git clone run402::<org>/<repo>
64
64
 
65
65
  Occasional:
66
- run402 repos snapshot [--project <id>] [--repo <repo_id>] [--message <text>] [--checkpoint] [--dry-run] [--allow-dirty]
66
+ run402 repos snapshot [--project <id>] [--repo <repo_id>] [--message <text>] [--checkpoint] [--dry-run] [--allow-dirty] [--manifest-out <path>]
67
67
  run402 repos mirror [<destination>] [--off] [--backfill] [--profile <name> | --ambient] [--region <r>] [--endpoint <url>] [--project <id>] [--repo <repo_id>]
68
68
  run402 repos recover <source> --out <dir> [--repo <repo_id>] [--profile <name> | --ambient] [--region <r>] [--endpoint <url>] [--human]
69
69
 
@@ -134,6 +134,15 @@ Subcommands:
134
134
  capture it as-is; the result then discloses exactly what was
135
135
  swept in (modified_captured / untracked_captured), printed to
136
136
  stderr too. \`--dry-run\` surfaces the same refusal.
137
+ Both \`--dry-run\` and a real snapshot print a SUMMARY by default —
138
+ file counts (files_total/files_changed/files_new), total/delta
139
+ bytes, and up to 200 changed/new paths (changed_more names any
140
+ overflow) — never the full captured-file inventory, which can run
141
+ to thousands of entries on a real repo. \`--manifest-out <path>\`
142
+ writes the complete inventory to a file (the result's
143
+ manifest_path names it); \`-v\`/\`--verbose\` inlines the full
144
+ inventory directly in the JSON (in addition to its usual stderr
145
+ stats line, not instead of it).
137
146
  mirror ONE flag-driven verb for the client-side, customer-
138
147
  owned ciphertext mirror — run402 never holds a credential to it.
139
148
  No argument: READ the configured destination + a keyless
@@ -145,12 +154,16 @@ Subcommands:
145
154
  mirror that fell behind). Exactly one of these per call. Mirror
146
155
  state also renders inside \`repos view\`; mirror INTEGRITY inside
147
156
  \`repos fsck --mirror\`.
148
- recover \`r402s-recover\`: rebuild a working git repository straight from
149
- a mirrored prefix, with NO SERVER INVOLVED — the offline disaster
150
- path (normal retrieval is plain \`git clone run402::<org>/<repo>\`,
151
- no \`repos clone\` verb exists). Proves this mirror's validity,
152
- never freshness read both honesty statements before relying on
153
- the result. \`--human\` renders a short summary instead of JSON.
157
+ recover \`r402s-recover\`: rebuild a BARE recovery repository (no working
158
+ files) straight from a mirrored prefix, with NO SERVER INVOLVED —
159
+ the offline disaster path (normal retrieval is plain \`git clone
160
+ run402::<org>/<repo>\`, no \`repos clone\` verb exists). The result's
161
+ \`layout\` is \`"bare"\` and its \`next_actions\` print the exact
162
+ \`git clone <out_dir> <out_dir>-worktree\` to run for a working
163
+ tree — recover itself never checks files out. Proves this
164
+ mirror's validity, never freshness — read both honesty statements
165
+ before relying on the result. \`--human\` renders a short summary
166
+ instead of JSON.
154
167
  fsck Walks the head chain AND materializes
155
168
  the ref map, advancing BOTH
156
169
  local trust pins — reported EXPLICITLY as local_state_changed +
@@ -181,7 +194,11 @@ Subcommands:
181
194
  has NOT yet been rotated away — pending_removal is honest
182
195
  bookkeeping, not enforcement, until \`access repair\`/\`revoke-key\`
183
196
  actually rotates. history_scope (which epochs each recipient can
184
- read) is not reported by this read — see the \`gap\` field.
197
+ read) is not reported by this read — see the \`gap\` field. An
198
+ enrolled teammate's key envelope is wrapped AUTOMATICALLY — no
199
+ manual step — by the next \`git push\` or \`repos snapshot\` any
200
+ key-holding client runs (best-effort, non-blocking; the retired
201
+ \`gitvault reconcile\` verb did this by hand and is REMOVED).
185
202
  \`--human\` renders a compact roster instead of JSON (the read
186
203
  form only — repair/revoke-key/declare-exposure stay JSON-only).
187
204
  access repair
@@ -252,6 +269,12 @@ Options:
252
269
  The result discloses exactly what was swept in
253
270
  (modified_captured / untracked_captured) — even this
254
271
  override never captures silently.
272
+ --manifest-out <path>
273
+ snapshot: write the complete captured-file inventory
274
+ (the full JSON the SDK returned, untouched) to a private
275
+ 0600 file instead of stdout's default summary. The
276
+ printed result's manifest_path names it. Composes with
277
+ --dry-run and with -v/--verbose.
255
278
  --off mirror: remove the configured destination (config only)
256
279
  --backfill mirror: copy every object the configured mirror is missing
257
280
  --profile <name> mirror / recover: the AWS credential profile name for an
@@ -283,7 +306,10 @@ Options:
283
306
  -v, --verbose Print one stderr summary line of this call's request
284
307
  stats (round trips, wire time, bytes). Coexists with
285
308
  --human. The JSON result always carries a \`stats\` block
286
- regardless of this flag.
309
+ regardless of this flag. On \`snapshot\`/\`snapshot
310
+ --dry-run\`, ALSO inlines the full captured-file
311
+ inventory in stdout's JSON (composes with the stats
312
+ line — both happen, not one or the other).
287
313
  --json No-op: stdout is already JSON.
288
314
 
289
315
  Terminal loss (protocol §0):
@@ -300,6 +326,7 @@ Examples:
300
326
  run402 repos list --org org_1a2b3c --human
301
327
  run402 repos rename my-notes --project prj_1a2b3c
302
328
  run402 repos snapshot --dry-run
329
+ run402 repos snapshot --dry-run --manifest-out /tmp/snapshot-plan.json
303
330
  run402 repos snapshot --allow-dirty
304
331
  run402 repos mirror s3://acme-vault-mirror --profile acme
305
332
  run402 repos mirror --backfill
@@ -1068,7 +1095,7 @@ async function del(args) {
1068
1095
 
1069
1096
  // ─── snapshot ───────────────────────────────────────────────────────────────
1070
1097
 
1071
- const SNAPSHOT_VALUE_FLAGS = [...COMMON_VALUE_FLAGS, "--message"];
1098
+ const SNAPSHOT_VALUE_FLAGS = [...COMMON_VALUE_FLAGS, "--message", "--manifest-out"];
1072
1099
 
1073
1100
  /**
1074
1101
  * When neither `--repo` nor `--project` was given explicitly, look at the
@@ -1107,6 +1134,74 @@ function printDirtyDisclosure(snapshot) {
1107
1134
  for (const p of snapshot.untracked_captured ?? []) console.error(`captured (untracked): ${p}`);
1108
1135
  }
1109
1136
 
1137
+ /** How many `changed_paths` entries {@link summarizeSnapshotPayload} inlines before capping. */
1138
+ const SNAPSHOT_CHANGED_PATHS_CAP = 200;
1139
+
1140
+ /**
1141
+ * item 1 (dogfood): `snapshot.captured` — the SDK's full captured-file
1142
+ * inventory (every tracked + untracked-not-ignored path in the repo, always
1143
+ * populated regardless of how small the actual push delta is) — is a
1144
+ * multi-thousand-line flood on a real repo, even when what actually
1145
+ * publishes is a handful of kilobytes. The SDK keeps returning it in full
1146
+ * (thin-shim law: other SDK consumers may want it) — this reshapes ONLY the
1147
+ * CLI's own stdout, by default:
1148
+ *
1149
+ * - `files_total` / `files_changed` / `files_new` — counts. `files_total`
1150
+ * is `captured.length`; `files_changed`/`files_new` are
1151
+ * `modified_captured`/`untracked_captured` — the ONLY per-path drift
1152
+ * this data distinguishes (the `--allow-dirty` sweep-in disclosure).
1153
+ * On the common clean-tree path both are empty, so `changed_paths` is
1154
+ * too — there is no `files_deleted` here, because `captured` only
1155
+ * lists paths PRESENT on disk today; nothing in this data names which
1156
+ * paths a plain clean push's new commits touched.
1157
+ * - `changed_paths` — `modified_captured` ∪ `untracked_captured`,
1158
+ * sorted, capped at `SNAPSHOT_CHANGED_PATHS_CAP`; `changed_more` names
1159
+ * the overflow explicitly rather than truncating silently.
1160
+ * - `snapshot.captured` / `.paths` / `.modified_captured` /
1161
+ * `.untracked_captured` are dropped from the default `snapshot` object
1162
+ * (its other scalar fields — kind, oid, tree_oid, head, head_oid,
1163
+ * captured_digest, top_level, global_excludes_path — stay). `verbose`
1164
+ * restores them (composes with the summary fields, does not replace
1165
+ * them) — the `-v`/`--verbose` flag already means "print a stats
1166
+ * line"; on `snapshot --dry-run`/`snapshot` it ALSO inlines the full
1167
+ * inventory.
1168
+ * - `manifest_path` is `null` unless `--manifest-out <path>` wrote the
1169
+ * COMPLETE, untouched payload to that file — see `writeManifestOut`.
1170
+ */
1171
+ function summarizeSnapshotPayload(payload, { verbose = false, manifestPath = null } = {}) {
1172
+ const out = { ...payload, manifest_path: manifestPath };
1173
+ const snapshot = payload.snapshot;
1174
+ if (!snapshot) return out;
1175
+ const modified = snapshot.modified_captured ?? [];
1176
+ const untracked = snapshot.untracked_captured ?? [];
1177
+ const changedAll = [...modified, ...untracked].sort();
1178
+ const changedPaths = changedAll.slice(0, SNAPSHOT_CHANGED_PATHS_CAP);
1179
+ out.files_total = Array.isArray(snapshot.captured) ? snapshot.captured.length : 0;
1180
+ out.files_changed = modified.length;
1181
+ out.files_new = untracked.length;
1182
+ out.changed_paths = changedPaths;
1183
+ out.changed_more = changedAll.length - changedPaths.length;
1184
+ if (!verbose) {
1185
+ const { captured, paths, modified_captured, untracked_captured, ...trimmedSnapshot } = snapshot;
1186
+ out.snapshot = trimmedSnapshot;
1187
+ }
1188
+ return out;
1189
+ }
1190
+
1191
+ /** `--manifest-out <path>`: write the COMPLETE, untouched plan/push payload — the full captured-file inventory included — to a private 0600 file. */
1192
+ function writeManifestOut(path, payload) {
1193
+ try {
1194
+ writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
1195
+ } catch (e) {
1196
+ fail({
1197
+ code: "MANIFEST_OUT_WRITE_FAILED",
1198
+ message: `could not write the full snapshot inventory to ${path}: ${e instanceof Error ? e.message : String(e)}`,
1199
+ hint: "Check that the path is writable and its parent directory exists.",
1200
+ details: { path },
1201
+ });
1202
+ }
1203
+ }
1204
+
1110
1205
  async function snapshot(args) {
1111
1206
  const a = normalizeArgv(args);
1112
1207
  assertKnownFlags(a, [...SNAPSHOT_VALUE_FLAGS, "--checkpoint", "--dry-run", "--allow-dirty", "-v", "--verbose", "--help", "-h"], SNAPSHOT_VALUE_FLAGS);
@@ -1137,10 +1232,13 @@ async function snapshot(args) {
1137
1232
  if (allowDirty) snapshotOpts.allowDirty = true;
1138
1233
  if (Object.keys(snapshotOpts).length > 0) opts.snapshot = snapshotOpts;
1139
1234
  if (a.includes("--checkpoint")) opts.checkpoint = true;
1235
+ const manifestOutPath = flagValue(a, "--manifest-out");
1236
+ const verbose = isVerbose(a);
1140
1237
  try {
1141
1238
  if (dryRun) {
1142
1239
  const plan = await sdk.gitvault.planPush(opts);
1143
- printJson(sdk, plan);
1240
+ if (manifestOutPath != null) writeManifestOut(manifestOutPath, plan);
1241
+ printJson(sdk, summarizeSnapshotPayload(plan, { verbose, manifestPath: manifestOutPath }));
1144
1242
  if (plan.allocation_needed) {
1145
1243
  console.error("dry-run: no repo allocated for this project yet — a real snapshot would allocate one first; object/byte sizing is not knowable until then");
1146
1244
  } else {
@@ -1154,7 +1252,8 @@ async function snapshot(args) {
1154
1252
  return;
1155
1253
  }
1156
1254
  const result = await sdk.gitvault.push(opts);
1157
- printJson(sdk, result);
1255
+ if (manifestOutPath != null) writeManifestOut(manifestOutPath, result);
1256
+ printJson(sdk, summarizeSnapshotPayload(result, { verbose, manifestPath: manifestOutPath }));
1158
1257
  console.error(`published generation ${result.generation} (${result.form})`);
1159
1258
  if (result.mirror_push?.outcome === "pushed") {
1160
1259
  console.error(`mirror: pushed generation ${result.generation} (${result.mirror_push.summary?.objects_copied ?? 0} object(s) copied)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "4.43.0",
3
+ "version": "4.44.0",
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": {