run402 4.80.0 → 4.81.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 (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 +131 -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 +137 -4
  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
@@ -1000,6 +1000,7 @@ export class Gitvault {
1000
1000
  repo_dir: options.repo_dir,
1001
1001
  org_id: options.org_id,
1002
1002
  project_id: options.project_id,
1003
+ ...(options.nested !== undefined ? { nested: options.nested } : {}),
1003
1004
  ...(options.remote_name !== undefined ? { remote_name: options.remote_name } : {}),
1004
1005
  ...(options.remote_url !== undefined ? { remote_url: options.remote_url } : {}),
1005
1006
  });
@@ -1060,16 +1061,32 @@ export class Gitvault {
1060
1061
  *
1061
1062
  * No key material or allocation is required — the cold-start path gains no
1062
1063
  * prompts or network dependencies from this.
1064
+ *
1065
+ * `nested: true` — the monorepo case. An app root that lies INSIDE another
1066
+ * repository (an agent workspace with many apps under one checkout) is
1067
+ * still made its own repository: `git init -b main` in `repo_dir`, the
1068
+ * remote added there, and ONE line — `/<relative path of repo_dir>/` —
1069
+ * appended to the ENCLOSING repository's local `.git/info/exclude`
1070
+ * (located via `rev-parse --git-path info/exclude`, so a worktree resolves
1071
+ * to its common dir) so the enclosing checkout never lists the nested
1072
+ * repository as untracked noise. Never `.gitignore` (tracked content),
1073
+ * never the index, never a submodule: nothing else in the enclosing
1074
+ * repository changes, and its remotes are untouched. Idempotent — a second
1075
+ * call on the already-nested app root adds nothing and re-reports the same
1076
+ * shape. Without `nested`, the same situation is reported `skipped` with a
1077
+ * `create_nested_repo` next_action naming this way out.
1063
1078
  */
1064
1079
  async scaffoldRemote(options) {
1065
1080
  const { hardenedGit } = await this.#snapshot();
1066
1081
  const url = options.remote_url ?? gitvaultRemoteUrl(options.org_id, options.project_id);
1067
1082
  let createdRepository = false;
1083
+ let enclosingToplevel = null;
1068
1084
  // The scaffold acts on the APP ROOT only (first-deploy-agent-dx, design
1069
1085
  // D3): a directory that is not a repository is initialized; a directory
1070
1086
  // that is itself a git toplevel gets the remote; a directory that merely
1071
1087
  // lies INSIDE some other repository is left exactly as it was — nobody
1072
- // asked for a remote on that repository. The result says which.
1088
+ // asked for a remote on that repository unless `nested` says to make
1089
+ // it its own repository. The result says which.
1073
1090
  let insideRepository = true;
1074
1091
  try {
1075
1092
  await hardenedGit(options.repo_dir, ["rev-parse", "--git-dir"]);
@@ -1077,23 +1094,47 @@ export class Gitvault {
1077
1094
  catch {
1078
1095
  insideRepository = false;
1079
1096
  }
1097
+ const { realpathSync } = await import("node:fs");
1098
+ const here = realpathSync(options.repo_dir);
1080
1099
  if (insideRepository) {
1081
1100
  const toplevel = (await hardenedGit(options.repo_dir, ["rev-parse", "--show-toplevel"])).text().trim();
1082
- const { realpathSync } = await import("node:fs");
1083
- const here = realpathSync(options.repo_dir);
1084
1101
  if (realpathSync(toplevel) !== here) {
1085
- return {
1086
- status: "skipped",
1087
- name: options.remote_name ?? "run402",
1088
- url,
1089
- created_repository: false,
1090
- already_present: false,
1091
- existing_url: null,
1092
- reason: `${options.repo_dir} is inside the repository at ${toplevel} — an enclosing repository is never touched; run from the app root, or make the app root its own repository`,
1093
- toplevel,
1094
- };
1102
+ if (!options.nested) {
1103
+ return {
1104
+ status: "skipped",
1105
+ name: options.remote_name ?? "run402",
1106
+ url,
1107
+ created_repository: false,
1108
+ already_present: false,
1109
+ existing_url: null,
1110
+ reason: `${options.repo_dir} is inside the repository at ${toplevel} — an enclosing repository is never touched; run from the app root, or make the app root its own repository`,
1111
+ toplevel,
1112
+ next_actions: [
1113
+ {
1114
+ type: "create_nested_repo",
1115
+ command: `run402 repos create --nested --project ${options.project_id}`,
1116
+ why: `The app lives inside the repository at ${toplevel}; a nested repo keeps an encrypted remote for the app without touching the enclosing repository.`,
1117
+ },
1118
+ ],
1119
+ };
1120
+ }
1121
+ // Nested: the app root becomes its own repository, exactly the way a
1122
+ // directory that is not a repository at all would below. The
1123
+ // enclosing repository is only ever read (its toplevel) and, once
1124
+ // the nested repository exists, told to exclude it locally.
1125
+ enclosingToplevel = toplevel;
1126
+ insideRepository = false;
1095
1127
  }
1096
1128
  }
1129
+ // Not inside any repository at all: `nested` is moot, and the result
1130
+ // truthfully omits `nested: true`.
1131
+ if (insideRepository && options.nested) {
1132
+ // The app root is already its own toplevel. A previous nested scaffold
1133
+ // (or a hand-made nested repository) may still sit inside another
1134
+ // checkout — find it from the PARENT directory, so the idempotent
1135
+ // second call keeps the exclude line in place.
1136
+ enclosingToplevel = await this.#enclosingToplevelOf(here);
1137
+ }
1097
1138
  if (!insideRepository) {
1098
1139
  // `main`, not whatever `init.defaultBranch` (or the pre-2.28 hardcoded
1099
1140
  // `master`) happens to be — the docs and this helper's own remedy text
@@ -1114,6 +1155,11 @@ export class Gitvault {
1114
1155
  }
1115
1156
  createdRepository = true;
1116
1157
  }
1158
+ let nestedFields = {};
1159
+ if (enclosingToplevel !== null) {
1160
+ const excluded = await this.#excludeNestedRepoInEnclosing(enclosingToplevel, here);
1161
+ nestedFields = { nested: true, enclosing_toplevel: enclosingToplevel, excluded_in_enclosing: excluded };
1162
+ }
1117
1163
  // An EXISTING repository is never touched — only the branch a fresh `init`
1118
1164
  // above just created gets steered to `main`; this repository's own HEAD
1119
1165
  // (whatever branch it already uses) is left exactly as it was.
@@ -1143,10 +1189,72 @@ export class Gitvault {
1143
1189
  already_present: true,
1144
1190
  existing_url: existing,
1145
1191
  reason: existing === url ? `'${name}' already points here — nothing to add` : `'${name}' points at ${existing} — left unchanged, nothing was added`,
1192
+ ...nestedFields,
1146
1193
  };
1147
1194
  }
1148
1195
  await add(name);
1149
- return { status: "scaffolded", name, url, created_repository: createdRepository, already_present: false, existing_url: null, reason: `no existing '${name}' remote — added` };
1196
+ return { status: "scaffolded", name, url, created_repository: createdRepository, already_present: false, existing_url: null, reason: `no existing '${name}' remote — added`, ...nestedFields };
1197
+ }
1198
+ /**
1199
+ * The toplevel of the repository that ENCLOSES `dir` (a realpath that is
1200
+ * itself a git toplevel), found from its parent directory; `null` when the
1201
+ * parent lies in no repository at all. Read-only.
1202
+ */
1203
+ async #enclosingToplevelOf(dir) {
1204
+ const { hardenedGit } = await this.#snapshot();
1205
+ const { dirname } = await import("node:path");
1206
+ const parent = dirname(dir);
1207
+ if (parent === dir)
1208
+ return null;
1209
+ try {
1210
+ const toplevel = (await hardenedGit(parent, ["rev-parse", "--show-toplevel"])).text().trim();
1211
+ return toplevel.length > 0 ? toplevel : null;
1212
+ }
1213
+ catch {
1214
+ return null;
1215
+ }
1216
+ }
1217
+ /**
1218
+ * Append `/<relative path of nestedDir from toplevel>/` to the enclosing
1219
+ * repository's LOCAL `.git/info/exclude` — never `.gitignore`, which is
1220
+ * tracked content (the same discipline as `excludeMessagingCacheFromGit`
1221
+ * in `gitvault-restore.ts`). Located via `rev-parse --git-path
1222
+ * info/exclude` so a linked worktree resolves to its common dir. Creates
1223
+ * the file when absent; idempotent when the line is already there.
1224
+ * Returns whether the line is in place — `false` (never a throw) when the
1225
+ * enclosing repository could not be written, so a read-only checkout
1226
+ * still gets its nested repository and the result says the exclude is
1227
+ * missing.
1228
+ */
1229
+ async #excludeNestedRepoInEnclosing(toplevel, nestedDir) {
1230
+ try {
1231
+ const { hardenedGit } = await this.#snapshot();
1232
+ const { realpathSync, readFileSync, appendFileSync, mkdirSync } = await import("node:fs");
1233
+ const { dirname, isAbsolute, relative, resolve, sep } = await import("node:path");
1234
+ const top = realpathSync(toplevel);
1235
+ const rel = relative(top, nestedDir);
1236
+ if (rel.length === 0 || rel.startsWith("..") || isAbsolute(rel))
1237
+ return false;
1238
+ const entry = `/${rel.split(sep).join("/")}/`;
1239
+ const rawPath = (await hardenedGit(top, ["rev-parse", "--git-path", "info/exclude"])).text().trim();
1240
+ const excludePath = isAbsolute(rawPath) ? rawPath : resolve(top, rawPath);
1241
+ let existing = "";
1242
+ try {
1243
+ existing = readFileSync(excludePath, "utf-8");
1244
+ }
1245
+ catch {
1246
+ existing = "";
1247
+ }
1248
+ if (existing.split("\n").some((line) => line.trim() === entry))
1249
+ return true;
1250
+ mkdirSync(dirname(excludePath), { recursive: true });
1251
+ const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
1252
+ appendFileSync(excludePath, `${needsLeadingNewline ? "\n" : ""}${entry}\n`);
1253
+ return true;
1254
+ }
1255
+ catch {
1256
+ return false;
1257
+ }
1150
1258
  }
1151
1259
  /**
1152
1260
  * What this machine and the control plane each believe about the vault.