claude-nomad 0.34.0 → 0.35.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 (70) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +31 -0
  3. package/dist/nomad.mjs +4843 -0
  4. package/package.json +9 -8
  5. package/src/color.ts +0 -81
  6. package/src/commands.adopt.ts +0 -183
  7. package/src/commands.doctor.check-schema.ts +0 -72
  8. package/src/commands.doctor.check-shared.scan.ts +0 -204
  9. package/src/commands.doctor.check-shared.ts +0 -178
  10. package/src/commands.doctor.checks.deps.ts +0 -97
  11. package/src/commands.doctor.checks.hooks.ts +0 -176
  12. package/src/commands.doctor.checks.pathmap.ts +0 -101
  13. package/src/commands.doctor.checks.repo.ts +0 -182
  14. package/src/commands.doctor.checks.repository.ts +0 -111
  15. package/src/commands.doctor.checks.settings.ts +0 -88
  16. package/src/commands.doctor.engine.ts +0 -90
  17. package/src/commands.doctor.format.ts +0 -22
  18. package/src/commands.doctor.gitleaks-version.ts +0 -132
  19. package/src/commands.doctor.mirror-actions.ts +0 -88
  20. package/src/commands.doctor.ts +0 -113
  21. package/src/commands.doctor.version.ts +0 -193
  22. package/src/commands.drop-session.git.ts +0 -81
  23. package/src/commands.drop-session.scrub-hint.ts +0 -74
  24. package/src/commands.drop-session.ts +0 -159
  25. package/src/commands.pull.ts +0 -169
  26. package/src/commands.push.allowlist.ts +0 -128
  27. package/src/commands.push.recovery.actions.ts +0 -175
  28. package/src/commands.push.recovery.drop.ts +0 -47
  29. package/src/commands.push.recovery.redact.ts +0 -113
  30. package/src/commands.push.recovery.seams.ts +0 -66
  31. package/src/commands.push.recovery.ts +0 -198
  32. package/src/commands.push.sections.ts +0 -171
  33. package/src/commands.push.ts +0 -218
  34. package/src/commands.redact.core.ts +0 -145
  35. package/src/commands.redact.ts +0 -187
  36. package/src/commands.update.ts +0 -30
  37. package/src/config.sharedDirs.guard.ts +0 -84
  38. package/src/config.ts +0 -211
  39. package/src/diff-lines.ts +0 -42
  40. package/src/diff.ts +0 -55
  41. package/src/extras-sync.core.ts +0 -96
  42. package/src/extras-sync.diff.ts +0 -40
  43. package/src/extras-sync.guards.ts +0 -29
  44. package/src/extras-sync.remap.ts +0 -138
  45. package/src/extras-sync.ts +0 -79
  46. package/src/gh-actions.ts +0 -137
  47. package/src/init.classify.ts +0 -87
  48. package/src/init.gh-onboard.ts +0 -139
  49. package/src/init.snapshot.ts +0 -56
  50. package/src/init.ts +0 -220
  51. package/src/links.ts +0 -132
  52. package/src/nomad.dispatch.ts +0 -177
  53. package/src/nomad.help.ts +0 -97
  54. package/src/nomad.ts +0 -214
  55. package/src/output-tree.ts +0 -91
  56. package/src/preview.ts +0 -126
  57. package/src/push-checks.ts +0 -176
  58. package/src/push-gitleaks.config.ts +0 -161
  59. package/src/push-gitleaks.scan.ts +0 -219
  60. package/src/push-gitleaks.ts +0 -208
  61. package/src/push-leak-verdict.ts +0 -166
  62. package/src/push-preview.ts +0 -160
  63. package/src/remap.ts +0 -229
  64. package/src/resume.ts +0 -160
  65. package/src/settings-keys.ts +0 -124
  66. package/src/summary.ts +0 -102
  67. package/src/utils.fs.ts +0 -152
  68. package/src/utils.json.ts +0 -55
  69. package/src/utils.lockfile.ts +0 -168
  70. package/src/utils.ts +0 -110
package/dist/nomad.mjs ADDED
@@ -0,0 +1,4843 @@
1
+ #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __commonJS = (cb, mod) => function __require() {
12
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
13
+ };
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
27
+ // If the importer is in node compatibility mode or this is not an ESM
28
+ // file that has been converted to a CommonJS file using a Babel-
29
+ // compatible transform (i.e. "__esModule" has not been set), then set
30
+ // "default" to the CommonJS "module.exports" for node compatibility.
31
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
32
+ mod
33
+ ));
34
+
35
+ // node_modules/picocolors/picocolors.js
36
+ var require_picocolors = __commonJS({
37
+ "node_modules/picocolors/picocolors.js"(exports, module) {
38
+ var p = process || {};
39
+ var argv = p.argv || [];
40
+ var env = p.env || {};
41
+ var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
42
+ var formatter = (open, close, replace = open) => (input) => {
43
+ let string = "" + input, index = string.indexOf(close, open.length);
44
+ return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
45
+ };
46
+ var replaceClose = (string, close, replace, index) => {
47
+ let result = "", cursor = 0;
48
+ do {
49
+ result += string.substring(cursor, index) + replace;
50
+ cursor = index + close.length;
51
+ index = string.indexOf(close, cursor);
52
+ } while (~index);
53
+ return result + string.substring(cursor);
54
+ };
55
+ var createColors = (enabled2 = isColorSupported) => {
56
+ let f = enabled2 ? formatter : () => String;
57
+ return {
58
+ isColorSupported: enabled2,
59
+ reset: f("\x1B[0m", "\x1B[0m"),
60
+ bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
61
+ dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
62
+ italic: f("\x1B[3m", "\x1B[23m"),
63
+ underline: f("\x1B[4m", "\x1B[24m"),
64
+ inverse: f("\x1B[7m", "\x1B[27m"),
65
+ hidden: f("\x1B[8m", "\x1B[28m"),
66
+ strikethrough: f("\x1B[9m", "\x1B[29m"),
67
+ black: f("\x1B[30m", "\x1B[39m"),
68
+ red: f("\x1B[31m", "\x1B[39m"),
69
+ green: f("\x1B[32m", "\x1B[39m"),
70
+ yellow: f("\x1B[33m", "\x1B[39m"),
71
+ blue: f("\x1B[34m", "\x1B[39m"),
72
+ magenta: f("\x1B[35m", "\x1B[39m"),
73
+ cyan: f("\x1B[36m", "\x1B[39m"),
74
+ white: f("\x1B[37m", "\x1B[39m"),
75
+ gray: f("\x1B[90m", "\x1B[39m"),
76
+ bgBlack: f("\x1B[40m", "\x1B[49m"),
77
+ bgRed: f("\x1B[41m", "\x1B[49m"),
78
+ bgGreen: f("\x1B[42m", "\x1B[49m"),
79
+ bgYellow: f("\x1B[43m", "\x1B[49m"),
80
+ bgBlue: f("\x1B[44m", "\x1B[49m"),
81
+ bgMagenta: f("\x1B[45m", "\x1B[49m"),
82
+ bgCyan: f("\x1B[46m", "\x1B[49m"),
83
+ bgWhite: f("\x1B[47m", "\x1B[49m"),
84
+ blackBright: f("\x1B[90m", "\x1B[39m"),
85
+ redBright: f("\x1B[91m", "\x1B[39m"),
86
+ greenBright: f("\x1B[92m", "\x1B[39m"),
87
+ yellowBright: f("\x1B[93m", "\x1B[39m"),
88
+ blueBright: f("\x1B[94m", "\x1B[39m"),
89
+ magentaBright: f("\x1B[95m", "\x1B[39m"),
90
+ cyanBright: f("\x1B[96m", "\x1B[39m"),
91
+ whiteBright: f("\x1B[97m", "\x1B[39m"),
92
+ bgBlackBright: f("\x1B[100m", "\x1B[49m"),
93
+ bgRedBright: f("\x1B[101m", "\x1B[49m"),
94
+ bgGreenBright: f("\x1B[102m", "\x1B[49m"),
95
+ bgYellowBright: f("\x1B[103m", "\x1B[49m"),
96
+ bgBlueBright: f("\x1B[104m", "\x1B[49m"),
97
+ bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
98
+ bgCyanBright: f("\x1B[106m", "\x1B[49m"),
99
+ bgWhiteBright: f("\x1B[107m", "\x1B[49m")
100
+ };
101
+ };
102
+ module.exports = createColors();
103
+ module.exports.createColors = createColors;
104
+ }
105
+ });
106
+
107
+ // src/color.ts
108
+ var import_picocolors, enabled, red, yellow, green, cyan, blue, dim, bold, wslNarrowPad, okGlyph, failGlyph, warnGlyph, infoGlyph;
109
+ var init_color = __esm({
110
+ "src/color.ts"() {
111
+ "use strict";
112
+ import_picocolors = __toESM(require_picocolors(), 1);
113
+ enabled = import_picocolors.default.isColorSupported;
114
+ red = (s) => enabled ? import_picocolors.default.red(s) : s;
115
+ yellow = (s) => enabled ? import_picocolors.default.yellow(s) : s;
116
+ green = (s) => enabled ? import_picocolors.default.green(s) : s;
117
+ cyan = (s) => enabled ? import_picocolors.default.cyan(s) : s;
118
+ blue = (s) => enabled ? import_picocolors.default.blue(s) : s;
119
+ dim = (s) => enabled ? import_picocolors.default.dim(s) : s;
120
+ bold = (s) => enabled ? import_picocolors.default.bold(s) : s;
121
+ wslNarrowPad = process.env.WSL_DISTRO_NAME ? " " : "";
122
+ okGlyph = `\u2713${wslNarrowPad}`;
123
+ failGlyph = `\u2717${wslNarrowPad}`;
124
+ warnGlyph = "\u26A0\uFE0E";
125
+ infoGlyph = "\u2139\uFE0E";
126
+ }
127
+ });
128
+
129
+ // src/utils.ts
130
+ import { execFileSync } from "node:child_process";
131
+ function gitOrFatal(args, context, cwd) {
132
+ try {
133
+ execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
134
+ } catch (err) {
135
+ const e = err;
136
+ if (e.stderr) process.stderr.write(e.stderr);
137
+ throw new NomadFatal(`${context} failed`);
138
+ }
139
+ }
140
+ var log, ok, warn, fail, NomadFatal, die, gitStatusPorcelainZ;
141
+ var init_utils = __esm({
142
+ "src/utils.ts"() {
143
+ "use strict";
144
+ init_color();
145
+ log = (msg) => console.log(`${dim(infoGlyph)} ${msg}`);
146
+ ok = (msg) => console.log(`${green(okGlyph)} ${msg}`);
147
+ warn = (msg) => {
148
+ console.error(`${yellow(warnGlyph)} ${msg}`);
149
+ };
150
+ fail = (msg) => {
151
+ console.error(`${red(failGlyph)} ${msg}`);
152
+ };
153
+ NomadFatal = class extends Error {
154
+ constructor(message) {
155
+ super(message);
156
+ this.name = "NomadFatal";
157
+ }
158
+ };
159
+ die = (msg) => {
160
+ throw new NomadFatal(msg);
161
+ };
162
+ gitStatusPorcelainZ = (cwd, opts = {}) => {
163
+ const args = ["status", "--porcelain=v1", "-z"];
164
+ if (opts.untrackedAll === true) args.push("--untracked-files=all");
165
+ return execFileSync("git", args, {
166
+ cwd,
167
+ stdio: ["ignore", "pipe", "pipe"]
168
+ }).toString();
169
+ };
170
+ }
171
+ });
172
+
173
+ // src/config.sharedDirs.guard.ts
174
+ function assertSafeLogical(logical) {
175
+ if (!SAFE_LOGICAL.test(logical) || logical === "." || logical === "..") {
176
+ throw new NomadFatal(
177
+ `invalid logical name in path-map.json: ${JSON.stringify(logical)} (must match [A-Za-z0-9._-]+; no path separators or '..')`
178
+ );
179
+ }
180
+ }
181
+ function isValidSharedDir(entry) {
182
+ if (typeof entry !== "string") return false;
183
+ if (!SAFE_SEGMENT.test(entry) || entry === "." || entry === "..") return false;
184
+ if (NEVER_SYNC.has(entry)) return false;
185
+ if (RESERVED_SHARED.has(entry)) return false;
186
+ return true;
187
+ }
188
+ var SAFE_LOGICAL, SAFE_SEGMENT, RESERVED_SHARED;
189
+ var init_config_sharedDirs_guard = __esm({
190
+ "src/config.sharedDirs.guard.ts"() {
191
+ "use strict";
192
+ init_config();
193
+ init_utils();
194
+ SAFE_LOGICAL = /^[A-Za-z0-9._-]+$/;
195
+ SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/;
196
+ RESERVED_SHARED = /* @__PURE__ */ new Set([
197
+ "settings.base.json",
198
+ "CLAUDE.md",
199
+ "agents",
200
+ "skills",
201
+ "commands",
202
+ "rules",
203
+ "my-statusline.cjs",
204
+ "hooks",
205
+ "hosts",
206
+ "path-map.json",
207
+ "extras",
208
+ "projects"
209
+ ]);
210
+ }
211
+ });
212
+
213
+ // src/settings-keys.ts
214
+ var SCHEMA_KEYS, APP_ONLY_KEYS, KNOWN_SETTINGS_KEYS;
215
+ var init_settings_keys = __esm({
216
+ "src/settings-keys.ts"() {
217
+ "use strict";
218
+ SCHEMA_KEYS = [
219
+ "$schema",
220
+ "agent",
221
+ "allowedChannelPlugins",
222
+ "allowedHttpHookUrls",
223
+ "allowedMcpServers",
224
+ "allowManagedHooksOnly",
225
+ "allowManagedMcpServersOnly",
226
+ "allowManagedPermissionRulesOnly",
227
+ "alwaysThinkingEnabled",
228
+ "apiKeyHelper",
229
+ "attribution",
230
+ "autoMemoryDirectory",
231
+ "autoMemoryEnabled",
232
+ "autoMode",
233
+ "autoUpdatesChannel",
234
+ "availableModels",
235
+ "awsAuthRefresh",
236
+ "awsCredentialExport",
237
+ "blockedMarketplaces",
238
+ "channelsEnabled",
239
+ "claudeMdExcludes",
240
+ "cleanupPeriodDays",
241
+ "companyAnnouncements",
242
+ "defaultShell",
243
+ "deniedMcpServers",
244
+ "disableAllHooks",
245
+ "disableDeepLinkRegistration",
246
+ "disabledMcpjsonServers",
247
+ "disableSkillShellExecution",
248
+ "effortLevel",
249
+ "enableAllProjectMcpServers",
250
+ "enabledMcpjsonServers",
251
+ "enabledPlugins",
252
+ "env",
253
+ "extraKnownMarketplaces",
254
+ "fastMode",
255
+ "fastModePerSessionOptIn",
256
+ "feedbackSurveyRate",
257
+ "fileSuggestion",
258
+ "forceLoginMethod",
259
+ "forceLoginOrgUUID",
260
+ "forceRemoteSettingsRefresh",
261
+ "hooks",
262
+ "httpHookAllowedEnvVars",
263
+ "includeCoAuthoredBy",
264
+ "includeGitInstructions",
265
+ "language",
266
+ "minimumVersion",
267
+ "model",
268
+ "modelOverrides",
269
+ "otelHeadersHelper",
270
+ "outputStyle",
271
+ "parentSettingsBehavior",
272
+ "permissions",
273
+ "plansDirectory",
274
+ "pluginConfigs",
275
+ "pluginTrustMessage",
276
+ "prefersReducedMotion",
277
+ "prUrlTemplate",
278
+ "respectGitignore",
279
+ "sandbox",
280
+ "showClearContextOnPlanAccept",
281
+ "showThinkingSummaries",
282
+ "showTurnDuration",
283
+ "skillOverrides",
284
+ "skipDangerousModePermissionPrompt",
285
+ "skippedMarketplaces",
286
+ "skippedPlugins",
287
+ "skipWebFetchPreflight",
288
+ "spinnerTipsEnabled",
289
+ "spinnerTipsOverride",
290
+ "spinnerVerbs",
291
+ "statusLine",
292
+ "strictKnownMarketplaces",
293
+ "strictPluginOnlyCustomization",
294
+ "subagentStatusLine",
295
+ "teammateMode",
296
+ "terminalProgressBarEnabled",
297
+ "tui",
298
+ "useAutoModeDuringPlan",
299
+ "viewMode",
300
+ "voiceEnabled",
301
+ "worktree",
302
+ "wslInheritsWindowsSettings"
303
+ ];
304
+ APP_ONLY_KEYS = [
305
+ "agentPushNotifEnabled",
306
+ "agents",
307
+ "apiKeyHelperTimeoutMs",
308
+ "awsLoginRefresh",
309
+ "awsRegion",
310
+ "awsRetryMode",
311
+ "disableNonEssentialModelCalls",
312
+ "enabledExperimentalFeatures",
313
+ "inputNeededNotifEnabled",
314
+ "installMethod",
315
+ "pluginGroups",
316
+ "pluginRepositoryEnabled",
317
+ "pluginsLocalConfig",
318
+ "proxy",
319
+ "skipAutoPermissionPrompt",
320
+ "statsig",
321
+ "subagents",
322
+ "theme"
323
+ ];
324
+ KNOWN_SETTINGS_KEYS = /* @__PURE__ */ new Set([...SCHEMA_KEYS, ...APP_ONLY_KEYS]);
325
+ }
326
+ });
327
+
328
+ // src/config.ts
329
+ import { homedir, hostname } from "node:os";
330
+ import { join, resolve } from "node:path";
331
+ function allSharedLinks(map) {
332
+ const extras = [];
333
+ for (const entry of map.sharedDirs ?? []) {
334
+ if (isValidSharedDir(entry)) {
335
+ extras.push(entry);
336
+ } else {
337
+ warn(
338
+ `sharedDirs entry ${JSON.stringify(entry)} is invalid (path separator, reserved name, or NEVER_SYNC); skipping`
339
+ );
340
+ }
341
+ }
342
+ return [...SHARED_LINKS, ...extras];
343
+ }
344
+ var HOME, CLAUDE_HOME, BACKUP_BASE, REPO_HOME, SETTINGS_SCHEMA_URL, GITLEAKS_PINNED_VERSION, HOST, SHARED_LINKS, SUPPORTED_EXTRAS, ALWAYS_NEVER_SYNC, NEVER_SYNC, PUSH_ALLOWED_STATIC;
345
+ var init_config = __esm({
346
+ "src/config.ts"() {
347
+ "use strict";
348
+ init_config_sharedDirs_guard();
349
+ init_utils();
350
+ init_settings_keys();
351
+ HOME = homedir();
352
+ CLAUDE_HOME = resolve(HOME, ".claude");
353
+ BACKUP_BASE = join(HOME, ".cache", "claude-nomad", "backup");
354
+ REPO_HOME = process.env.NOMAD_REPO || resolve(HOME, "claude-nomad");
355
+ SETTINGS_SCHEMA_URL = "https://json.schemastore.org/claude-code-settings.json";
356
+ GITLEAKS_PINNED_VERSION = "8.30.1";
357
+ HOST = (process.env.NOMAD_HOST || hostname()).toLowerCase();
358
+ SHARED_LINKS = [
359
+ "CLAUDE.md",
360
+ "agents",
361
+ "skills",
362
+ "commands",
363
+ "rules",
364
+ "my-statusline.cjs",
365
+ "hooks"
366
+ ];
367
+ SUPPORTED_EXTRAS = [".planning", "CLAUDE.md"];
368
+ ALWAYS_NEVER_SYNC = /* @__PURE__ */ new Set([
369
+ ".claude.json",
370
+ ".credentials.json",
371
+ "settings.local.json",
372
+ "history.jsonl",
373
+ "stats-cache.json"
374
+ ]);
375
+ NEVER_SYNC = /* @__PURE__ */ new Set([
376
+ ".claude.json",
377
+ ".credentials.json",
378
+ "history.jsonl",
379
+ "settings.local.json",
380
+ "stats-cache.json",
381
+ "todos",
382
+ "shell-snapshots",
383
+ "debug",
384
+ "file-history",
385
+ "plans",
386
+ "session-env",
387
+ "statsig",
388
+ "telemetry",
389
+ "ide",
390
+ // Host-local caches and runtime state (sharedDirs guard also rejects these).
391
+ "cache",
392
+ "backups",
393
+ "paste-cache",
394
+ "daemon",
395
+ "jobs",
396
+ "tasks",
397
+ "security",
398
+ "sessions"
399
+ ]);
400
+ PUSH_ALLOWED_STATIC = [
401
+ "shared/CLAUDE.md",
402
+ "shared/my-statusline.cjs",
403
+ "shared/settings.base.json",
404
+ "shared/agents/",
405
+ "shared/skills/",
406
+ "shared/commands/",
407
+ "shared/rules/",
408
+ "shared/.gitignore",
409
+ "shared/hooks/",
410
+ "hosts/",
411
+ "path-map.json",
412
+ ".gitleaksignore",
413
+ // written by nomad push Allow action (D-04)
414
+ ".gitleaks.overlay.toml"
415
+ // user-owned gitleaks allowlist overlay layered on the bundled base
416
+ ];
417
+ }
418
+ });
419
+
420
+ // src/utils.json.ts
421
+ import { readFileSync } from "node:fs";
422
+ function readJson(path) {
423
+ const data = JSON.parse(readFileSync(path, "utf8"));
424
+ return data;
425
+ }
426
+ function readPathMap(mapPath) {
427
+ try {
428
+ return readJson(mapPath);
429
+ } catch (err) {
430
+ const verb = err instanceof SyntaxError ? "parse" : "read";
431
+ throw new NomadFatal(`could not ${verb} path-map.json: ${err.message}`);
432
+ }
433
+ }
434
+ function deepMerge(target, source) {
435
+ const out = { ...target };
436
+ for (const [key, value] of Object.entries(source)) {
437
+ const existing = out[key];
438
+ const bothObjects = value !== null && typeof value === "object" && !Array.isArray(value) && existing !== null && typeof existing === "object" && !Array.isArray(existing);
439
+ out[key] = bothObjects ? deepMerge(existing, value) : value;
440
+ }
441
+ return out;
442
+ }
443
+ var encodePath;
444
+ var init_utils_json = __esm({
445
+ "src/utils.json.ts"() {
446
+ "use strict";
447
+ init_config();
448
+ init_utils();
449
+ encodePath = (absPath) => absPath.replaceAll("/", "-");
450
+ }
451
+ });
452
+
453
+ // src/utils.fs.ts
454
+ import {
455
+ closeSync,
456
+ cpSync,
457
+ existsSync,
458
+ fsyncSync,
459
+ lstatSync,
460
+ mkdirSync,
461
+ openSync,
462
+ renameSync,
463
+ statSync,
464
+ symlinkSync,
465
+ writeFileSync
466
+ } from "node:fs";
467
+ import { dirname, join as join2, relative } from "node:path";
468
+ function writeJsonAtomic(path, data) {
469
+ const mode = existsSync(path) ? statSync(path).mode & 511 : 384;
470
+ const tmp = `${path}.tmp.${process.pid}`;
471
+ const fd = openSync(tmp, "w", mode);
472
+ try {
473
+ writeFileSync(fd, JSON.stringify(data, null, 2) + "\n");
474
+ fsyncSync(fd);
475
+ } finally {
476
+ closeSync(fd);
477
+ }
478
+ renameSync(tmp, path);
479
+ const dirFd = openSync(dirname(path), "r");
480
+ try {
481
+ fsyncSync(dirFd);
482
+ } finally {
483
+ closeSync(dirFd);
484
+ }
485
+ }
486
+ function nowTimestamp() {
487
+ const d = /* @__PURE__ */ new Date();
488
+ const pad = (n) => n.toString().padStart(2, "0");
489
+ return d.getFullYear().toString() + pad(d.getMonth() + 1) + pad(d.getDate()) + "-" + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds());
490
+ }
491
+ function freshBackupTs(backupRoot) {
492
+ const base = nowTimestamp();
493
+ if (!existsSync(join2(backupRoot, base))) return base;
494
+ let n = 1;
495
+ while (existsSync(join2(backupRoot, `${base}-${n}`))) n++;
496
+ return `${base}-${n}`;
497
+ }
498
+ function ensureSymlink(linkPath, target) {
499
+ if (existsSync(linkPath)) {
500
+ if (lstatSync(linkPath).isSymbolicLink()) return;
501
+ die(`${linkPath} exists and is not a symlink. Move it aside first.`);
502
+ }
503
+ mkdirSync(dirname(linkPath), { recursive: true });
504
+ symlinkSync(target, linkPath);
505
+ log(`linked ${linkPath} -> ${target}`);
506
+ }
507
+ function backupBeforeWrite(absPath, ts) {
508
+ if (!existsSync(absPath)) return;
509
+ const rel = relative(CLAUDE_HOME, absPath);
510
+ if (rel.startsWith("..") || rel === "") return;
511
+ const backupRoot = join2(HOME, ".cache", "claude-nomad", "backup", ts);
512
+ const dst = join2(backupRoot, rel);
513
+ mkdirSync(dirname(dst), { recursive: true });
514
+ cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
515
+ }
516
+ function backupRepoWrite(absPath, ts, repoHome) {
517
+ if (!existsSync(absPath)) return;
518
+ const rel = relative(repoHome, absPath);
519
+ if (rel.startsWith("..") || rel === "") return;
520
+ const backupRoot = join2(HOME, ".cache", "claude-nomad", "backup", ts, "repo");
521
+ const dst = join2(backupRoot, rel);
522
+ mkdirSync(dirname(dst), { recursive: true });
523
+ cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
524
+ }
525
+ function backupExtrasWrite(absPath, ts, projectRoot) {
526
+ if (!existsSync(absPath)) return;
527
+ const rel = relative(projectRoot, absPath);
528
+ if (rel.startsWith("..") || rel === "") return;
529
+ const backupRoot = join2(HOME, ".cache", "claude-nomad", "backup", ts, "extras");
530
+ const dst = join2(backupRoot, encodePath(projectRoot), rel);
531
+ mkdirSync(dirname(dst), { recursive: true });
532
+ cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
533
+ }
534
+ var init_utils_fs = __esm({
535
+ "src/utils.fs.ts"() {
536
+ "use strict";
537
+ init_config();
538
+ init_utils_json();
539
+ init_utils();
540
+ }
541
+ });
542
+
543
+ // src/push-gitleaks.scan.ts
544
+ import { execFileSync as execFileSync2 } from "node:child_process";
545
+ import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync3 } from "node:fs";
546
+ import { homedir as homedir2 } from "node:os";
547
+ import { join as join9 } from "node:path";
548
+ import { fileURLToPath } from "node:url";
549
+ function resolveTomlPath() {
550
+ const repoToml = join9(REPO_HOME, ".gitleaks.toml");
551
+ if (existsSync8(repoToml)) return repoToml;
552
+ const bundled = fileURLToPath(new URL("../.gitleaks.toml", import.meta.url));
553
+ return existsSync8(bundled) ? bundled : null;
554
+ }
555
+ function readGitleaksReport(reportPath) {
556
+ try {
557
+ const raw = readFileSync2(reportPath, "utf8");
558
+ const parsed = JSON.parse(raw);
559
+ if (!Array.isArray(parsed)) return null;
560
+ return parsed;
561
+ } catch {
562
+ return null;
563
+ }
564
+ }
565
+ function scanStagedTree(repoDir, forwardStreams = false) {
566
+ const cacheDir = join9(homedir2(), ".cache", "claude-nomad");
567
+ mkdirSync2(cacheDir, { recursive: true });
568
+ const reportPath = join9(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
569
+ const { path: toml, tempPath } = resolveTomlConfig();
570
+ const args = [
571
+ "protect",
572
+ "--staged",
573
+ "--redact",
574
+ "-v",
575
+ "--report-format=json",
576
+ `--report-path=${reportPath}`
577
+ ];
578
+ if (toml !== null) args.push("--config", toml);
579
+ const opts = { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] };
580
+ try {
581
+ execFileSync2("git", ["init", "-q"], opts);
582
+ execFileSync2("git", ["add", "-A"], opts);
583
+ execFileSync2("gitleaks", args, opts);
584
+ return [];
585
+ } catch (err) {
586
+ const e = err;
587
+ if (e.code === "ENOENT") throw err;
588
+ const report = readGitleaksReport(reportPath);
589
+ if (forwardStreams && report === null) {
590
+ if (e.stderr) process.stderr.write(e.stderr);
591
+ if (e.stdout) process.stdout.write(e.stdout);
592
+ }
593
+ return report;
594
+ } finally {
595
+ if (tempPath !== null) rmSync3(tempPath, { recursive: true, force: true });
596
+ rmSync3(reportPath, { force: true });
597
+ }
598
+ }
599
+ function scanFile(filePath, forwardStreams = false) {
600
+ const cacheDir = join9(homedir2(), ".cache", "claude-nomad");
601
+ mkdirSync2(cacheDir, { recursive: true });
602
+ const reportPath = join9(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
603
+ const { path: toml, tempPath } = resolveTomlConfig();
604
+ const args = [
605
+ "detect",
606
+ "--no-git",
607
+ "--source",
608
+ filePath,
609
+ "--report-format=json",
610
+ `--report-path=${reportPath}`
611
+ ];
612
+ if (toml !== null) args.push("--config", toml);
613
+ const opts = { stdio: ["ignore", "pipe", "pipe"] };
614
+ try {
615
+ execFileSync2("gitleaks", args, opts);
616
+ return [];
617
+ } catch (err) {
618
+ const e = err;
619
+ if (e.code === "ENOENT") return null;
620
+ const report = readGitleaksReport(reportPath);
621
+ if (forwardStreams && report === null) {
622
+ if (e.stderr) process.stderr.write(e.stderr);
623
+ if (e.stdout) process.stdout.write(e.stdout);
624
+ }
625
+ return report;
626
+ } finally {
627
+ if (tempPath !== null) rmSync3(tempPath, { recursive: true, force: true });
628
+ rmSync3(reportPath, { force: true });
629
+ }
630
+ }
631
+ var init_push_gitleaks_scan = __esm({
632
+ "src/push-gitleaks.scan.ts"() {
633
+ "use strict";
634
+ init_config();
635
+ init_push_gitleaks_config();
636
+ init_utils_fs();
637
+ }
638
+ });
639
+
640
+ // src/push-gitleaks.config.ts
641
+ import { existsSync as existsSync9, mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
642
+ import { tmpdir } from "node:os";
643
+ import { join as join10 } from "node:path";
644
+ function buildOverlayTempConfig(overlayBody, bundled) {
645
+ const tempBody = `[extend]
646
+ path = ${JSON.stringify(bundled)}
647
+
648
+ ${overlayBody}`;
649
+ const tempPath = mkdtempSync(join10(tmpdir(), "nomad-gitleaks-cfg-"));
650
+ const configPath = join10(tempPath, "config.toml");
651
+ writeFileSync2(configPath, tempBody, { mode: 384, flag: "wx" });
652
+ return { configPath, tempPath };
653
+ }
654
+ function resolveTomlConfig() {
655
+ const overlayPath = join10(REPO_HOME, ".gitleaks.overlay.toml");
656
+ const repoToml = join10(REPO_HOME, ".gitleaks.toml");
657
+ const bundled = resolveTomlPath();
658
+ if (!existsSync9(overlayPath)) {
659
+ return { path: bundled, tempPath: null };
660
+ }
661
+ if (bundled === repoToml) {
662
+ warn(
663
+ ".gitleaks.overlay.toml ignored: REPO_HOME/.gitleaks.toml takes precedence (full manual control)"
664
+ );
665
+ return { path: bundled, tempPath: null };
666
+ }
667
+ if (bundled === null) {
668
+ return { path: null, tempPath: null };
669
+ }
670
+ try {
671
+ const overlayBody = readFileSync3(overlayPath, "utf8");
672
+ if (OVERLAY_EXTEND_RE.test(overlayBody)) {
673
+ throw new NomadFatal(
674
+ ".gitleaks.overlay.toml must not contain an [extend] block; it is generated automatically. Remove the [extend] section and retry."
675
+ );
676
+ }
677
+ const { configPath, tempPath } = buildOverlayTempConfig(overlayBody, bundled);
678
+ return { path: configPath, tempPath };
679
+ } catch (err) {
680
+ if (err instanceof NomadFatal) throw err;
681
+ warn(
682
+ `.gitleaks.overlay.toml merge failed (${err.message}); falling back to the bundled allowlist`
683
+ );
684
+ return { path: bundled, tempPath: null };
685
+ }
686
+ }
687
+ var OVERLAY_EXTEND_RE;
688
+ var init_push_gitleaks_config = __esm({
689
+ "src/push-gitleaks.config.ts"() {
690
+ "use strict";
691
+ init_config();
692
+ init_push_gitleaks_scan();
693
+ init_utils();
694
+ OVERLAY_EXTEND_RE = /^\s*(?:\[\s*extend\s*\]|extend\s*[.=])/m;
695
+ }
696
+ });
697
+
698
+ // src/push-checks.ts
699
+ import { execFileSync as execFileSync3 } from "node:child_process";
700
+ import { readdirSync as readdirSync3, rmSync as rmSync4 } from "node:fs";
701
+ import { homedir as homedir3, platform } from "node:os";
702
+ import { join as join11 } from "node:path";
703
+ function gitleaksInstallHint() {
704
+ const head = "gitleaks not on PATH (required for nomad push). Install:";
705
+ const plat = platform();
706
+ if (plat === "darwin") {
707
+ return `${head}
708
+ brew install gitleaks`;
709
+ }
710
+ if (plat === "linux") {
711
+ const archMap = { x64: "x64", arm64: "arm64", arm: "armv7" };
712
+ const arch = archMap[process.arch];
713
+ const lines = [
714
+ head,
715
+ arch ? ` 1. Download the linux_${arch} tarball: https://github.com/gitleaks/gitleaks/releases` : ` 1. Download the linux artifact for arch=${process.arch}: https://github.com/gitleaks/gitleaks/releases`,
716
+ " 2. Install (replace TARBALL with the path to your download):",
717
+ " mkdir -p ~/.local/bin",
718
+ " tar -xzf TARBALL -C ~/.local/bin gitleaks",
719
+ " chmod +x ~/.local/bin/gitleaks",
720
+ " ~/.local/bin/gitleaks version # verify"
721
+ ];
722
+ const localBin = `${homedir3()}/.local/bin`;
723
+ const paths = (process.env.PATH ?? "").split(":");
724
+ if (!paths.includes(localBin)) {
725
+ lines.push(
726
+ " 3. ~/.local/bin is not on PATH; add to your shell rc:",
727
+ ' export PATH="$HOME/.local/bin:$PATH"'
728
+ );
729
+ }
730
+ return lines.join("\n");
731
+ }
732
+ return `${head}
733
+ See https://github.com/gitleaks/gitleaks/releases`;
734
+ }
735
+ function findGitlinks(dir) {
736
+ const hits = [];
737
+ function walk(current) {
738
+ let entries;
739
+ try {
740
+ entries = readdirSync3(current, { withFileTypes: true });
741
+ } catch {
742
+ return;
743
+ }
744
+ for (const e of entries) {
745
+ const p = join11(current, e.name);
746
+ if (e.name === ".git") {
747
+ hits.push(p);
748
+ continue;
749
+ }
750
+ if (e.isDirectory()) walk(p);
751
+ }
752
+ }
753
+ walk(dir);
754
+ return hits;
755
+ }
756
+ function probeGitleaks() {
757
+ const { path: toml, tempPath } = resolveTomlConfig();
758
+ const args = ["version"];
759
+ if (toml !== null) args.push("--config", toml);
760
+ try {
761
+ return execFileSync3("gitleaks", args, { stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
762
+ } catch (err) {
763
+ const e = err;
764
+ if (e.code === "ENOENT") throw new NomadFatal(gitleaksInstallHint());
765
+ throw new NomadFatal(`gitleaks --version failed: ${e.message}`);
766
+ } finally {
767
+ if (tempPath !== null) rmSync4(tempPath, { recursive: true, force: true });
768
+ }
769
+ }
770
+ function rebaseBeforePush() {
771
+ try {
772
+ execFileSync3("git", ["pull", "--rebase", "--autostash"], {
773
+ cwd: REPO_HOME,
774
+ stdio: ["ignore", "pipe", "pipe"]
775
+ });
776
+ } catch (err) {
777
+ const e = err;
778
+ if (e.stderr) process.stderr.write(e.stderr);
779
+ throw new NomadFatal(
780
+ 'rebase failed; if a conflict was reported, resolve it in ~/claude-nomad/ and run "git rebase --continue" (or "git rebase --abort" to give up). Re-run nomad push after resolution.'
781
+ );
782
+ }
783
+ }
784
+ var init_push_checks = __esm({
785
+ "src/push-checks.ts"() {
786
+ "use strict";
787
+ init_config();
788
+ init_push_gitleaks_config();
789
+ init_utils();
790
+ }
791
+ });
792
+
793
+ // src/push-gitleaks.ts
794
+ function partitionFindings(findings) {
795
+ const bySession = /* @__PURE__ */ new Map();
796
+ const other = [];
797
+ for (const f of findings) {
798
+ const m = SESSION_PATH.exec(f.File);
799
+ if (m === null) {
800
+ other.push(f);
801
+ continue;
802
+ }
803
+ const sid = m[1];
804
+ if (sid === void 0) continue;
805
+ let counts = bySession.get(sid);
806
+ if (counts === void 0) {
807
+ counts = /* @__PURE__ */ new Map();
808
+ bySession.set(sid, counts);
809
+ }
810
+ counts.set(f.RuleID, (counts.get(f.RuleID) ?? 0) + 1);
811
+ }
812
+ return { bySession, other };
813
+ }
814
+ function formatOtherFinding(f) {
815
+ const loc = Number.isInteger(f.StartLine) && f.StartLine > 0 ? `:${f.StartLine}` : "";
816
+ return ` ${f.File}${loc} ${f.RuleID}`;
817
+ }
818
+ function otherFindingHint(f) {
819
+ const m = SUBAGENT_SESSION_PATH.exec(f.File);
820
+ if (m !== null) {
821
+ const sid = m[1];
822
+ if (sid === void 0) return " Review with: git diff --cached, then unstage manually.";
823
+ return ` Recover with: nomad drop-session ${sid} (or: nomad redact ${sid})`;
824
+ }
825
+ return " Review with: git diff --cached, then unstage manually.";
826
+ }
827
+ function buildSessionAwareFatal(bySession, other) {
828
+ if (bySession.size === 0) return LEGACY_FATAL;
829
+ const lines = [];
830
+ lines.push(
831
+ `gitleaks detected secrets in ${bySession.size} session transcript(s).`,
832
+ "nomad drop-session also clears each session's sibling subagent transcript directory."
833
+ );
834
+ for (const [sid, counts] of bySession) {
835
+ const summary = [...counts.entries()].map(([rule, n]) => `${rule} (${n})`).join(", ");
836
+ lines.push("", `Session ${sid}:`, ` ${summary}`, ` Recover with: nomad drop-session ${sid}`);
837
+ }
838
+ if (other.length > 0) {
839
+ for (const f of other) {
840
+ lines.push("", "Also found:", formatOtherFinding(f), otherFindingHint(f));
841
+ }
842
+ }
843
+ lines.push("", "After recovery, re-run nomad push.");
844
+ return lines.join("\n");
845
+ }
846
+ var SESSION_PATH, SUBAGENT_SESSION_PATH, LEGACY_FATAL;
847
+ var init_push_gitleaks = __esm({
848
+ "src/push-gitleaks.ts"() {
849
+ "use strict";
850
+ init_config();
851
+ init_push_checks();
852
+ init_push_gitleaks_scan();
853
+ init_utils();
854
+ init_push_gitleaks_scan();
855
+ SESSION_PATH = /^shared\/projects\/[^/]+\/([^/]+)\.jsonl$/;
856
+ SUBAGENT_SESSION_PATH = /^shared\/projects\/[^/]+\/([^/]+)\//;
857
+ LEGACY_FATAL = "gitleaks detected secrets; review staged changes with git diff --cached and unstage offending files before retry";
858
+ }
859
+ });
860
+
861
+ // src/push-leak-verdict.ts
862
+ var push_leak_verdict_exports = {};
863
+ __export(push_leak_verdict_exports, {
864
+ failRow: () => failRow,
865
+ leakVerdictRow: () => leakVerdictRow,
866
+ noLeaksRow: () => noLeaksRow,
867
+ scanPushVerdict: () => scanPushVerdict,
868
+ verdictFromFindings: () => verdictFromFindings,
869
+ verdictScanError: () => verdictScanError
870
+ });
871
+ function leakVerdictRow(findings) {
872
+ const { bySession } = partitionFindings(findings);
873
+ const n = bySession.size > 0 ? bySession.size : findings.length;
874
+ return failRow(`gitleaks detected secrets in ${n} session transcript(s)`);
875
+ }
876
+ function leakFound(findings) {
877
+ const { bySession, other } = partitionFindings(findings);
878
+ return {
879
+ leak: true,
880
+ verdictRow: leakVerdictRow(findings),
881
+ recovery: buildSessionAwareFatal(bySession, other),
882
+ findings
883
+ };
884
+ }
885
+ function verdictFromFindings(findings) {
886
+ if (findings === null) {
887
+ process.exitCode = 1;
888
+ return {
889
+ leak: false,
890
+ verdictRow: failRow("scan failed, no parseable report"),
891
+ recovery: null,
892
+ findings: []
893
+ };
894
+ }
895
+ if (findings.length === 0) {
896
+ return { leak: false, verdictRow: noLeaksRow(), recovery: null, findings: [] };
897
+ }
898
+ process.exitCode = 1;
899
+ return leakFound(findings);
900
+ }
901
+ function verdictScanError(text) {
902
+ process.exitCode = 1;
903
+ return { leak: false, verdictRow: failRow(text), recovery: null, findings: [] };
904
+ }
905
+ function scanPushVerdict() {
906
+ let findings;
907
+ try {
908
+ findings = scanStagedTree(REPO_HOME, true);
909
+ } catch (err) {
910
+ if (err.code === "ENOENT") {
911
+ return {
912
+ leak: true,
913
+ verdictRow: failRow("gitleaks not found"),
914
+ recovery: gitleaksInstallHint(),
915
+ findings: []
916
+ };
917
+ }
918
+ throw err;
919
+ }
920
+ if (findings === null) {
921
+ return {
922
+ leak: true,
923
+ verdictRow: failRow("scan failed, no parseable report"),
924
+ recovery: "gitleaks scan failed: no parseable JSON report. Review the gitleaks output above.",
925
+ findings: []
926
+ };
927
+ }
928
+ if (findings.length === 0) {
929
+ return { leak: false, verdictRow: noLeaksRow(), recovery: null, findings: [] };
930
+ }
931
+ return leakFound(findings);
932
+ }
933
+ var noLeaksRow, failRow;
934
+ var init_push_leak_verdict = __esm({
935
+ "src/push-leak-verdict.ts"() {
936
+ "use strict";
937
+ init_color();
938
+ init_config();
939
+ init_push_checks();
940
+ init_push_gitleaks();
941
+ noLeaksRow = () => `${green(okGlyph)} no leaks`;
942
+ failRow = (text) => `${red(failGlyph)} ${text}`;
943
+ }
944
+ });
945
+
946
+ // src/commands.adopt.ts
947
+ init_config();
948
+ init_config_sharedDirs_guard();
949
+ init_utils();
950
+ init_utils_fs();
951
+ init_utils_json();
952
+ import { cpSync as cpSync2, existsSync as existsSync2, lstatSync as lstatSync2, rmSync } from "node:fs";
953
+ import { join as join3 } from "node:path";
954
+ var ADOPT_PUSH_HINT = "run `nomad push` to share with other hosts";
955
+ function lexists(p) {
956
+ try {
957
+ lstatSync2(p);
958
+ return true;
959
+ } catch {
960
+ return false;
961
+ }
962
+ }
963
+ function readMapIfPresent(repoHome) {
964
+ const mapPath = join3(repoHome, "path-map.json");
965
+ return existsSync2(mapPath) ? readPathMap(mapPath) : { projects: {} };
966
+ }
967
+ function isConfiguredTarget(name, map) {
968
+ return SHARED_LINKS.includes(name) || (map.sharedDirs?.includes(name) ?? false);
969
+ }
970
+ function isValidAdoptName(name) {
971
+ if (SHARED_LINKS.includes(name)) return true;
972
+ return isValidSharedDir(name);
973
+ }
974
+ function performAdoptMove(name, linkPath, sharedTarget) {
975
+ const backupBase = join3(HOME, ".cache", "claude-nomad", "backup");
976
+ const ts = freshBackupTs(backupBase);
977
+ backupBeforeWrite(linkPath, ts);
978
+ cpSync2(linkPath, sharedTarget, { recursive: true, force: true, preserveTimestamps: true });
979
+ rmSync(linkPath, { recursive: true, force: true });
980
+ ensureSymlink(linkPath, sharedTarget);
981
+ const rel = join3("shared", name);
982
+ gitOrFatal(["add", "--", rel], `git add shared/${name}`, REPO_HOME);
983
+ log(`adopted ${name}; ${ADOPT_PUSH_HINT}`);
984
+ }
985
+ function cmdAdopt(name, opts = {}) {
986
+ const dryRun = opts.dryRun === true;
987
+ if (!isValidAdoptName(name)) {
988
+ fail(`invalid name: ${JSON.stringify(name)}`);
989
+ process.exit(1);
990
+ }
991
+ const map = readMapIfPresent(REPO_HOME);
992
+ if (!isConfiguredTarget(name, map)) {
993
+ fail(
994
+ `${name}: not a configured shared target. Add it to sharedDirs in path-map.json first, then re-run adopt.`
995
+ );
996
+ process.exit(1);
997
+ }
998
+ const linkPath = join3(CLAUDE_HOME, name);
999
+ const sharedTarget = join3(REPO_HOME, "shared", name);
1000
+ if (!existsSync2(linkPath)) {
1001
+ log(`${name}: nothing to adopt (not present in ~/.claude/)`);
1002
+ return;
1003
+ }
1004
+ if (lstatSync2(linkPath).isSymbolicLink()) {
1005
+ log(`${name}: already adopted (already a symlink)`);
1006
+ return;
1007
+ }
1008
+ if (lexists(sharedTarget)) {
1009
+ fail(`${name}: shared/${name} already exists; would clobber. Remove it first.`);
1010
+ process.exit(1);
1011
+ }
1012
+ if (dryRun) {
1013
+ const backupBase = join3(HOME, ".cache", "claude-nomad", "backup");
1014
+ const ts = freshBackupTs(backupBase);
1015
+ log(`would backup: ${linkPath} -> backup/${ts}/${name}`);
1016
+ log(`would move: ${linkPath} -> shared/${name}`);
1017
+ log(`would stage: shared/${name}`);
1018
+ return;
1019
+ }
1020
+ try {
1021
+ performAdoptMove(name, linkPath, sharedTarget);
1022
+ } catch (err) {
1023
+ if (!(err instanceof NomadFatal)) throw err;
1024
+ fail(err.message);
1025
+ process.exitCode = 1;
1026
+ }
1027
+ }
1028
+
1029
+ // src/commands.clean.ts
1030
+ init_config();
1031
+ init_utils();
1032
+ import { existsSync as existsSync3, lstatSync as lstatSync3, readdirSync, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
1033
+ import { join as join4 } from "node:path";
1034
+ var TS_SHAPE = /^\d{8}-\d{6}(-\d+)?$/;
1035
+ var DURATION_RE = /^(\d+)([dhm])$/;
1036
+ var UNIT_MS = { d: 864e5, h: 36e5, m: 6e4 };
1037
+ var CLEAN_DEFAULT_OLDER_THAN_MS = 14 * 24 * 60 * 60 * 1e3;
1038
+ function isTsDir(name) {
1039
+ return TS_SHAPE.test(name);
1040
+ }
1041
+ function parseDuration(s) {
1042
+ const m = DURATION_RE.exec(s);
1043
+ if (!m) return null;
1044
+ return Number(m[1]) * UNIT_MS[m[2]];
1045
+ }
1046
+ function listBackupDirs(backupBase) {
1047
+ if (!existsSync3(backupBase)) return [];
1048
+ return readdirSync(backupBase).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join4(backupBase, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
1049
+ }
1050
+ function prunableByAge(dirs, olderThanMs, nowMs) {
1051
+ return dirs.filter((d) => nowMs - d.mtimeMs > olderThanMs).map((d) => d.name);
1052
+ }
1053
+ function prunableByCount(dirs, keep) {
1054
+ return dirs.slice(keep).map((d) => d.name);
1055
+ }
1056
+ function safeDelete(backupBase, name) {
1057
+ if (!isTsDir(name)) return;
1058
+ const full = join4(backupBase, name);
1059
+ const st = lstatSync3(full, { throwIfNoEntry: false });
1060
+ if (!st || st.isSymbolicLink() || !st.isDirectory()) return;
1061
+ rmSync2(full, { recursive: true, force: true });
1062
+ }
1063
+ function resolveTargets(dirs, olderThanMs, keep) {
1064
+ if (keep !== void 0) return prunableByCount(dirs, keep);
1065
+ return prunableByAge(dirs, olderThanMs, Date.now());
1066
+ }
1067
+ function cmdClean(opts, backupBase = BACKUP_BASE) {
1068
+ const { dryRun, olderThan, keep } = opts;
1069
+ if (olderThan !== void 0 && keep !== void 0) {
1070
+ fail("--older-than and --keep are mutually exclusive");
1071
+ process.exit(1);
1072
+ }
1073
+ let olderThanMs = CLEAN_DEFAULT_OLDER_THAN_MS;
1074
+ if (olderThan !== void 0) {
1075
+ const parsed = parseDuration(olderThan);
1076
+ if (parsed === null) {
1077
+ fail(`invalid --older-than duration: ${olderThan} (expected e.g. 14d, 24h, 30m)`);
1078
+ process.exit(1);
1079
+ }
1080
+ olderThanMs = parsed;
1081
+ }
1082
+ const dirs = listBackupDirs(backupBase);
1083
+ const targets = resolveTargets(dirs, olderThanMs, keep);
1084
+ if (dryRun) {
1085
+ for (const name of targets) log(`would remove ${name}`);
1086
+ log(`dry-run: ${targets.length} backup(s) would be removed`);
1087
+ return;
1088
+ }
1089
+ for (const name of targets) safeDelete(backupBase, name);
1090
+ log(`removed ${targets.length} backup(s)`);
1091
+ }
1092
+
1093
+ // src/commands.doctor.ts
1094
+ import { existsSync as existsSync18 } from "node:fs";
1095
+ import { join as join21 } from "node:path";
1096
+
1097
+ // src/commands.doctor.checks.repo.ts
1098
+ init_color();
1099
+ init_config();
1100
+ import { existsSync as existsSync5, lstatSync as lstatSync4, statSync as statSync3 } from "node:fs";
1101
+ import { join as join6 } from "node:path";
1102
+
1103
+ // src/commands.doctor.format.ts
1104
+ init_color();
1105
+
1106
+ // src/output-tree.ts
1107
+ init_color();
1108
+ var FAIL_GLYPH_BARE = "\u2717";
1109
+ function section(header) {
1110
+ return { header, items: [] };
1111
+ }
1112
+ function addItem(s, text) {
1113
+ s.items.push(text);
1114
+ }
1115
+ function sectionFailed(s) {
1116
+ return s.items.some((line) => line.includes(failGlyph));
1117
+ }
1118
+ function renderSection(s) {
1119
+ const header = sectionFailed(s) ? `${red(FAIL_GLYPH_BARE)} ${s.header}` : s.header;
1120
+ console.log(header);
1121
+ const lastContent = s.items.reduce((acc, item, j) => item !== "" ? j : acc, -1);
1122
+ for (let j = 0; j < s.items.length; j++) {
1123
+ if (s.items[j] === "") console.log("");
1124
+ else console.log(`${j === lastContent ? " \u2514 " : " \u251C "}${s.items[j]}`);
1125
+ }
1126
+ }
1127
+ function renderTree(sections) {
1128
+ const visible = sections.filter((s) => s.items.length > 0);
1129
+ for (let i = 0; i < visible.length; i++) {
1130
+ if (i > 0) console.log("");
1131
+ renderSection(visible[i]);
1132
+ }
1133
+ }
1134
+ var renderDoctor = renderTree;
1135
+
1136
+ // src/commands.doctor.format.ts
1137
+ init_utils_json();
1138
+ function readJsonSafe(path, label, section2) {
1139
+ try {
1140
+ return readJson(path);
1141
+ } catch (err) {
1142
+ addItem(section2, `${red(failGlyph)} ${label} malformed JSON: ${err.message}`);
1143
+ process.exitCode = 1;
1144
+ return null;
1145
+ }
1146
+ }
1147
+
1148
+ // src/init.classify.ts
1149
+ init_config();
1150
+ init_utils_json();
1151
+ import { existsSync as existsSync4 } from "node:fs";
1152
+ import { join as join5 } from "node:path";
1153
+ function classifyRepoState(repoHome, host) {
1154
+ const basePath = join5(repoHome, "shared", "settings.base.json");
1155
+ const mapPath = join5(repoHome, "path-map.json");
1156
+ const hostPath = join5(repoHome, "hosts", `${host}.json`);
1157
+ const hasBase = existsSync4(basePath);
1158
+ const hasMap = existsSync4(mapPath);
1159
+ const hasHost = existsSync4(hostPath);
1160
+ let mapEntryCount = 0;
1161
+ if (hasMap) {
1162
+ try {
1163
+ const map = readJson(mapPath);
1164
+ mapEntryCount = Object.keys(map.projects).length;
1165
+ } catch {
1166
+ mapEntryCount = 0;
1167
+ }
1168
+ }
1169
+ if (!hasBase && mapEntryCount === 0) return "empty";
1170
+ if (hasBase && mapEntryCount > 0 && hasHost) return "populated";
1171
+ return "partial";
1172
+ }
1173
+ function reasonForPartial(repoHome, host) {
1174
+ const basePath = join5(repoHome, "shared", "settings.base.json");
1175
+ const mapPath = join5(repoHome, "path-map.json");
1176
+ const hostPath = join5(repoHome, "hosts", `${host}.json`);
1177
+ if (!existsSync4(basePath)) return "- shared/settings.base.json missing";
1178
+ if (!existsSync4(mapPath)) return "- path-map.json missing";
1179
+ let mapEntryCount;
1180
+ try {
1181
+ const map = readJson(mapPath);
1182
+ mapEntryCount = Object.keys(map.projects).length;
1183
+ } catch {
1184
+ mapEntryCount = 0;
1185
+ }
1186
+ if (mapEntryCount === 0) return "- path-map.json.projects has no entries";
1187
+ if (!existsSync4(hostPath)) return `- hosts/${host}.json missing`;
1188
+ return "- partial state (unknown gap)";
1189
+ }
1190
+
1191
+ // src/commands.doctor.checks.repo.ts
1192
+ function isOverrideActive() {
1193
+ return Boolean(process.env.NOMAD_REPO);
1194
+ }
1195
+ function reportHostAndPaths(section2) {
1196
+ addItem(section2, `${dim(infoGlyph)} host: ${cyan(HOST)}`);
1197
+ addItem(
1198
+ section2,
1199
+ `${existsSync5(REPO_HOME) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(REPO_HOME)}`
1200
+ );
1201
+ addItem(
1202
+ section2,
1203
+ `${existsSync5(CLAUDE_HOME) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(CLAUDE_HOME)}`
1204
+ );
1205
+ }
1206
+ function reportRepoState(section2) {
1207
+ const state = classifyRepoState(REPO_HOME, HOST);
1208
+ const overrideLabel = isOverrideActive() ? " (NOMAD_REPO)" : "";
1209
+ if (state === "populated") {
1210
+ addItem(section2, `${green(okGlyph)} repo state: populated${overrideLabel}`);
1211
+ } else if (state === "partial") {
1212
+ addItem(
1213
+ section2,
1214
+ `${yellow(warnGlyph)} repo state: partial ${reasonForPartial(REPO_HOME, HOST)}${overrideLabel}`
1215
+ );
1216
+ } else {
1217
+ addItem(
1218
+ section2,
1219
+ `${red(failGlyph)} repo state: empty - run 'nomad init' to scaffold${overrideLabel}`
1220
+ );
1221
+ process.exitCode = 1;
1222
+ }
1223
+ }
1224
+ function repoHasSharedSource(name) {
1225
+ return existsSync5(join6(REPO_HOME, "shared", name));
1226
+ }
1227
+ function classifySharedLink(name, p) {
1228
+ let stat;
1229
+ try {
1230
+ stat = lstatSync4(p);
1231
+ } catch (err) {
1232
+ const code = err.code;
1233
+ if (code === "ENOENT") {
1234
+ return repoHasSharedSource(name) ? {
1235
+ line: `${yellow(warnGlyph)} ${name}: missing (run \`nomad pull\` to restore)`,
1236
+ fail: false
1237
+ } : { line: `${dim(infoGlyph)} ${name}: not synced (nothing in shared/)`, fail: false };
1238
+ }
1239
+ return { line: `${red(failGlyph)} ${name}: could not stat (${String(code)})`, fail: true };
1240
+ }
1241
+ if (!stat.isSymbolicLink()) {
1242
+ return {
1243
+ line: `${red(failGlyph)} ${name}: NOT a symlink (blocks sync); run \`nomad adopt ${name}\` to fix`,
1244
+ fail: true
1245
+ };
1246
+ }
1247
+ return classifySymlinkTarget(name, p);
1248
+ }
1249
+ function classifySymlinkTarget(name, p) {
1250
+ try {
1251
+ statSync3(p);
1252
+ return { line: `${green(okGlyph)} ${name}: symlink`, fail: false };
1253
+ } catch (err) {
1254
+ const code = err.code;
1255
+ if (code === "ENOENT") {
1256
+ return repoHasSharedSource(name) ? {
1257
+ line: `${yellow(warnGlyph)} ${name}: broken symlink (target missing, run \`nomad pull\`)`,
1258
+ fail: false
1259
+ } : {
1260
+ line: `${dim(infoGlyph)} ${name}: stale symlink (no longer in shared/, safe to remove)`,
1261
+ fail: false
1262
+ };
1263
+ }
1264
+ return {
1265
+ line: `${yellow(warnGlyph)} ${name}: symlink target unreadable (${String(code)})`,
1266
+ fail: false
1267
+ };
1268
+ }
1269
+ }
1270
+ function reportSharedLinks(section2, map) {
1271
+ for (const name of allSharedLinks(map)) {
1272
+ const p = join6(CLAUDE_HOME, name);
1273
+ const { line, fail: fail2 } = classifySharedLink(name, p);
1274
+ addItem(section2, line);
1275
+ if (fail2) process.exitCode = 1;
1276
+ }
1277
+ }
1278
+
1279
+ // src/commands.doctor.checks.settings.ts
1280
+ init_color();
1281
+ init_config();
1282
+ import { existsSync as existsSync6, readdirSync as readdirSync2 } from "node:fs";
1283
+ import { join as join7 } from "node:path";
1284
+ function loadBaseSettings(section2) {
1285
+ const basePath = join7(REPO_HOME, "shared", "settings.base.json");
1286
+ if (!existsSync6(basePath)) {
1287
+ addItem(section2, `${red(failGlyph)} shared/settings.base.json missing at ${blue(basePath)}`);
1288
+ process.exitCode = 1;
1289
+ return null;
1290
+ }
1291
+ return readJsonSafe(basePath, basePath, section2);
1292
+ }
1293
+ function loadAndReportSettings(section2) {
1294
+ const settingsPath = join7(CLAUDE_HOME, "settings.json");
1295
+ if (!existsSync6(settingsPath)) return null;
1296
+ const settings = readJsonSafe(settingsPath, settingsPath, section2);
1297
+ if (settings === null) return null;
1298
+ const unknownKeys = Object.keys(settings).filter((k) => !KNOWN_SETTINGS_KEYS.has(k));
1299
+ if (unknownKeys.length > 0) {
1300
+ addItem(
1301
+ section2,
1302
+ `${yellow(warnGlyph)} settings.json has unknown keys (schema drift?): ${unknownKeys.join(", ")}`
1303
+ );
1304
+ } else {
1305
+ addItem(section2, `${green(okGlyph)} settings.json schema: known keys only`);
1306
+ }
1307
+ return settings;
1308
+ }
1309
+ function reportHostOverrides(section2, base, settings) {
1310
+ const hostFile = join7(REPO_HOME, "hosts", `${HOST}.json`);
1311
+ let drift = [];
1312
+ if (base !== null && settings !== null) {
1313
+ const baseKeys = new Set(Object.keys(base));
1314
+ drift = Object.keys(settings).filter((k) => !baseKeys.has(k));
1315
+ }
1316
+ if (existsSync6(hostFile)) {
1317
+ if (readJsonSafe(hostFile, hostFile, section2) !== null) {
1318
+ addItem(section2, `${green(okGlyph)} host overrides: ${blue(hostFile)}`);
1319
+ }
1320
+ } else if (drift.length > 0) {
1321
+ addItem(
1322
+ section2,
1323
+ `${red(failGlyph)} no hosts/${HOST}.json AND settings.json has unbased keys ${JSON.stringify(drift)}`
1324
+ );
1325
+ const hostsDir = join7(REPO_HOME, "hosts");
1326
+ if (existsSync6(hostsDir)) {
1327
+ const cands = readdirSync2(hostsDir).filter((f) => f.endsWith(".json"));
1328
+ if (cands.length > 0) addItem(section2, `${dim(infoGlyph)} candidates: ${cands.join(", ")}`);
1329
+ }
1330
+ process.exitCode = 1;
1331
+ } else {
1332
+ addItem(
1333
+ section2,
1334
+ `${green(okGlyph)} host overrides: none (base-only is fine, no settings drift)`
1335
+ );
1336
+ }
1337
+ }
1338
+
1339
+ // src/commands.doctor.checks.pathmap.ts
1340
+ init_color();
1341
+ init_config();
1342
+ import { existsSync as existsSync7 } from "node:fs";
1343
+ import { join as join8 } from "node:path";
1344
+ init_utils_json();
1345
+ function reportMappedProjects(section2, map) {
1346
+ const mapped = Object.entries(map.projects).filter(([, hosts]) => hosts[HOST]);
1347
+ addItem(
1348
+ section2,
1349
+ `${dim(infoGlyph)} mapped projects for ${cyan(HOST)}: ${dim(String(mapped.length))}`
1350
+ );
1351
+ for (const [name, hosts] of mapped) {
1352
+ addItem(section2, ` ${name} -> ${blue(hosts[HOST])}`);
1353
+ }
1354
+ }
1355
+ function reportPathCollisions(section2, map) {
1356
+ const seen = /* @__PURE__ */ new Map();
1357
+ let collisionCount = 0;
1358
+ for (const hosts of Object.values(map.projects)) {
1359
+ for (const abspath of Object.values(hosts)) {
1360
+ if (!abspath || abspath === "TBD") continue;
1361
+ const encoded = encodePath(abspath);
1362
+ const prior = seen.get(encoded);
1363
+ if (prior !== void 0 && prior !== abspath) {
1364
+ addItem(
1365
+ section2,
1366
+ `${red(failGlyph)} path-encoding collision: ${prior} and ${abspath} both encode to ${encoded}`
1367
+ );
1368
+ collisionCount++;
1369
+ } else {
1370
+ seen.set(encoded, abspath);
1371
+ }
1372
+ }
1373
+ }
1374
+ if (collisionCount > 0) process.exitCode = 1;
1375
+ else addItem(section2, `${green(okGlyph)} path-encoding: no collisions`);
1376
+ }
1377
+ function reportPathMap(section2) {
1378
+ const mapPath = join8(REPO_HOME, "path-map.json");
1379
+ if (!existsSync7(mapPath)) {
1380
+ addItem(section2, `${red(failGlyph)} path-map.json missing at ${blue(mapPath)}`);
1381
+ process.exitCode = 1;
1382
+ return;
1383
+ }
1384
+ const map = readJsonSafe(mapPath, mapPath, section2);
1385
+ if (map === null) return;
1386
+ const projects = map.projects;
1387
+ if (projects === null || typeof projects !== "object" || Array.isArray(projects)) {
1388
+ addItem(
1389
+ section2,
1390
+ `${red(failGlyph)} path-map.json invalid schema: "projects" must be an object`
1391
+ );
1392
+ process.exitCode = 1;
1393
+ return;
1394
+ }
1395
+ for (const [name, hosts] of Object.entries(projects)) {
1396
+ if (hosts === null || typeof hosts !== "object" || Array.isArray(hosts)) {
1397
+ addItem(
1398
+ section2,
1399
+ `${red(failGlyph)} path-map.json invalid schema: project "${name}" hosts must be an object`
1400
+ );
1401
+ process.exitCode = 1;
1402
+ return;
1403
+ }
1404
+ for (const [hostName, mappedPath] of Object.entries(hosts)) {
1405
+ if (typeof mappedPath !== "string") {
1406
+ addItem(
1407
+ section2,
1408
+ `${red(failGlyph)} path-map.json invalid schema: project "${name}" host "${hostName}" path must be a string`
1409
+ );
1410
+ process.exitCode = 1;
1411
+ return;
1412
+ }
1413
+ }
1414
+ }
1415
+ reportMappedProjects(section2, map);
1416
+ reportPathCollisions(section2, map);
1417
+ }
1418
+ function reportNeverSync(section2) {
1419
+ addItem(section2, `${dim(infoGlyph)} never-sync items: ${[...NEVER_SYNC].join(", ")}`);
1420
+ }
1421
+
1422
+ // src/commands.doctor.checks.repository.ts
1423
+ init_color();
1424
+ init_config();
1425
+ import { execFileSync as execFileSync4 } from "node:child_process";
1426
+ import { existsSync as existsSync10 } from "node:fs";
1427
+ import { join as join12, relative as relative2 } from "node:path";
1428
+ init_push_checks();
1429
+ init_utils();
1430
+ function reportGitleaksProbe(section2) {
1431
+ try {
1432
+ const v = execFileSync4("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
1433
+ addItem(section2, `${green(okGlyph)} gitleaks: ${dim(v)}`);
1434
+ return true;
1435
+ } catch (err) {
1436
+ if (err.code === "ENOENT") {
1437
+ addItem(section2, `${yellow(warnGlyph)} gitleaks: not on PATH (required for nomad push)`);
1438
+ } else {
1439
+ addItem(section2, `${red(failGlyph)} gitleaks: probe failed: ${err.message}`);
1440
+ process.exitCode = 1;
1441
+ }
1442
+ return false;
1443
+ }
1444
+ }
1445
+ function reportGitlinks(section2) {
1446
+ const sharedDir = join12(REPO_HOME, "shared");
1447
+ if (existsSync10(sharedDir)) {
1448
+ const gitlinks = findGitlinks(sharedDir);
1449
+ for (const p of gitlinks) {
1450
+ const rel = relative2(REPO_HOME, p);
1451
+ addItem(
1452
+ section2,
1453
+ `${red(failGlyph)} gitlink: ${blue(rel)} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`
1454
+ );
1455
+ }
1456
+ if (gitlinks.length > 0) {
1457
+ process.exitCode = 1;
1458
+ } else {
1459
+ addItem(section2, `${green(okGlyph)} gitlink scan: no nested .git in shared/`);
1460
+ }
1461
+ }
1462
+ }
1463
+ function reportRemote(section2) {
1464
+ try {
1465
+ const url = execFileSync4("git", ["remote", "get-url", "origin"], {
1466
+ cwd: REPO_HOME,
1467
+ stdio: ["ignore", "pipe", "pipe"]
1468
+ }).toString().trim();
1469
+ addItem(section2, `${dim(infoGlyph)} remote origin: ${cyan(url)}`);
1470
+ } catch {
1471
+ addItem(section2, `${dim(infoGlyph)} remote origin: not configured`);
1472
+ }
1473
+ }
1474
+ function reportRebaseClean(section2) {
1475
+ try {
1476
+ const status = gitStatusPorcelainZ(REPO_HOME);
1477
+ if (status.length > 0) {
1478
+ addItem(
1479
+ section2,
1480
+ `${yellow(warnGlyph)} ${blue("~/claude-nomad/")} has uncommitted changes (nomad push will --autostash these)`
1481
+ );
1482
+ }
1483
+ } catch {
1484
+ }
1485
+ }
1486
+
1487
+ // src/commands.doctor.checks.backups.ts
1488
+ init_color();
1489
+ import { existsSync as existsSync11, lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
1490
+ import { join as join13 } from "node:path";
1491
+ init_config();
1492
+ var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
1493
+ function safeReaddir(dir) {
1494
+ try {
1495
+ return readdirSync4(dir);
1496
+ } catch {
1497
+ return [];
1498
+ }
1499
+ }
1500
+ var DOCTOR_BACKUP_COUNT_WARN = 20;
1501
+ var DOCTOR_BACKUP_SIZE_WARN_MB = 200;
1502
+ var BYTES_PER_MB = 1024 * 1024;
1503
+ function dirSizeBytes(dir) {
1504
+ let bytes = 0;
1505
+ for (const entry of safeReaddir(dir)) {
1506
+ const full = join13(dir, entry);
1507
+ const st = lstatSync5(full, { throwIfNoEntry: false });
1508
+ if (!st) continue;
1509
+ if (st.isSymbolicLink()) continue;
1510
+ if (st.isDirectory()) bytes += dirSizeBytes(full);
1511
+ else bytes += st.size;
1512
+ }
1513
+ return bytes;
1514
+ }
1515
+ function totalSizeMb(backupBase, dirs) {
1516
+ let bytes = 0;
1517
+ for (const name of dirs) bytes += dirSizeBytes(join13(backupBase, name));
1518
+ return bytes / BYTES_PER_MB;
1519
+ }
1520
+ function reportBackupsCheck(section2, backupBase = BACKUP_BASE) {
1521
+ if (!existsSync11(backupBase)) return;
1522
+ const dirs = safeReaddir(backupBase).filter((n) => TS_SHAPE2.test(n));
1523
+ const count = dirs.length;
1524
+ const sizeMb = totalSizeMb(backupBase, dirs);
1525
+ if (count > DOCTOR_BACKUP_COUNT_WARN || sizeMb > DOCTOR_BACKUP_SIZE_WARN_MB) {
1526
+ addItem(
1527
+ section2,
1528
+ `${yellow(warnGlyph)} backups: ${count} dirs / ${sizeMb.toFixed(1)} MB (run 'nomad clean --backups')`
1529
+ );
1530
+ }
1531
+ }
1532
+
1533
+ // src/commands.doctor.check-schema.ts
1534
+ init_color();
1535
+ import { execFileSync as execFileSync5 } from "node:child_process";
1536
+ import { existsSync as existsSync12 } from "node:fs";
1537
+ import { join as join14 } from "node:path";
1538
+ init_config();
1539
+ function fetchSchemaKeys() {
1540
+ try {
1541
+ const raw = execFileSync5("curl", ["-fsSL", "-m", "3", SETTINGS_SCHEMA_URL], {
1542
+ stdio: ["ignore", "pipe", "pipe"]
1543
+ }).toString();
1544
+ const parsed = JSON.parse(raw);
1545
+ if (typeof parsed.properties !== "object" || parsed.properties === null) return null;
1546
+ return Object.keys(parsed.properties);
1547
+ } catch {
1548
+ return null;
1549
+ }
1550
+ }
1551
+ function reportCheckSchema(section2) {
1552
+ const settingsPath = join14(CLAUDE_HOME, "settings.json");
1553
+ if (!existsSync12(settingsPath)) {
1554
+ addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
1555
+ return;
1556
+ }
1557
+ const settings = readJsonSafe(settingsPath, settingsPath, section2);
1558
+ if (settings === null) return;
1559
+ const liveKeys = fetchSchemaKeys();
1560
+ if (liveKeys === null) {
1561
+ addItem(
1562
+ section2,
1563
+ `${yellow(warnGlyph)} schema check skipped (offline, curl missing, or schema unreachable)`
1564
+ );
1565
+ return;
1566
+ }
1567
+ const liveSet = new Set(liveKeys);
1568
+ const candidates = Object.keys(settings).filter((k) => !liveSet.has(k));
1569
+ if (candidates.length === 0) {
1570
+ addItem(section2, `${green(okGlyph)} settings.json keys all present in the published schema`);
1571
+ } else {
1572
+ addItem(
1573
+ section2,
1574
+ `${yellow(warnGlyph)} settings.json keys absent from published schema (APP_ONLY_KEYS candidates): ${candidates.join(", ")}`
1575
+ );
1576
+ }
1577
+ }
1578
+
1579
+ // src/commands.doctor.check-shared.ts
1580
+ init_color();
1581
+ import { randomBytes } from "node:crypto";
1582
+ import { execFileSync as execFileSync6 } from "node:child_process";
1583
+ import { existsSync as existsSync14, mkdirSync as mkdirSync4, readdirSync as readdirSync6, rmSync as rmSync6 } from "node:fs";
1584
+ import { homedir as homedir4 } from "node:os";
1585
+ import { join as join17 } from "node:path";
1586
+
1587
+ // src/commands.doctor.check-shared.scan.ts
1588
+ init_color();
1589
+ import { join as join15 } from "node:path";
1590
+ init_config();
1591
+ init_push_gitleaks();
1592
+ function scrubPath(logical, sid, logicalToEncoded) {
1593
+ const encoded = logicalToEncoded.get(logical) ?? logical;
1594
+ return join15(CLAUDE_HOME, "projects", encoded, `${sid}.jsonl`);
1595
+ }
1596
+ function reportSessionFindings(section2, bySession) {
1597
+ for (const [sid, counts] of bySession) {
1598
+ const summary = [...counts.entries()].map(([rule, n]) => `${rule} (${n})`).join(", ");
1599
+ addItem(section2, `${red(failGlyph)} ${red(summary)} in session ${sid}`);
1600
+ }
1601
+ process.exitCode = 1;
1602
+ }
1603
+ function reportOtherFindings(section2, other) {
1604
+ for (const f of other) {
1605
+ addItem(section2, `${red(failGlyph)} ${red(f.RuleID)} leak in ${f.File}`);
1606
+ }
1607
+ process.exitCode = 1;
1608
+ }
1609
+ function reportRemediation(section2, bySession, logicalBySession, logicalToEncoded) {
1610
+ addItem(section2, "");
1611
+ addItem(section2, bold("Remediation"));
1612
+ for (const [sid] of bySession) {
1613
+ const logical = logicalBySession.get(sid);
1614
+ if (logical !== void 0) {
1615
+ const rotateLine = dim(
1616
+ `- rotate the credential, then scrub ${scrubPath(logical, sid, logicalToEncoded)}`
1617
+ );
1618
+ addItem(section2, ` ${rotateLine}`);
1619
+ }
1620
+ }
1621
+ addItem(section2, ` ${dim("- false positive? add a pattern to .gitleaks.toml")}`);
1622
+ }
1623
+ var SESSION_PATH_LOGICAL = /^shared\/projects\/([^/]+)\/([^/]+)\.jsonl$/;
1624
+ function emitClean(section2, staged) {
1625
+ addItem(section2, `${green(okGlyph)} ${staged} project(s) scanned, no leaks`);
1626
+ }
1627
+ function buildLogicalBySession(findings) {
1628
+ const logicalBySession = /* @__PURE__ */ new Map();
1629
+ for (const f of findings) {
1630
+ const m = SESSION_PATH_LOGICAL.exec(f.File);
1631
+ if (m?.[2] !== void 0 && !logicalBySession.has(m[2])) {
1632
+ logicalBySession.set(m[2], m[1] ?? "");
1633
+ }
1634
+ }
1635
+ return logicalBySession;
1636
+ }
1637
+ function emitDescriptionLegend(section2, findings) {
1638
+ const descByRule = /* @__PURE__ */ new Map();
1639
+ for (const f of findings) {
1640
+ if (f.Description && !descByRule.has(f.RuleID)) descByRule.set(f.RuleID, f.Description);
1641
+ }
1642
+ if (descByRule.size === 0) return;
1643
+ addItem(section2, "");
1644
+ addItem(section2, bold("Finding types"));
1645
+ for (const [rule, desc] of descByRule) {
1646
+ const ruleLabel = red(`- [${rule}]`);
1647
+ addItem(section2, ` ${ruleLabel}: ${dim(desc)}`);
1648
+ }
1649
+ }
1650
+ function scanAndReport(section2, tmpRoot, staged, logicalToEncoded) {
1651
+ let findings;
1652
+ try {
1653
+ findings = scanStagedTree(tmpRoot);
1654
+ } catch (err) {
1655
+ addItem(section2, `${red(failGlyph)} scan failed: ${err.message}`);
1656
+ process.exitCode = 1;
1657
+ return;
1658
+ }
1659
+ if (findings === null) {
1660
+ addItem(section2, `${red(failGlyph)} scan failed: no parseable gitleaks report`);
1661
+ process.exitCode = 1;
1662
+ return;
1663
+ }
1664
+ const { bySession, other } = partitionFindings(findings);
1665
+ if (bySession.size === 0 && other.length === 0) {
1666
+ emitClean(section2, staged);
1667
+ return;
1668
+ }
1669
+ if (other.length > 0) reportOtherFindings(section2, other);
1670
+ if (bySession.size > 0) reportSessionFindings(section2, bySession);
1671
+ if (bySession.size > 0) {
1672
+ reportRemediation(section2, bySession, buildLogicalBySession(findings), logicalToEncoded);
1673
+ }
1674
+ emitDescriptionLegend(section2, findings);
1675
+ }
1676
+
1677
+ // src/commands.doctor.check-shared.ts
1678
+ init_config();
1679
+
1680
+ // src/remap.ts
1681
+ init_config_sharedDirs_guard();
1682
+ init_config();
1683
+ init_utils();
1684
+ init_utils_fs();
1685
+ init_utils_json();
1686
+ import { cpSync as cpSync3, existsSync as existsSync13, mkdirSync as mkdirSync3, readdirSync as readdirSync5, rmSync as rmSync5, statSync as statSync4 } from "node:fs";
1687
+ import { join as join16, relative as relative3, sep } from "node:path";
1688
+ function copyDir(src, dst) {
1689
+ rmSync5(dst, { recursive: true, force: true });
1690
+ cpSync3(src, dst, { recursive: true, force: true });
1691
+ }
1692
+ function copyDirJsonlOnly(src, dst) {
1693
+ rmSync5(dst, { recursive: true, force: true });
1694
+ cpSync3(src, dst, {
1695
+ recursive: true,
1696
+ force: true,
1697
+ filter: (srcPath) => {
1698
+ const rel = relative3(src, srcPath);
1699
+ if (rel === "") return true;
1700
+ if (rel.split(sep).length > 1) return true;
1701
+ if (statSync4(srcPath).isDirectory()) return true;
1702
+ if (srcPath.endsWith(".jsonl")) return true;
1703
+ log(`skip ${rel}: extension not in allowlist`);
1704
+ return false;
1705
+ }
1706
+ });
1707
+ }
1708
+ function remapPull(ts, opts = {}) {
1709
+ const dryRun = opts.dryRun === true;
1710
+ let unmapped = 0;
1711
+ const pulled = [];
1712
+ const wouldPull = [];
1713
+ const mapPath = join16(REPO_HOME, "path-map.json");
1714
+ const repoProjects = join16(REPO_HOME, "shared", "projects");
1715
+ if (!existsSync13(mapPath) || !existsSync13(repoProjects)) {
1716
+ log("no path-map or repo projects dir; skipping session remap");
1717
+ return { unmapped: 0, pulled, wouldPull };
1718
+ }
1719
+ const map = readJson(mapPath);
1720
+ const localProjects = join16(CLAUDE_HOME, "projects");
1721
+ if (!dryRun) mkdirSync3(localProjects, { recursive: true });
1722
+ for (const [logical, hosts] of Object.entries(map.projects)) {
1723
+ assertSafeLogical(logical);
1724
+ const localPath = hosts[HOST];
1725
+ if (!localPath || localPath === "TBD") {
1726
+ unmapped++;
1727
+ continue;
1728
+ }
1729
+ const src = join16(repoProjects, logical);
1730
+ if (!existsSync13(src)) continue;
1731
+ const dst = join16(localProjects, encodePath(localPath));
1732
+ if (dryRun) {
1733
+ wouldPull.push(logical);
1734
+ log(`would overwrite: ${dst} (from ${src})`);
1735
+ continue;
1736
+ }
1737
+ backupBeforeWrite(dst, ts);
1738
+ copyDir(src, dst);
1739
+ pulled.push(logical);
1740
+ }
1741
+ return { unmapped, pulled, wouldPull };
1742
+ }
1743
+ function buildReverseMap(map) {
1744
+ const reverse = /* @__PURE__ */ new Map();
1745
+ const encodedPaths = /* @__PURE__ */ new Map();
1746
+ for (const [logical, hosts] of Object.entries(map.projects)) {
1747
+ assertSafeLogical(logical);
1748
+ const p = hosts[HOST];
1749
+ if (!p || p === "TBD") continue;
1750
+ const encoded = encodePath(p);
1751
+ const prior = encodedPaths.get(encoded);
1752
+ if (prior !== void 0) {
1753
+ if (prior !== p) {
1754
+ die(
1755
+ `encoded-path collision in path-map.json: "${prior}" and "${p}" both encode to "${encoded}" (encodePath replaces every / with -). Edit path-map.json so the two paths do not encode identically. Run nomad doctor for the full list of collisions.`
1756
+ );
1757
+ }
1758
+ die(
1759
+ `duplicate path in path-map.json: logical names "${reverse.get(encoded)}" and "${logical}" both map to "${p}" for ${HOST}, so only one could be pushed and the other's shared/projects/ copy would be orphaned. Edit path-map.json so each host path maps to a single logical name.`
1760
+ );
1761
+ }
1762
+ encodedPaths.set(encoded, p);
1763
+ reverse.set(encoded, logical);
1764
+ }
1765
+ return reverse;
1766
+ }
1767
+ function remapPush(ts, opts = {}) {
1768
+ const dryRun = opts.dryRun === true;
1769
+ let unmapped = 0;
1770
+ const pushed = [];
1771
+ const wouldPush = [];
1772
+ const mapPath = join16(REPO_HOME, "path-map.json");
1773
+ if (!existsSync13(mapPath)) {
1774
+ log("no path-map.json; skipping session export");
1775
+ return { unmapped: 0, collisions: 0, pushed, wouldPush };
1776
+ }
1777
+ const map = readJson(mapPath);
1778
+ const localProjects = join16(CLAUDE_HOME, "projects");
1779
+ const repoProjects = join16(REPO_HOME, "shared", "projects");
1780
+ const reverse = buildReverseMap(map);
1781
+ if (!existsSync13(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
1782
+ if (!dryRun) mkdirSync3(repoProjects, { recursive: true });
1783
+ for (const dir of readdirSync5(localProjects)) {
1784
+ const logical = reverse.get(dir);
1785
+ if (!logical) {
1786
+ unmapped++;
1787
+ continue;
1788
+ }
1789
+ const repoDst = join16(repoProjects, logical);
1790
+ if (dryRun) {
1791
+ wouldPush.push(logical);
1792
+ continue;
1793
+ }
1794
+ backupRepoWrite(repoDst, ts, REPO_HOME);
1795
+ copyDirJsonlOnly(join16(localProjects, dir), repoDst);
1796
+ pushed.push(logical);
1797
+ }
1798
+ return { unmapped, collisions: 0, pushed, wouldPush };
1799
+ }
1800
+
1801
+ // src/commands.doctor.check-shared.ts
1802
+ init_utils_fs();
1803
+ init_utils_json();
1804
+ function buildScanTree(tmpRoot) {
1805
+ const logicalToEncoded = /* @__PURE__ */ new Map();
1806
+ let staged = 0;
1807
+ const mapPath = join17(REPO_HOME, "path-map.json");
1808
+ if (!existsSync14(mapPath)) return { logicalToEncoded, staged, malformed: false };
1809
+ let map;
1810
+ try {
1811
+ map = readJson(mapPath);
1812
+ } catch {
1813
+ return { logicalToEncoded, staged, malformed: true };
1814
+ }
1815
+ if (typeof map.projects !== "object" || map.projects === null) {
1816
+ return { logicalToEncoded, staged, malformed: false };
1817
+ }
1818
+ const reverse = /* @__PURE__ */ new Map();
1819
+ for (const [logical, hosts] of Object.entries(map.projects)) {
1820
+ if (typeof hosts !== "object" || hosts === null) continue;
1821
+ const p = hosts[HOST];
1822
+ if (!p || p === "TBD") continue;
1823
+ reverse.set(encodePath(p), logical);
1824
+ }
1825
+ const localProjects = join17(CLAUDE_HOME, "projects");
1826
+ if (!existsSync14(localProjects)) return { logicalToEncoded, staged, malformed: false };
1827
+ for (const dir of readdirSync6(localProjects)) {
1828
+ const logical = reverse.get(dir);
1829
+ if (!logical) continue;
1830
+ copyDirJsonlOnly(join17(localProjects, dir), join17(tmpRoot, "shared", "projects", logical));
1831
+ logicalToEncoded.set(logical, dir);
1832
+ staged++;
1833
+ }
1834
+ return { logicalToEncoded, staged, malformed: false };
1835
+ }
1836
+ function probeGitleaksForScan() {
1837
+ try {
1838
+ execFileSync6("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
1839
+ return "ok";
1840
+ } catch (err) {
1841
+ if (err.code === "ENOENT") return "missing";
1842
+ return { fail: err.message };
1843
+ }
1844
+ }
1845
+ function ensureGitleaksReady(section2, gitleaksReady) {
1846
+ if (gitleaksReady === true) return true;
1847
+ const probe = probeGitleaksForScan();
1848
+ if (probe === "missing") {
1849
+ addItem(section2, `${yellow(warnGlyph)} gitleaks not on PATH; shared scan skipped`);
1850
+ return false;
1851
+ }
1852
+ if (probe !== "ok") {
1853
+ addItem(section2, `${red(failGlyph)} gitleaks probe failed: ${probe.fail}`);
1854
+ process.exitCode = 1;
1855
+ return false;
1856
+ }
1857
+ return true;
1858
+ }
1859
+ function reportCheckShared(section2, gitleaksReady) {
1860
+ if (!ensureGitleaksReady(section2, gitleaksReady)) return;
1861
+ const cacheDir = join17(homedir4(), ".cache", "claude-nomad");
1862
+ mkdirSync4(cacheDir, { recursive: true });
1863
+ const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes(4).toString("hex")}`;
1864
+ const reportPath = join17(cacheDir, `check-shared-${stamp}.json`);
1865
+ const tmpRoot = join17(cacheDir, `check-shared-tree-${stamp}`);
1866
+ try {
1867
+ const { logicalToEncoded, staged, malformed } = buildScanTree(tmpRoot);
1868
+ if (malformed) {
1869
+ addItem(section2, `${red(failGlyph)} path-map.json malformed JSON; shared scan skipped`);
1870
+ process.exitCode = 1;
1871
+ return;
1872
+ }
1873
+ if (staged === 0) {
1874
+ emitClean(section2, 0);
1875
+ return;
1876
+ }
1877
+ scanAndReport(section2, tmpRoot, staged, logicalToEncoded);
1878
+ } finally {
1879
+ rmSync6(reportPath, { force: true });
1880
+ rmSync6(tmpRoot, { recursive: true, force: true });
1881
+ }
1882
+ }
1883
+
1884
+ // src/commands.doctor.checks.hooks.scope.ts
1885
+ init_color();
1886
+ import { existsSync as existsSync15, readFileSync as readFileSync4, readdirSync as readdirSync7, realpathSync } from "node:fs";
1887
+ import { dirname as dirname2, extname, join as join18 } from "node:path";
1888
+ init_config();
1889
+ function typeFromPackageJson(pkgPath) {
1890
+ try {
1891
+ const parsed = JSON.parse(readFileSync4(pkgPath, "utf8"));
1892
+ return parsed.type === "module" ? "esm" : "cjs";
1893
+ } catch {
1894
+ return "cjs";
1895
+ }
1896
+ }
1897
+ function effectiveType(hookPath) {
1898
+ const ext = extname(hookPath);
1899
+ if (ext === ".mjs") return "esm";
1900
+ if (ext === ".cjs") return "cjs";
1901
+ let real;
1902
+ try {
1903
+ real = realpathSync(hookPath);
1904
+ } catch {
1905
+ return null;
1906
+ }
1907
+ let dir = dirname2(real);
1908
+ for (; ; ) {
1909
+ const pkg = join18(dir, "package.json");
1910
+ if (existsSync15(pkg)) return typeFromPackageJson(pkg);
1911
+ const parent = dirname2(dir);
1912
+ if (parent === dir) return "cjs";
1913
+ dir = parent;
1914
+ }
1915
+ }
1916
+ function stripCommentsAndStrings(src) {
1917
+ return src.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|'[^']*'|"[^"]*"|`[^`]*`/g, (match) => {
1918
+ const open = match[0];
1919
+ if (open === "'") return "''";
1920
+ if (open === '"') return '""';
1921
+ if (open === "`") return "``";
1922
+ return " ";
1923
+ });
1924
+ }
1925
+ function classifySource(src) {
1926
+ const code = stripCommentsAndStrings(src);
1927
+ const cjs = /\brequire\s*\(/.test(code) || /\bmodule\.exports\b/.test(code) || /\bexports\.\w/.test(code);
1928
+ const esm = /^[^\S\r\n]*import\s/m.test(code) || /^[^\S\r\n]*export\s/m.test(code) || /\bimport\.meta\b/.test(code);
1929
+ if (cjs && !esm) return "cjs";
1930
+ if (esm && !cjs) return "esm";
1931
+ if (cjs && esm) return "cjs";
1932
+ return "unknown";
1933
+ }
1934
+ function remedy(family) {
1935
+ return family === "cjs" ? 'rename to .cjs, or add { "type": "commonjs" } to the hooks dir' : "rename to .mjs (a synced hooks/ dir treats .js as CommonJS)";
1936
+ }
1937
+ function safeReaddir2(dir) {
1938
+ try {
1939
+ return readdirSync7(dir);
1940
+ } catch {
1941
+ return [];
1942
+ }
1943
+ }
1944
+ function safeRead(path) {
1945
+ try {
1946
+ return readFileSync4(path, "utf8");
1947
+ } catch {
1948
+ return null;
1949
+ }
1950
+ }
1951
+ function reportHookScopeCheck(section2) {
1952
+ const hooksDir = join18(CLAUDE_HOME, "hooks");
1953
+ if (!existsSync15(hooksDir)) {
1954
+ addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
1955
+ return;
1956
+ }
1957
+ let anyWarn = false;
1958
+ for (const name of safeReaddir2(hooksDir)) {
1959
+ if (extname(name) !== ".js") continue;
1960
+ const abs = join18(hooksDir, name);
1961
+ const eff = effectiveType(abs);
1962
+ if (eff === null) continue;
1963
+ const src = safeRead(abs);
1964
+ if (src === null) continue;
1965
+ const fam = classifySource(src);
1966
+ if (fam === "unknown" || fam === eff) continue;
1967
+ addItem(
1968
+ section2,
1969
+ `${yellow(warnGlyph)} hooks/${name}: ${fam} source loads as ${eff} (${remedy(fam)})`
1970
+ );
1971
+ anyWarn = true;
1972
+ }
1973
+ if (!anyWarn) {
1974
+ addItem(section2, `${green(okGlyph)} hooks: module type consistent`);
1975
+ }
1976
+ }
1977
+
1978
+ // src/commands.doctor.checks.hooks.ts
1979
+ init_color();
1980
+ import { existsSync as existsSync16 } from "node:fs";
1981
+ import { join as join19 } from "node:path";
1982
+ init_config();
1983
+ function expandHome(token) {
1984
+ return token.replace(/^\$\{HOME\}/, HOME).replace(/^\$HOME/, HOME).replace(/^~/, HOME);
1985
+ }
1986
+ function stripShellPunctuation(token) {
1987
+ return token.replace(/^['"]+/, "").replace(/['"`;)|&>]+$/, "");
1988
+ }
1989
+ function* claudePathsIn(command) {
1990
+ const claudePrefix = `${CLAUDE_HOME}/`;
1991
+ for (const segment of command.split(/&&|\|\||;|\|/)) {
1992
+ for (const raw of segment.trim().split(/\s+/).filter(Boolean)) {
1993
+ const expanded = expandHome(stripShellPunctuation(raw));
1994
+ if (expanded.startsWith(claudePrefix)) yield expanded;
1995
+ }
1996
+ }
1997
+ }
1998
+ function* commandsFromFlat(entries) {
1999
+ for (const entry of entries) {
2000
+ if (typeof entry !== "object" || entry === null) continue;
2001
+ const e = entry;
2002
+ if (e.type === "command" && typeof e.command === "string") yield e.command;
2003
+ }
2004
+ }
2005
+ function* commandsFromGroup(group) {
2006
+ if (typeof group !== "object" || group === null) return;
2007
+ const g = group;
2008
+ if (Array.isArray(g.hooks)) {
2009
+ yield* commandsFromFlat(g.hooks);
2010
+ return;
2011
+ }
2012
+ if (g.type === "command" && typeof g.command === "string") yield g.command;
2013
+ }
2014
+ function checkEventGroups(section2, event, groups) {
2015
+ let anyFail = false;
2016
+ for (const group of groups) {
2017
+ for (const cmd of commandsFromGroup(group)) {
2018
+ for (const resolved of claudePathsIn(cmd)) {
2019
+ if (existsSync16(resolved)) continue;
2020
+ addItem(section2, `${red(failGlyph)} hooks/${event}: command target missing: ${resolved}`);
2021
+ process.exitCode = 1;
2022
+ anyFail = true;
2023
+ }
2024
+ }
2025
+ }
2026
+ return anyFail;
2027
+ }
2028
+ function reportHooksTargetCheck(section2) {
2029
+ const settingsPath = join19(CLAUDE_HOME, "settings.json");
2030
+ if (!existsSync16(settingsPath)) {
2031
+ addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
2032
+ return;
2033
+ }
2034
+ const settings = readJsonSafe(settingsPath, settingsPath, section2);
2035
+ if (settings === null) return;
2036
+ const hooks = settings.hooks;
2037
+ if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) {
2038
+ addItem(section2, `${green(okGlyph)} hooks: all command targets present`);
2039
+ return;
2040
+ }
2041
+ let anyFail = false;
2042
+ for (const [event, groups] of Object.entries(hooks)) {
2043
+ if (!Array.isArray(groups)) continue;
2044
+ if (checkEventGroups(section2, event, groups)) anyFail = true;
2045
+ }
2046
+ if (!anyFail) {
2047
+ addItem(section2, `${green(okGlyph)} hooks: all command targets present`);
2048
+ }
2049
+ }
2050
+
2051
+ // src/commands.doctor.ts
2052
+ init_config();
2053
+
2054
+ // src/commands.doctor.engine.ts
2055
+ init_color();
2056
+ import { readFileSync as readFileSync6 } from "node:fs";
2057
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
2058
+
2059
+ // src/commands.doctor.version.ts
2060
+ init_color();
2061
+ import { execFileSync as execFileSync7 } from "node:child_process";
2062
+ import { readFileSync as readFileSync5 } from "node:fs";
2063
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
2064
+ var STRICT_SEMVER = /^\d+\.\d+\.\d+$/;
2065
+ var STRICT_SEMVER_PREFIX = /^(\d+\.\d+\.\d+)(?:[-+]|$)/;
2066
+ function compareSemver(a, b) {
2067
+ if (!STRICT_SEMVER.test(a) || !STRICT_SEMVER.test(b)) return 0;
2068
+ const [aMajor, aMinor, aPatch] = a.split(".").map((x) => Number.parseInt(x, 10));
2069
+ const [bMajor, bMinor, bPatch] = b.split(".").map((x) => Number.parseInt(x, 10));
2070
+ if (aMajor !== bMajor) return aMajor < bMajor ? -1 : 1;
2071
+ if (aMinor !== bMinor) return aMinor < bMinor ? -1 : 1;
2072
+ if (aPatch !== bPatch) return aPatch < bPatch ? -1 : 1;
2073
+ return 0;
2074
+ }
2075
+ function readLocalVersion() {
2076
+ try {
2077
+ const pkgPath = fileURLToPath2(new URL("../package.json", import.meta.url));
2078
+ const parsed = JSON.parse(readFileSync5(pkgPath, "utf8"));
2079
+ if (typeof parsed.version === "string" && parsed.version.length > 0) {
2080
+ return parsed.version;
2081
+ }
2082
+ return null;
2083
+ } catch {
2084
+ return null;
2085
+ }
2086
+ }
2087
+ function fetchLatestVersion() {
2088
+ try {
2089
+ const url = "https://registry.npmjs.org/claude-nomad/latest";
2090
+ const raw = execFileSync7("curl", ["-fsSL", "-m", "3", url], {
2091
+ stdio: ["ignore", "pipe", "pipe"]
2092
+ }).toString();
2093
+ const parsed = JSON.parse(raw);
2094
+ if (typeof parsed.version !== "string") return null;
2095
+ if (!STRICT_SEMVER.test(parsed.version)) return null;
2096
+ return parsed.version;
2097
+ } catch {
2098
+ return null;
2099
+ }
2100
+ }
2101
+ function reportVersionCheck(section2) {
2102
+ const local = readLocalVersion();
2103
+ if (local === null) return;
2104
+ const localPure = STRICT_SEMVER_PREFIX.exec(local)?.[1] ?? null;
2105
+ if (localPure === null) return;
2106
+ const latest = fetchLatestVersion();
2107
+ if (latest === null) return;
2108
+ const cmp = compareSemver(localPure, latest);
2109
+ if (cmp === 0) {
2110
+ addItem(section2, `${green(okGlyph)} claude-nomad: ${local} (latest)`);
2111
+ } else if (cmp === -1) {
2112
+ addItem(
2113
+ section2,
2114
+ `${yellow(warnGlyph)} claude-nomad: ${local} -> ${latest} (run \`nomad update\`)`
2115
+ );
2116
+ } else {
2117
+ addItem(
2118
+ section2,
2119
+ `${dim(infoGlyph)} claude-nomad: ${local} (ahead of latest release ${latest})`
2120
+ );
2121
+ }
2122
+ }
2123
+
2124
+ // src/commands.doctor.engine.ts
2125
+ var ENGINES_GTE = /^>=\s*(\d+\.\d+\.\d+)$/;
2126
+ function parseMinVersion(spec) {
2127
+ const m = ENGINES_GTE.exec(spec);
2128
+ return m ? m[1] : null;
2129
+ }
2130
+ function readEnginesNode() {
2131
+ try {
2132
+ const pkgPath = fileURLToPath3(new URL("../package.json", import.meta.url));
2133
+ const parsed = JSON.parse(readFileSync6(pkgPath, "utf8"));
2134
+ const node = parsed.engines?.node;
2135
+ if (typeof node === "string" && node.length > 0) return node;
2136
+ return null;
2137
+ } catch {
2138
+ return null;
2139
+ }
2140
+ }
2141
+ function reportNodeEngineCheck(section2) {
2142
+ const required = readEnginesNode();
2143
+ if (required === null) return;
2144
+ const min = parseMinVersion(required);
2145
+ if (min === null) return;
2146
+ const current = process.version.replace(/^v/, "");
2147
+ if (!/^\d+\.\d+\.\d+$/.test(current)) return;
2148
+ const cmp = compareSemver(current, min);
2149
+ if (cmp === -1) {
2150
+ addItem(
2151
+ section2,
2152
+ `${yellow(warnGlyph)} node: ${process.version} (below required >=${min}, run \`nvm install\`)`
2153
+ );
2154
+ return;
2155
+ }
2156
+ addItem(section2, `${green(okGlyph)} node: ${process.version} (satisfies >=${min})`);
2157
+ }
2158
+
2159
+ // src/commands.doctor.gitleaks-version.ts
2160
+ init_color();
2161
+ import { execFileSync as execFileSync8 } from "node:child_process";
2162
+ import { existsSync as existsSync17 } from "node:fs";
2163
+ import { join as join20 } from "node:path";
2164
+ init_config();
2165
+ var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
2166
+ var GITLEAKS_TIMEOUT_MS = 5e3;
2167
+ function majorMinorOf(value) {
2168
+ const m = SEMVER_MAJOR_MINOR.exec(value);
2169
+ return m === null ? null : [m[1], m[2]];
2170
+ }
2171
+ function readGitleaksVersion(run, tomlExists) {
2172
+ const tomlPath = join20(REPO_HOME, ".gitleaks.toml");
2173
+ const args = ["version"];
2174
+ if (tomlExists(tomlPath)) args.push("--config", tomlPath);
2175
+ try {
2176
+ return run("gitleaks", args, {
2177
+ stdio: ["ignore", "pipe", "pipe"],
2178
+ timeout: GITLEAKS_TIMEOUT_MS
2179
+ }).toString().trim();
2180
+ } catch {
2181
+ return null;
2182
+ }
2183
+ }
2184
+ function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists = existsSync17) {
2185
+ const raw = readGitleaksVersion(run, tomlExists);
2186
+ if (raw === null) return;
2187
+ const local = majorMinorOf(raw);
2188
+ if (local === null) return;
2189
+ const pin = majorMinorOf(GITLEAKS_PINNED_VERSION);
2190
+ if (pin === null) return;
2191
+ const sameMajorMinor = local[0] === pin[0] && local[1] === pin[1];
2192
+ if (sameMajorMinor) {
2193
+ addItem(section2, `${green(okGlyph)} gitleaks: ${raw} (matches pinned ${pin[0]}.${pin[1]})`);
2194
+ return;
2195
+ }
2196
+ addItem(
2197
+ section2,
2198
+ `${yellow(warnGlyph)} gitleaks: ${raw} -> ${GITLEAKS_PINNED_VERSION} (CI pins this; local drift may change scan results)`
2199
+ );
2200
+ }
2201
+
2202
+ // src/commands.doctor.checks.deps.ts
2203
+ init_color();
2204
+ import { execFileSync as execFileSync9 } from "node:child_process";
2205
+ var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
2206
+ function parseFirstVersion(line) {
2207
+ const m = VERSION_TOKEN.exec(line);
2208
+ return m ? m[1] : null;
2209
+ }
2210
+ function probeOptionalDep(bin, run) {
2211
+ try {
2212
+ const firstLine = run(bin, ["--version"], { stdio: ["ignore", "pipe", "pipe"] }).toString().split("\n")[0].trim();
2213
+ const version = parseFirstVersion(firstLine);
2214
+ return { status: "present", version };
2215
+ } catch (err) {
2216
+ if (err.code === "ENOENT") {
2217
+ return { status: "not-installed" };
2218
+ }
2219
+ return { status: "present", version: null };
2220
+ }
2221
+ }
2222
+ function reportOptionalDeps(section2, run = execFileSync9) {
2223
+ const gh = probeOptionalDep("gh", run);
2224
+ if (gh.status === "present") {
2225
+ addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
2226
+ } else {
2227
+ addItem(
2228
+ section2,
2229
+ `${yellow(warnGlyph)} gh: not installed (optional; needed for nomad init Actions auto-disable + mirror-Actions drift check)`
2230
+ );
2231
+ }
2232
+ const curl = probeOptionalDep("curl", run);
2233
+ if (curl.status === "present") {
2234
+ addItem(section2, `${green(okGlyph)} curl: ${curl.version ?? "present"}`);
2235
+ } else {
2236
+ addItem(
2237
+ section2,
2238
+ `${yellow(warnGlyph)} curl: not installed (optional; needed for release-version staleness check + nomad doctor --check-schema)`
2239
+ );
2240
+ }
2241
+ }
2242
+
2243
+ // src/commands.doctor.mirror-actions.ts
2244
+ init_color();
2245
+ import { execFileSync as execFileSync11 } from "node:child_process";
2246
+ init_config();
2247
+
2248
+ // src/gh-actions.ts
2249
+ import { execFileSync as execFileSync10 } from "node:child_process";
2250
+ var GH_TIMEOUT_MS = 5e3;
2251
+ function parseGitHubRemote(remoteUrl) {
2252
+ const normalized = remoteUrl.trim().replace(/\/$/, "");
2253
+ const m = /github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/.exec(normalized);
2254
+ if (m === null) return null;
2255
+ return { owner: m[1], repo: m[2] };
2256
+ }
2257
+ function ghAuthStatus(run = execFileSync10) {
2258
+ try {
2259
+ run("gh", ["auth", "status"], {
2260
+ stdio: ["ignore", "ignore", "ignore"],
2261
+ timeout: GH_TIMEOUT_MS
2262
+ });
2263
+ return null;
2264
+ } catch (err) {
2265
+ const e = err;
2266
+ if (e.code === "ENOENT") return "gh-not-installed";
2267
+ if (typeof e.status === "number") return "gh-not-authed";
2268
+ return "gh-probe-error";
2269
+ }
2270
+ }
2271
+ function isRepoPrivate(ref, run = execFileSync10) {
2272
+ const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
2273
+ stdio: ["ignore", "pipe", "ignore"],
2274
+ timeout: GH_TIMEOUT_MS
2275
+ }).toString();
2276
+ const parsed = JSON.parse(out);
2277
+ return parsed.isPrivate === true;
2278
+ }
2279
+ function isActionsEnabled(ref, run = execFileSync10) {
2280
+ const out = run(
2281
+ "gh",
2282
+ ["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
2283
+ { stdio: ["ignore", "pipe", "ignore"], timeout: GH_TIMEOUT_MS }
2284
+ ).toString().trim();
2285
+ return out === "true";
2286
+ }
2287
+ function disableActions(ref, run = execFileSync10) {
2288
+ run(
2289
+ "gh",
2290
+ [
2291
+ "api",
2292
+ "-X",
2293
+ "PUT",
2294
+ `repos/${ref.owner}/${ref.repo}/actions/permissions`,
2295
+ "-F",
2296
+ "enabled=false"
2297
+ ],
2298
+ { stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
2299
+ );
2300
+ }
2301
+ function readOriginRemote(cwd, run = execFileSync10) {
2302
+ return run("git", ["remote", "get-url", "origin"], {
2303
+ cwd,
2304
+ stdio: ["ignore", "pipe", "ignore"]
2305
+ }).toString().trim();
2306
+ }
2307
+
2308
+ // src/commands.doctor.mirror-actions.ts
2309
+ function reportMirrorActions(section2, run = execFileSync11) {
2310
+ let remote;
2311
+ try {
2312
+ remote = readOriginRemote(REPO_HOME, run);
2313
+ } catch {
2314
+ return;
2315
+ }
2316
+ const ref = parseGitHubRemote(remote);
2317
+ if (ref === null) return;
2318
+ const auth = ghAuthStatus(run);
2319
+ if (auth === "gh-not-installed" || auth === "gh-not-authed") return;
2320
+ let isPrivate;
2321
+ try {
2322
+ isPrivate = isRepoPrivate(ref, run);
2323
+ } catch {
2324
+ return;
2325
+ }
2326
+ if (!isPrivate) return;
2327
+ let enabled2;
2328
+ try {
2329
+ enabled2 = isActionsEnabled(ref, run);
2330
+ } catch {
2331
+ return;
2332
+ }
2333
+ if (!enabled2) return;
2334
+ addItem(
2335
+ section2,
2336
+ `${yellow(warnGlyph)} mirror Actions: enabled on private mirror ${ref.owner}/${ref.repo} (re-disable with 'gh api -X PUT repos/${ref.owner}/${ref.repo}/actions/permissions -F enabled=false')`
2337
+ );
2338
+ }
2339
+
2340
+ // src/commands.doctor.ts
2341
+ function cmdDoctor(opts = {}) {
2342
+ const host = section("Host");
2343
+ reportHostAndPaths(host);
2344
+ reportRepoState(host);
2345
+ const links = section("Shared links");
2346
+ const mapPath = join21(REPO_HOME, "path-map.json");
2347
+ const rawMap = existsSync18(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
2348
+ const map = rawMap ?? { projects: {} };
2349
+ reportSharedLinks(links, map);
2350
+ const hooksScan = section("Hook targets");
2351
+ reportHooksTargetCheck(hooksScan);
2352
+ reportHookScopeCheck(hooksScan);
2353
+ const settings = section("Settings");
2354
+ const base = loadBaseSettings(settings);
2355
+ const parsedSettings = loadAndReportSettings(settings);
2356
+ reportHostOverrides(settings, base, parsedSettings);
2357
+ const pathMap = section("Path map");
2358
+ reportPathMap(pathMap);
2359
+ const neverSync = section("Never-sync");
2360
+ reportNeverSync(neverSync);
2361
+ const repository = section("Repository");
2362
+ const gitleaksReady = reportGitleaksProbe(repository);
2363
+ reportGitlinks(repository);
2364
+ reportRemote(repository);
2365
+ reportRebaseClean(repository);
2366
+ reportMirrorActions(repository);
2367
+ const version = section("Version Checks");
2368
+ reportVersionCheck(version);
2369
+ reportNodeEngineCheck(version);
2370
+ reportGitleaksVersionCheck(version);
2371
+ reportOptionalDeps(version);
2372
+ reportBackupsCheck(version);
2373
+ const sharedScan = section("Shared scan");
2374
+ if (opts.checkShared === true) reportCheckShared(sharedScan, gitleaksReady);
2375
+ const schemaScan = section("Schema scan");
2376
+ if (opts.checkSchema === true) reportCheckSchema(schemaScan);
2377
+ renderDoctor([
2378
+ version,
2379
+ host,
2380
+ links,
2381
+ hooksScan,
2382
+ settings,
2383
+ pathMap,
2384
+ neverSync,
2385
+ repository,
2386
+ sharedScan,
2387
+ schemaScan
2388
+ ]);
2389
+ }
2390
+
2391
+ // src/commands.drop-session.ts
2392
+ init_config();
2393
+ import { execFileSync as execFileSync13 } from "node:child_process";
2394
+ import { existsSync as existsSync20, readdirSync as readdirSync8, statSync as statSync5 } from "node:fs";
2395
+ import { join as join24, relative as relative4 } from "node:path";
2396
+
2397
+ // src/commands.drop-session.git.ts
2398
+ init_config();
2399
+ import { execFileSync as execFileSync12 } from "node:child_process";
2400
+ function expandStagedDir(dirRel) {
2401
+ try {
2402
+ const out = execFileSync12("git", ["ls-files", "-z", "--", dirRel], {
2403
+ cwd: REPO_HOME,
2404
+ stdio: ["ignore", "pipe", "pipe"]
2405
+ });
2406
+ return out.toString().split("\0").filter((p) => p !== "");
2407
+ } catch {
2408
+ return [];
2409
+ }
2410
+ }
2411
+ function isTrackedInHead(rel) {
2412
+ try {
2413
+ execFileSync12("git", ["cat-file", "-e", `HEAD:${rel}`], {
2414
+ cwd: REPO_HOME,
2415
+ stdio: ["ignore", "pipe", "pipe"]
2416
+ });
2417
+ return true;
2418
+ } catch {
2419
+ return false;
2420
+ }
2421
+ }
2422
+ function isInIndex(rel) {
2423
+ try {
2424
+ const out = execFileSync12("git", ["ls-files", "--", rel], {
2425
+ cwd: REPO_HOME,
2426
+ stdio: ["ignore", "pipe", "pipe"]
2427
+ });
2428
+ return out.toString().trim() !== "";
2429
+ } catch {
2430
+ return false;
2431
+ }
2432
+ }
2433
+
2434
+ // src/commands.drop-session.scrub-hint.ts
2435
+ init_config();
2436
+ init_utils();
2437
+ init_utils_json();
2438
+ import { existsSync as existsSync19 } from "node:fs";
2439
+ import { join as join22 } from "node:path";
2440
+ var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
2441
+ function reportScrubHint(id, matches) {
2442
+ const live = resolveLiveTranscript(id, matches);
2443
+ const target = live ?? `~/.claude/projects/<encoded>/${id}.jsonl`;
2444
+ log(
2445
+ `note: this only un-stages the session from the next push.
2446
+ The local source still contains the secret, so nomad push re-stages it
2447
+ on the next run and nomad doctor --check-shared keeps reporting it.
2448
+ To fully remediate: rotate the credential, then run:
2449
+ nomad redact ${id}
2450
+ (or scrub ${target} manually)`
2451
+ );
2452
+ }
2453
+ function resolveLiveTranscript(id, matches) {
2454
+ try {
2455
+ const mapPath = join22(REPO_HOME, "path-map.json");
2456
+ if (!existsSync19(mapPath)) return null;
2457
+ const projects = readJson(mapPath).projects;
2458
+ for (const rel of matches) {
2459
+ const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
2460
+ if (logical === void 0) continue;
2461
+ const abs = projects[logical]?.[HOST];
2462
+ if (abs === void 0) continue;
2463
+ const live = join22(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2464
+ if (existsSync19(live)) return live;
2465
+ }
2466
+ return null;
2467
+ } catch {
2468
+ return null;
2469
+ }
2470
+ }
2471
+
2472
+ // src/commands.drop-session.ts
2473
+ init_utils();
2474
+
2475
+ // src/utils.lockfile.ts
2476
+ init_config();
2477
+ init_utils();
2478
+ import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
2479
+ import { dirname as dirname3, join as join23 } from "node:path";
2480
+ var LOCK_PATH = join23(HOME, ".cache", "claude-nomad", "nomad.lock");
2481
+ function acquireLock(verb) {
2482
+ mkdirSync5(dirname3(LOCK_PATH), { recursive: true });
2483
+ try {
2484
+ const fd = openSync2(LOCK_PATH, "wx");
2485
+ try {
2486
+ writeFileSync3(fd, String(process.pid));
2487
+ } catch (writeErr) {
2488
+ try {
2489
+ closeSync2(fd);
2490
+ } catch {
2491
+ }
2492
+ try {
2493
+ unlinkSync(LOCK_PATH);
2494
+ } catch {
2495
+ }
2496
+ throw writeErr;
2497
+ }
2498
+ return { fd };
2499
+ } catch (err) {
2500
+ const code = err.code;
2501
+ if (code !== "EEXIST") throw err;
2502
+ return checkStaleAndRetry(verb);
2503
+ }
2504
+ }
2505
+ function releaseLock(handle) {
2506
+ if (handle === null) return;
2507
+ try {
2508
+ closeSync2(handle.fd);
2509
+ } catch {
2510
+ }
2511
+ try {
2512
+ unlinkSync(LOCK_PATH);
2513
+ } catch (err) {
2514
+ if (err.code !== "ENOENT") throw err;
2515
+ }
2516
+ }
2517
+ function unlinkIfSamePid(expectedPidStr) {
2518
+ let current;
2519
+ try {
2520
+ current = readFileSync7(LOCK_PATH, "utf8").trim();
2521
+ } catch {
2522
+ return false;
2523
+ }
2524
+ if (current !== expectedPidStr) return false;
2525
+ try {
2526
+ unlinkSync(LOCK_PATH);
2527
+ return true;
2528
+ } catch {
2529
+ return false;
2530
+ }
2531
+ }
2532
+ function checkStaleAndRetry(verb) {
2533
+ let pidStr;
2534
+ try {
2535
+ pidStr = readFileSync7(LOCK_PATH, "utf8").trim();
2536
+ } catch {
2537
+ pidStr = "";
2538
+ }
2539
+ const pid = Number.parseInt(pidStr, 10);
2540
+ if (!Number.isFinite(pid) || pid <= 0) {
2541
+ if (unlinkIfSamePid(pidStr)) return retryOnce(verb);
2542
+ warn(`another nomad ${verb} running, skipping`);
2543
+ return null;
2544
+ }
2545
+ try {
2546
+ process.kill(pid, 0);
2547
+ warn(`another nomad ${verb} running, skipping`);
2548
+ return null;
2549
+ } catch (err) {
2550
+ const code = err.code;
2551
+ if (code === "ESRCH") {
2552
+ if (unlinkIfSamePid(pidStr)) return retryOnce(verb);
2553
+ warn(`another nomad ${verb} running, skipping`);
2554
+ return null;
2555
+ }
2556
+ warn(`another nomad ${verb} running, skipping`);
2557
+ return null;
2558
+ }
2559
+ }
2560
+ function retryOnce(verb) {
2561
+ try {
2562
+ const fd = openSync2(LOCK_PATH, "wx");
2563
+ try {
2564
+ writeFileSync3(fd, String(process.pid));
2565
+ } catch {
2566
+ try {
2567
+ closeSync2(fd);
2568
+ } catch {
2569
+ }
2570
+ try {
2571
+ unlinkSync(LOCK_PATH);
2572
+ } catch {
2573
+ }
2574
+ warn(`another nomad ${verb} running, skipping`);
2575
+ return null;
2576
+ }
2577
+ return { fd };
2578
+ } catch {
2579
+ warn(`another nomad ${verb} running, skipping`);
2580
+ return null;
2581
+ }
2582
+ }
2583
+
2584
+ // src/commands.drop-session.ts
2585
+ function cmdDropSession(id) {
2586
+ if (id.length === 0 || id.length > 128 || !/^[A-Za-z0-9_-]+$/.test(id)) {
2587
+ fail(`invalid session id: ${id}`);
2588
+ process.exit(1);
2589
+ }
2590
+ if (!existsSync20(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2591
+ const handle = acquireLock("drop-session");
2592
+ if (handle === null) process.exit(0);
2593
+ try {
2594
+ const repoProjects = join24(REPO_HOME, "shared", "projects");
2595
+ if (!existsSync20(repoProjects)) {
2596
+ throw new NomadFatal(`no staged session matches ${id}`);
2597
+ }
2598
+ const matches = collectMatches(repoProjects, id);
2599
+ if (matches.length === 0) {
2600
+ throw new NomadFatal(`no staged session matches ${id}`);
2601
+ }
2602
+ for (const rel of matches) unstageOne(rel);
2603
+ reportScrubHint(id, matches);
2604
+ } catch (err) {
2605
+ if (!(err instanceof NomadFatal)) {
2606
+ throw err;
2607
+ }
2608
+ fail(err.message);
2609
+ process.exitCode = 1;
2610
+ } finally {
2611
+ releaseLock(handle);
2612
+ }
2613
+ }
2614
+ function collectMatches(repoProjects, id) {
2615
+ const matches = [];
2616
+ for (const logical of readdirSync8(repoProjects)) {
2617
+ const candidate = join24(repoProjects, logical, `${id}.jsonl`);
2618
+ if (existsSync20(candidate)) {
2619
+ matches.push(relative4(REPO_HOME, candidate));
2620
+ }
2621
+ const dir = join24(repoProjects, logical, id);
2622
+ if (existsSync20(dir) && statSync5(dir).isDirectory()) {
2623
+ const dirRel = relative4(REPO_HOME, dir);
2624
+ const staged = expandStagedDir(dirRel);
2625
+ if (staged.length > 0) matches.push(...staged);
2626
+ else matches.push(dirRel);
2627
+ }
2628
+ }
2629
+ return matches;
2630
+ }
2631
+ function unstageOne(rel) {
2632
+ if (!isInIndex(rel)) {
2633
+ log(`dropped ${rel} (already absent from index)`);
2634
+ return;
2635
+ }
2636
+ try {
2637
+ if (isTrackedInHead(rel)) {
2638
+ execFileSync13("git", ["restore", "--staged", "--worktree", "--", rel], {
2639
+ cwd: REPO_HOME,
2640
+ stdio: ["ignore", "pipe", "pipe"]
2641
+ });
2642
+ } else {
2643
+ execFileSync13("git", ["rm", "--cached", "-f", "--", rel], {
2644
+ cwd: REPO_HOME,
2645
+ stdio: ["ignore", "pipe", "pipe"]
2646
+ });
2647
+ }
2648
+ } catch (err) {
2649
+ const e = err;
2650
+ const detail = e.stderr?.toString().trim() ?? e.message;
2651
+ throw new NomadFatal(`git failed to unstage ${rel}: ${detail}`);
2652
+ }
2653
+ log(`dropped ${rel}`);
2654
+ }
2655
+
2656
+ // src/commands.redact.ts
2657
+ init_config();
2658
+ import { existsSync as existsSync21, readFileSync as readFileSync8, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
2659
+ import { join as join26 } from "node:path";
2660
+
2661
+ // src/commands.redact.core.ts
2662
+ init_config();
2663
+ import { appendFileSync } from "node:fs";
2664
+ import { join as join25 } from "node:path";
2665
+ function collectMatchIntervals(content, findings) {
2666
+ const intervals = [];
2667
+ for (const f of findings) {
2668
+ const match = f.Match;
2669
+ if (match === "") continue;
2670
+ let from = 0;
2671
+ let pos = content.indexOf(match, from);
2672
+ while (pos !== -1) {
2673
+ intervals.push({ start: pos, end: pos + match.length, ruleId: f.RuleID });
2674
+ from = pos + match.length;
2675
+ pos = content.indexOf(match, from);
2676
+ }
2677
+ }
2678
+ return intervals;
2679
+ }
2680
+ function mergeIntervals(intervals) {
2681
+ if (intervals.length === 0) return [];
2682
+ const sorted = [...intervals].sort((a, b) => a.start - b.start || b.end - a.end);
2683
+ let last = { ...sorted[0] };
2684
+ const merged = [last];
2685
+ for (let i = 1; i < sorted.length; i++) {
2686
+ const cur = sorted[i];
2687
+ if (cur.start <= last.end) {
2688
+ if (cur.end > last.end) last.end = cur.end;
2689
+ } else {
2690
+ last = { ...cur };
2691
+ merged.push(last);
2692
+ }
2693
+ }
2694
+ return merged;
2695
+ }
2696
+ function applyRedactions(content, findings) {
2697
+ const raw = collectMatchIntervals(content, findings);
2698
+ if (raw.length === 0) return content;
2699
+ const merged = mergeIntervals(raw);
2700
+ let result = content;
2701
+ for (let i = merged.length - 1; i >= 0; i--) {
2702
+ const { start, end, ruleId } = merged[i];
2703
+ result = result.slice(0, start) + `[REDACTED:${ruleId}]` + result.slice(end);
2704
+ }
2705
+ return result;
2706
+ }
2707
+ function formatFingerprint(fingerprint) {
2708
+ return fingerprint.replace(/[\r\n]/g, "") + "\n";
2709
+ }
2710
+ function isRecentlyModified(mtimeMs, nowMs, thresholdMs = 5 * 60 * 1e3) {
2711
+ return nowMs - mtimeMs < thresholdMs;
2712
+ }
2713
+ function appendGitleaksIgnore(fingerprint) {
2714
+ appendFileSync(join25(REPO_HOME, ".gitleaksignore"), formatFingerprint(fingerprint), "utf8");
2715
+ }
2716
+
2717
+ // src/commands.redact.ts
2718
+ init_push_gitleaks_scan();
2719
+ init_utils_fs();
2720
+ init_utils_json();
2721
+ init_utils();
2722
+ function resolveLiveTranscript2(id) {
2723
+ try {
2724
+ const mapPath = join26(REPO_HOME, "path-map.json");
2725
+ if (!existsSync21(mapPath)) return null;
2726
+ const projects = readJson(mapPath).projects;
2727
+ for (const hostMap of Object.values(projects)) {
2728
+ const abs = hostMap[HOST];
2729
+ if (abs === void 0) continue;
2730
+ const live = join26(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2731
+ if (existsSync21(live)) return live;
2732
+ }
2733
+ return null;
2734
+ } catch {
2735
+ return null;
2736
+ }
2737
+ }
2738
+ function resolveRedactFindings(localPath, rawFindings, rule, scan) {
2739
+ const source = rawFindings ?? scan(localPath);
2740
+ if (source === null) return null;
2741
+ return source.filter((f) => rule === void 0 || f.RuleID === rule);
2742
+ }
2743
+ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
2744
+ const { id, rule, dryRun = false, findings: rawFindings } = opts;
2745
+ if (id.length === 0 || id.length > 128 || !/^[A-Za-z0-9_-]+$/.test(id)) {
2746
+ fail(`invalid session id: ${id}`);
2747
+ process.exit(1);
2748
+ }
2749
+ if (!existsSync21(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2750
+ const handle = acquireLock("redact");
2751
+ if (handle === null) process.exit(0);
2752
+ try {
2753
+ const localPath = resolveLiveTranscript2(id);
2754
+ if (localPath === null || !existsSync21(localPath)) {
2755
+ fail(`could not resolve local transcript for session ${id} on this host`);
2756
+ process.exitCode = 1;
2757
+ return;
2758
+ }
2759
+ const mtimeMs = statSync6(localPath).mtimeMs;
2760
+ if (isRecentlyModified(mtimeMs, nowMs())) {
2761
+ log(
2762
+ `session ${id} was modified recently and may be active.
2763
+ Refusing to rewrite a potentially live transcript.
2764
+ To proceed: wait for the session to end, then re-run nomad redact.
2765
+ Or drop from the staged tree: nomad drop-session ${id}
2766
+ Or skip this finding during nomad push.`
2767
+ );
2768
+ return;
2769
+ }
2770
+ const findings = resolveRedactFindings(localPath, rawFindings, rule, scan);
2771
+ if (findings === null) {
2772
+ fail(`gitleaks scan failed for session ${id} (is gitleaks installed?)`);
2773
+ process.exitCode = 1;
2774
+ return;
2775
+ }
2776
+ if (findings.length === 0) {
2777
+ const ruleClause = rule === void 0 ? "" : ` for rule ${rule}`;
2778
+ log(`no findings${ruleClause} in session ${id}`);
2779
+ return;
2780
+ }
2781
+ if (dryRun) {
2782
+ log(
2783
+ `dry-run: would redact ${findings.length} finding(s) in ${localPath}
2784
+ ` + findings.map((f) => ` line ${f.StartLine} [${f.RuleID}]`).join("\n")
2785
+ );
2786
+ return;
2787
+ }
2788
+ const backupBase = join26(HOME, ".cache", "claude-nomad", "backup");
2789
+ const ts = freshBackupTs(backupBase);
2790
+ backupBeforeWrite(localPath, ts);
2791
+ const original = readFileSync8(localPath, "utf8");
2792
+ const redacted = applyRedactions(original, findings);
2793
+ writeFileSync4(localPath, redacted, "utf8");
2794
+ log(`redacted ${findings.length} finding(s) in ${localPath} (backup: ${ts})`);
2795
+ } catch (err) {
2796
+ if (!(err instanceof NomadFatal)) {
2797
+ throw err;
2798
+ }
2799
+ fail(err.message);
2800
+ process.exitCode = 1;
2801
+ } finally {
2802
+ releaseLock(handle);
2803
+ }
2804
+ }
2805
+
2806
+ // src/commands.pull.ts
2807
+ import { existsSync as existsSync27, mkdirSync as mkdirSync7 } from "node:fs";
2808
+ import { join as join32 } from "node:path";
2809
+
2810
+ // src/commands.push.sections.ts
2811
+ init_color();
2812
+
2813
+ // src/summary.ts
2814
+ init_color();
2815
+ init_utils();
2816
+ function summaryText(verb, unmapped, collisions = 0, extrasSkipped = 0) {
2817
+ const extras = extrasSkipped > 0 ? `, ${extrasSkipped} extras skipped` : "";
2818
+ if (verb === "push") {
2819
+ if (unmapped === 0 && collisions === 0 && extrasSkipped === 0) {
2820
+ return { text: "summary: clean", clean: true };
2821
+ }
2822
+ const base = `summary: ${unmapped} unmapped on push, ${collisions} collisions`;
2823
+ return { text: `${base}${extras} (run nomad doctor to list)`, clean: false };
2824
+ }
2825
+ if (unmapped === 0 && extrasSkipped === 0) {
2826
+ return { text: "summary: clean", clean: true };
2827
+ }
2828
+ return {
2829
+ text: `summary: ${unmapped} unmapped on ${verb}${extras} (run nomad doctor to list)`,
2830
+ clean: false
2831
+ };
2832
+ }
2833
+ function summaryRow(verb, unmapped, collisions = 0, extrasSkipped = 0) {
2834
+ const { text, clean } = summaryText(verb, unmapped, collisions, extrasSkipped);
2835
+ return clean ? `${green(okGlyph)} ${text}` : `${yellow(warnGlyph)} ${text}`;
2836
+ }
2837
+ function emitSummary(verb, unmapped, collisions = 0, extrasSkipped = 0) {
2838
+ const { text, clean } = summaryText(verb, unmapped, collisions, extrasSkipped);
2839
+ if (clean) {
2840
+ ok(text);
2841
+ return;
2842
+ }
2843
+ warn(text);
2844
+ }
2845
+
2846
+ // src/commands.push.sections.ts
2847
+ function collapsedSkipRow(n, noun) {
2848
+ if (n <= 0) return null;
2849
+ return `${dim(infoGlyph)} ${n} ${noun}`;
2850
+ }
2851
+ function buildSettingsSection(label) {
2852
+ const s = section("Settings");
2853
+ addItem(s, `${green(okGlyph)} settings.json (base + ${label})`);
2854
+ return s;
2855
+ }
2856
+ function buildSessionsSection(items, unmapped) {
2857
+ const s = section("Sessions");
2858
+ for (const logical of items) addItem(s, `${green(okGlyph)} ${logical}`);
2859
+ const skip = collapsedSkipRow(unmapped, "not in path-map (run nomad doctor to list)");
2860
+ if (skip !== null) addItem(s, skip);
2861
+ return s;
2862
+ }
2863
+ function buildExtrasSection(items, extrasSkipped) {
2864
+ const s = section("Extras");
2865
+ for (const entry of items) addItem(s, `${green(okGlyph)} ${entry}`);
2866
+ const skip = collapsedSkipRow(extrasSkipped, "extras skipped");
2867
+ if (skip !== null) addItem(s, skip);
2868
+ return s;
2869
+ }
2870
+ function syncedSections(st) {
2871
+ const sessions = st.dryRun ? st.remap.wouldPush : st.remap.pushed;
2872
+ const extras = st.dryRun ? st.extras.wouldPush : st.extras.pushed;
2873
+ return [
2874
+ buildSessionsSection(sessions, st.remap.unmapped),
2875
+ buildExtrasSection(extras, st.extras.skipped)
2876
+ ];
2877
+ }
2878
+ function summarySection(st) {
2879
+ const s = section("Summary");
2880
+ const unmapped = st.remap.unmapped + st.extras.unmapped;
2881
+ addItem(s, summaryRow("push", unmapped, st.remap.collisions, st.extras.skipped));
2882
+ return s;
2883
+ }
2884
+ function renderPushTree(st, verdict) {
2885
+ const leakScan = section("Leak scan");
2886
+ addItem(leakScan, verdict.verdictRow);
2887
+ renderTree([...syncedSections(st), leakScan, summarySection(st)]);
2888
+ }
2889
+ function renderNoScanTree(st, opts = {}) {
2890
+ const sections = [];
2891
+ if (opts.noMapHint === true) {
2892
+ const pathMap = section("Path map");
2893
+ addItem(pathMap, `${dim(infoGlyph)} no path-map.json (nothing to preview)`);
2894
+ sections.push(pathMap);
2895
+ }
2896
+ renderTree([...sections, ...syncedSections(st), summarySection(st)]);
2897
+ }
2898
+
2899
+ // src/commands.pull.ts
2900
+ init_config();
2901
+
2902
+ // src/extras-sync.ts
2903
+ init_config();
2904
+ import { existsSync as existsSync24 } from "node:fs";
2905
+ import { join as join29 } from "node:path";
2906
+
2907
+ // src/extras-sync.diff.ts
2908
+ init_utils();
2909
+ import { execFileSync as execFileSync14 } from "node:child_process";
2910
+ function listDivergingFiles(a, b) {
2911
+ try {
2912
+ const stdout = execFileSync14("git", ["diff", "--no-index", "--name-only", a, b], {
2913
+ stdio: ["ignore", "pipe", "pipe"]
2914
+ }).toString();
2915
+ return stdout.split("\n").filter((line) => line.length > 0);
2916
+ } catch (err) {
2917
+ const e = err;
2918
+ if (e.status === 1 && e.stdout !== void 0) {
2919
+ return e.stdout.toString().split("\n").filter((line) => line.length > 0);
2920
+ }
2921
+ if (e.code === "ENOENT") {
2922
+ warn(`git not on PATH; divergence check skipped for ${a}`);
2923
+ return [];
2924
+ }
2925
+ warn(`divergence check failed for ${a}: ${e.message ?? String(err)}`);
2926
+ return [];
2927
+ }
2928
+ }
2929
+
2930
+ // src/extras-sync.core.ts
2931
+ init_config();
2932
+ import { cpSync as cpSync4, existsSync as existsSync22, rmSync as rmSync7 } from "node:fs";
2933
+ import { join as join27 } from "node:path";
2934
+
2935
+ // src/extras-sync.guards.ts
2936
+ init_utils();
2937
+ init_config_sharedDirs_guard();
2938
+ import { isAbsolute, normalize } from "node:path";
2939
+ function assertSafeLocalRoot(localRoot, logical) {
2940
+ if (!isAbsolute(localRoot)) {
2941
+ throw new NomadFatal(
2942
+ `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
2943
+ );
2944
+ }
2945
+ if (localRoot !== normalize(localRoot)) {
2946
+ throw new NomadFatal(
2947
+ `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be already-normalized; no '..' or redundant segments)`
2948
+ );
2949
+ }
2950
+ }
2951
+
2952
+ // src/extras-sync.core.ts
2953
+ init_utils();
2954
+ init_utils_json();
2955
+ function loadValidatedExtras(opts) {
2956
+ const mapPath = join27(REPO_HOME, "path-map.json");
2957
+ const repoExtras = join27(REPO_HOME, "shared", "extras");
2958
+ if (!existsSync22(mapPath) || opts.requireRepoExtras === true && !existsSync22(repoExtras)) {
2959
+ if (opts.missingMsg !== void 0) log(opts.missingMsg);
2960
+ return null;
2961
+ }
2962
+ const map = readPathMap(mapPath);
2963
+ const extrasMap = map.extras ?? {};
2964
+ if (Object.keys(extrasMap).length === 0) return null;
2965
+ for (const logical of Object.keys(extrasMap)) {
2966
+ assertSafeLogical(logical);
2967
+ const localRoot = map.projects[logical]?.[HOST];
2968
+ if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
2969
+ }
2970
+ return { map, extrasMap };
2971
+ }
2972
+ function* eachExtrasTarget(v, counts) {
2973
+ const whitelist = SUPPORTED_EXTRAS;
2974
+ for (const [logical, dirnames] of Object.entries(v.extrasMap)) {
2975
+ const localRoot = v.map.projects[logical]?.[HOST];
2976
+ if (!localRoot || localRoot === "TBD") {
2977
+ counts.unmapped++;
2978
+ continue;
2979
+ }
2980
+ for (const dirname4 of dirnames) {
2981
+ if (!whitelist.includes(dirname4)) {
2982
+ counts.skipped++;
2983
+ continue;
2984
+ }
2985
+ yield { logical, localRoot, dirname: dirname4 };
2986
+ }
2987
+ }
2988
+ }
2989
+ function copyExtras(src, dst) {
2990
+ rmSync7(dst, { recursive: true, force: true });
2991
+ cpSync4(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
2992
+ }
2993
+
2994
+ // src/extras-sync.ts
2995
+ init_utils();
2996
+ init_utils_json();
2997
+
2998
+ // src/extras-sync.remap.ts
2999
+ init_config();
3000
+ import { existsSync as existsSync23, mkdirSync as mkdirSync6 } from "node:fs";
3001
+ import { join as join28 } from "node:path";
3002
+ init_utils_fs();
3003
+ function runExtrasOp(v, dryRun, paths, backup) {
3004
+ const counts = { unmapped: 0, skipped: 0 };
3005
+ const done = [];
3006
+ const would = [];
3007
+ for (const t of eachExtrasTarget(v, counts)) {
3008
+ const { src, dst } = paths(t);
3009
+ if (!existsSync23(src)) continue;
3010
+ const item = `${t.logical}/${t.dirname}`;
3011
+ if (dryRun) {
3012
+ would.push(item);
3013
+ continue;
3014
+ }
3015
+ backup(dst, t.localRoot);
3016
+ copyExtras(src, dst);
3017
+ done.push(item);
3018
+ }
3019
+ return { ...counts, done, would };
3020
+ }
3021
+ function remapExtrasPush(ts, opts = {}) {
3022
+ const dryRun = opts.dryRun === true;
3023
+ const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
3024
+ if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
3025
+ const repoExtras = join28(REPO_HOME, "shared", "extras");
3026
+ if (!dryRun) mkdirSync6(repoExtras, { recursive: true });
3027
+ const { unmapped, skipped, done, would } = runExtrasOp(
3028
+ v,
3029
+ dryRun,
3030
+ ({ localRoot, logical, dirname: dirname4 }) => ({
3031
+ src: join28(localRoot, dirname4),
3032
+ dst: join28(repoExtras, logical, dirname4)
3033
+ }),
3034
+ (dst) => backupRepoWrite(dst, ts, REPO_HOME)
3035
+ );
3036
+ return { unmapped, skipped, pushed: done, wouldPush: would };
3037
+ }
3038
+ function remapExtrasPull(ts, opts = {}) {
3039
+ const dryRun = opts.dryRun === true;
3040
+ const v = loadValidatedExtras({
3041
+ requireRepoExtras: true,
3042
+ missingMsg: "no path-map or repo extras dir; skipping extras remap"
3043
+ });
3044
+ if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
3045
+ const repoExtras = join28(REPO_HOME, "shared", "extras");
3046
+ const { unmapped, skipped, done, would } = runExtrasOp(
3047
+ v,
3048
+ dryRun,
3049
+ ({ localRoot, logical, dirname: dirname4 }) => ({
3050
+ src: join28(repoExtras, logical, dirname4),
3051
+ dst: join28(localRoot, dirname4)
3052
+ }),
3053
+ // Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
3054
+ // localRoot so the backup tree mirrors the project layout.
3055
+ (dst, localRoot) => backupExtrasWrite(dst, ts, localRoot)
3056
+ );
3057
+ return { unmapped, skipped, pulled: done, wouldPull: would };
3058
+ }
3059
+
3060
+ // src/extras-sync.ts
3061
+ function divergenceCheckExtras(ts) {
3062
+ const v = loadValidatedExtras({});
3063
+ if (v === null) return;
3064
+ const counts = { unmapped: 0, skipped: 0 };
3065
+ const backupRoot = join29(HOME, ".cache", "claude-nomad", "backup", ts, "extras");
3066
+ for (const { logical, localRoot, dirname: dirname4 } of eachExtrasTarget(v, counts)) {
3067
+ const local = join29(localRoot, dirname4);
3068
+ const repo = join29(REPO_HOME, "shared", "extras", logical, dirname4);
3069
+ if (!existsSync24(local) || !existsSync24(repo)) continue;
3070
+ const diff = listDivergingFiles(local, repo);
3071
+ if (diff.length === 0) continue;
3072
+ const projectBackupRoot = join29(backupRoot, encodePath(localRoot));
3073
+ warn(
3074
+ `local ${dirname4} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
3075
+ );
3076
+ for (const f of diff) warn(` ${f}`);
3077
+ }
3078
+ }
3079
+
3080
+ // src/links.ts
3081
+ init_config();
3082
+ init_utils();
3083
+ init_utils_fs();
3084
+ init_utils_json();
3085
+ import { existsSync as existsSync25, lstatSync as lstatSync6, rmSync as rmSync8 } from "node:fs";
3086
+ import { join as join30 } from "node:path";
3087
+ function applySharedLinks(ts, map, opts = {}) {
3088
+ const dryRun = opts.dryRun === true;
3089
+ const linkNames = allSharedLinks(map);
3090
+ for (const name of linkNames) {
3091
+ const linkPath = join30(CLAUDE_HOME, name);
3092
+ const target = join30(REPO_HOME, "shared", name);
3093
+ if (!existsSync25(linkPath)) continue;
3094
+ if (lstatSync6(linkPath).isSymbolicLink()) continue;
3095
+ if (!existsSync25(target)) continue;
3096
+ if (dryRun) {
3097
+ log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
3098
+ continue;
3099
+ }
3100
+ backupBeforeWrite(linkPath, ts);
3101
+ rmSync8(linkPath, { recursive: true, force: true });
3102
+ }
3103
+ for (const name of linkNames) {
3104
+ const target = join30(REPO_HOME, "shared", name);
3105
+ if (!existsSync25(target)) continue;
3106
+ if (dryRun) {
3107
+ log(`would create symlink: ${join30(CLAUDE_HOME, name)} -> ${target}`);
3108
+ continue;
3109
+ }
3110
+ ensureSymlink(join30(CLAUDE_HOME, name), target);
3111
+ }
3112
+ }
3113
+ function regenerateSettings(ts, opts = {}) {
3114
+ const dryRun = opts.dryRun === true;
3115
+ const basePath = join30(REPO_HOME, "shared", "settings.base.json");
3116
+ const hostPath = join30(REPO_HOME, "hosts", `${HOST}.json`);
3117
+ if (!existsSync25(basePath)) {
3118
+ die("repo not initialized; run 'nomad init' to scaffold");
3119
+ }
3120
+ const base = readJson(basePath);
3121
+ const hasOverrides = existsSync25(hostPath);
3122
+ const overrides = hasOverrides ? readJson(hostPath) : {};
3123
+ const merged = deepMerge(base, overrides);
3124
+ const settingsPath = join30(CLAUDE_HOME, "settings.json");
3125
+ if (!hasOverrides && existsSync25(settingsPath)) {
3126
+ try {
3127
+ const existing = readJson(settingsPath);
3128
+ const baseKeys = new Set(Object.keys(base));
3129
+ const drift = Object.keys(existing).filter((k) => !baseKeys.has(k));
3130
+ if (drift.length > 0) {
3131
+ warn(
3132
+ `no hosts/${HOST}.json found; existing settings has unbased keys ${JSON.stringify(drift)}. Set NOMAD_HOST to match a hosts/*.json or rerun 'nomad doctor' for candidates.`
3133
+ );
3134
+ }
3135
+ } catch {
3136
+ warn("existing settings.json is malformed; skipping drift-check and regenerating.");
3137
+ }
3138
+ }
3139
+ const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
3140
+ if (dryRun) {
3141
+ log(`would write settings.json (base + ${overrideLabel})`);
3142
+ return { label: overrideLabel };
3143
+ }
3144
+ backupBeforeWrite(settingsPath, ts);
3145
+ writeJsonAtomic(settingsPath, merged);
3146
+ return { label: overrideLabel };
3147
+ }
3148
+
3149
+ // src/preview.ts
3150
+ init_config();
3151
+ import { existsSync as existsSync26 } from "node:fs";
3152
+ import { join as join31 } from "node:path";
3153
+
3154
+ // node_modules/diff/libesm/diff/base.js
3155
+ var Diff = class {
3156
+ diff(oldStr, newStr, options = {}) {
3157
+ let callback;
3158
+ if (typeof options === "function") {
3159
+ callback = options;
3160
+ options = {};
3161
+ } else if ("callback" in options) {
3162
+ callback = options.callback;
3163
+ }
3164
+ const oldString = this.castInput(oldStr, options);
3165
+ const newString = this.castInput(newStr, options);
3166
+ const oldTokens = this.removeEmpty(this.tokenize(oldString, options));
3167
+ const newTokens = this.removeEmpty(this.tokenize(newString, options));
3168
+ return this.diffWithOptionsObj(oldTokens, newTokens, options, callback);
3169
+ }
3170
+ diffWithOptionsObj(oldTokens, newTokens, options, callback) {
3171
+ var _a;
3172
+ const done = (value) => {
3173
+ value = this.postProcess(value, options);
3174
+ if (callback) {
3175
+ setTimeout(function() {
3176
+ callback(value);
3177
+ }, 0);
3178
+ return void 0;
3179
+ } else {
3180
+ return value;
3181
+ }
3182
+ };
3183
+ const newLen = newTokens.length, oldLen = oldTokens.length;
3184
+ let editLength = 1;
3185
+ let maxEditLength = newLen + oldLen;
3186
+ if (options.maxEditLength != null) {
3187
+ maxEditLength = Math.min(maxEditLength, options.maxEditLength);
3188
+ }
3189
+ const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity;
3190
+ const abortAfterTimestamp = Date.now() + maxExecutionTime;
3191
+ const bestPath = [{ oldPos: -1, lastComponent: void 0 }];
3192
+ let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
3193
+ if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
3194
+ return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
3195
+ }
3196
+ let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
3197
+ const execEditLength = () => {
3198
+ for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
3199
+ let basePath;
3200
+ const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
3201
+ if (removePath) {
3202
+ bestPath[diagonalPath - 1] = void 0;
3203
+ }
3204
+ let canAdd = false;
3205
+ if (addPath) {
3206
+ const addPathNewPos = addPath.oldPos - diagonalPath;
3207
+ canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
3208
+ }
3209
+ const canRemove = removePath && removePath.oldPos + 1 < oldLen;
3210
+ if (!canAdd && !canRemove) {
3211
+ bestPath[diagonalPath] = void 0;
3212
+ continue;
3213
+ }
3214
+ if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) {
3215
+ basePath = this.addToPath(addPath, true, false, 0, options);
3216
+ } else {
3217
+ basePath = this.addToPath(removePath, false, true, 1, options);
3218
+ }
3219
+ newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
3220
+ if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
3221
+ return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
3222
+ } else {
3223
+ bestPath[diagonalPath] = basePath;
3224
+ if (basePath.oldPos + 1 >= oldLen) {
3225
+ maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
3226
+ }
3227
+ if (newPos + 1 >= newLen) {
3228
+ minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
3229
+ }
3230
+ }
3231
+ }
3232
+ editLength++;
3233
+ };
3234
+ if (callback) {
3235
+ (function exec() {
3236
+ setTimeout(function() {
3237
+ if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
3238
+ return callback(void 0);
3239
+ }
3240
+ if (!execEditLength()) {
3241
+ exec();
3242
+ }
3243
+ }, 0);
3244
+ })();
3245
+ } else {
3246
+ while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
3247
+ const ret = execEditLength();
3248
+ if (ret) {
3249
+ return ret;
3250
+ }
3251
+ }
3252
+ }
3253
+ }
3254
+ addToPath(path, added, removed, oldPosInc, options) {
3255
+ const last = path.lastComponent;
3256
+ if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
3257
+ return {
3258
+ oldPos: path.oldPos + oldPosInc,
3259
+ lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
3260
+ };
3261
+ } else {
3262
+ return {
3263
+ oldPos: path.oldPos + oldPosInc,
3264
+ lastComponent: { count: 1, added, removed, previousComponent: last }
3265
+ };
3266
+ }
3267
+ }
3268
+ extractCommon(basePath, newTokens, oldTokens, diagonalPath, options) {
3269
+ const newLen = newTokens.length, oldLen = oldTokens.length;
3270
+ let oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
3271
+ while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) {
3272
+ newPos++;
3273
+ oldPos++;
3274
+ commonCount++;
3275
+ if (options.oneChangePerToken) {
3276
+ basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false };
3277
+ }
3278
+ }
3279
+ if (commonCount && !options.oneChangePerToken) {
3280
+ basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false };
3281
+ }
3282
+ basePath.oldPos = oldPos;
3283
+ return newPos;
3284
+ }
3285
+ equals(left, right, options) {
3286
+ if (options.comparator) {
3287
+ return options.comparator(left, right);
3288
+ } else {
3289
+ return left === right || !!options.ignoreCase && left.toLowerCase() === right.toLowerCase();
3290
+ }
3291
+ }
3292
+ removeEmpty(array) {
3293
+ const ret = [];
3294
+ for (let i = 0; i < array.length; i++) {
3295
+ if (array[i]) {
3296
+ ret.push(array[i]);
3297
+ }
3298
+ }
3299
+ return ret;
3300
+ }
3301
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3302
+ castInput(value, options) {
3303
+ return value;
3304
+ }
3305
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3306
+ tokenize(value, options) {
3307
+ return Array.from(value);
3308
+ }
3309
+ join(chars) {
3310
+ return chars.join("");
3311
+ }
3312
+ postProcess(changeObjects, options) {
3313
+ return changeObjects;
3314
+ }
3315
+ get useLongestToken() {
3316
+ return false;
3317
+ }
3318
+ buildValues(lastComponent, newTokens, oldTokens) {
3319
+ const components = [];
3320
+ let nextComponent;
3321
+ while (lastComponent) {
3322
+ components.push(lastComponent);
3323
+ nextComponent = lastComponent.previousComponent;
3324
+ delete lastComponent.previousComponent;
3325
+ lastComponent = nextComponent;
3326
+ }
3327
+ components.reverse();
3328
+ const componentLen = components.length;
3329
+ let componentPos = 0, newPos = 0, oldPos = 0;
3330
+ for (; componentPos < componentLen; componentPos++) {
3331
+ const component = components[componentPos];
3332
+ if (!component.removed) {
3333
+ if (!component.added && this.useLongestToken) {
3334
+ let value = newTokens.slice(newPos, newPos + component.count);
3335
+ value = value.map(function(value2, i) {
3336
+ const oldValue = oldTokens[oldPos + i];
3337
+ return oldValue.length > value2.length ? oldValue : value2;
3338
+ });
3339
+ component.value = this.join(value);
3340
+ } else {
3341
+ component.value = this.join(newTokens.slice(newPos, newPos + component.count));
3342
+ }
3343
+ newPos += component.count;
3344
+ if (!component.added) {
3345
+ oldPos += component.count;
3346
+ }
3347
+ } else {
3348
+ component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count));
3349
+ oldPos += component.count;
3350
+ }
3351
+ }
3352
+ return components;
3353
+ }
3354
+ };
3355
+
3356
+ // node_modules/diff/libesm/diff/line.js
3357
+ var LineDiff = class extends Diff {
3358
+ constructor() {
3359
+ super(...arguments);
3360
+ this.tokenize = tokenize;
3361
+ }
3362
+ equals(left, right, options) {
3363
+ if (options.ignoreWhitespace) {
3364
+ if (!options.newlineIsToken || !left.includes("\n")) {
3365
+ left = left.trim();
3366
+ }
3367
+ if (!options.newlineIsToken || !right.includes("\n")) {
3368
+ right = right.trim();
3369
+ }
3370
+ } else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
3371
+ if (left.endsWith("\n")) {
3372
+ left = left.slice(0, -1);
3373
+ }
3374
+ if (right.endsWith("\n")) {
3375
+ right = right.slice(0, -1);
3376
+ }
3377
+ }
3378
+ return super.equals(left, right, options);
3379
+ }
3380
+ };
3381
+ var lineDiff = new LineDiff();
3382
+ function diffLines(oldStr, newStr, options) {
3383
+ return lineDiff.diff(oldStr, newStr, options);
3384
+ }
3385
+ function tokenize(value, options) {
3386
+ if (options.stripTrailingCr) {
3387
+ value = value.replace(/\r\n/g, "\n");
3388
+ }
3389
+ const retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
3390
+ if (!linesAndNewlines[linesAndNewlines.length - 1]) {
3391
+ linesAndNewlines.pop();
3392
+ }
3393
+ for (let i = 0; i < linesAndNewlines.length; i++) {
3394
+ const line = linesAndNewlines[i];
3395
+ if (i % 2 && !options.newlineIsToken) {
3396
+ retLines[retLines.length - 1] += line;
3397
+ } else {
3398
+ retLines.push(line);
3399
+ }
3400
+ }
3401
+ return retLines;
3402
+ }
3403
+
3404
+ // src/diff-lines.ts
3405
+ init_color();
3406
+ function diffLinesToUnified(oldStr, newStr) {
3407
+ const parts = diffLines(oldStr, newStr);
3408
+ const lines = [];
3409
+ for (const part of parts) {
3410
+ const partLines = part.value.split("\n");
3411
+ if (partLines.at(-1) === "") {
3412
+ partLines.pop();
3413
+ }
3414
+ let prefix;
3415
+ if (part.removed) prefix = (line) => red(`-${line}`);
3416
+ else if (part.added) prefix = (line) => green(`+${line}`);
3417
+ else prefix = (line) => ` ${line}`;
3418
+ for (const line of partLines) {
3419
+ lines.push(prefix(line));
3420
+ }
3421
+ }
3422
+ return lines;
3423
+ }
3424
+
3425
+ // src/preview.ts
3426
+ init_utils();
3427
+ init_utils_json();
3428
+ function diffJsonStrings(currentJsonText, newJsonText) {
3429
+ if (currentJsonText === newJsonText) return "";
3430
+ const lines = [
3431
+ "--- ~/.claude/settings.json",
3432
+ "+++ would write",
3433
+ ...diffLinesToUnified(currentJsonText, newJsonText)
3434
+ ];
3435
+ return lines.join("\n");
3436
+ }
3437
+ function readJsonOrNull(path) {
3438
+ if (!existsSync26(path)) return null;
3439
+ try {
3440
+ return readJson(path);
3441
+ } catch {
3442
+ return null;
3443
+ }
3444
+ }
3445
+ function previewSettings(basePath, hostPath, settingsPath) {
3446
+ const base = readJsonOrNull(basePath);
3447
+ if (base === null) {
3448
+ log("settings.json: section skipped (base or current missing)");
3449
+ return;
3450
+ }
3451
+ const hostOverrides = readJsonOrNull(hostPath);
3452
+ if (hostOverrides === null && existsSync26(hostPath)) {
3453
+ log(`settings.json: malformed hosts/${HOST}.json; ignoring overrides`);
3454
+ }
3455
+ const merged = deepMerge(base, hostOverrides ?? {});
3456
+ const current = readJsonOrNull(settingsPath);
3457
+ if (current === null && existsSync26(settingsPath)) {
3458
+ log("settings.json: malformed; skipping diff");
3459
+ return;
3460
+ }
3461
+ const diff = diffJsonStrings(
3462
+ JSON.stringify(current ?? {}, null, 2),
3463
+ JSON.stringify(merged, null, 2)
3464
+ );
3465
+ if (diff === "") {
3466
+ log("settings.json: no changes");
3467
+ } else {
3468
+ log("settings.json:");
3469
+ for (const line of diff.split("\n")) log(line);
3470
+ }
3471
+ }
3472
+ function computePreview(ts, map) {
3473
+ log(`would pull on host=${HOST} (dry-run; no mutation)`);
3474
+ applySharedLinks(ts, map, { dryRun: true });
3475
+ previewSettings(
3476
+ join31(REPO_HOME, "shared", "settings.base.json"),
3477
+ join31(REPO_HOME, "hosts", `${HOST}.json`),
3478
+ join31(CLAUDE_HOME, "settings.json")
3479
+ );
3480
+ const remapResult = remapPull(ts, { dryRun: true });
3481
+ return { unmapped: remapResult.unmapped, collisions: 0 };
3482
+ }
3483
+
3484
+ // src/commands.pull.ts
3485
+ init_utils();
3486
+ init_utils_fs();
3487
+ init_utils_json();
3488
+ function applyWetPull(ts, map) {
3489
+ applySharedLinks(ts, map);
3490
+ const { label } = regenerateSettings(ts);
3491
+ const remapResult = remapPull(ts);
3492
+ const extrasResult = remapExtrasPull(ts);
3493
+ const summary = section("Summary");
3494
+ addItem(
3495
+ summary,
3496
+ summaryRow("pull", remapResult.unmapped + extrasResult.unmapped, 0, extrasResult.skipped)
3497
+ );
3498
+ renderTree([
3499
+ buildSettingsSection(label),
3500
+ buildSessionsSection(remapResult.pulled, remapResult.unmapped),
3501
+ buildExtrasSection(extrasResult.pulled, extrasResult.skipped),
3502
+ summary
3503
+ ]);
3504
+ }
3505
+ function cmdPull(opts = {}) {
3506
+ const dryRun = opts.dryRun === true;
3507
+ if (!existsSync27(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
3508
+ if (!existsSync27(join32(REPO_HOME, "shared", "settings.base.json"))) {
3509
+ die("repo not initialized; run 'nomad init' to scaffold");
3510
+ }
3511
+ const handle = acquireLock("pull");
3512
+ if (handle === null) process.exit(0);
3513
+ try {
3514
+ const ts = freshBackupTs(BACKUP_BASE);
3515
+ if (!dryRun) {
3516
+ const backupRoot = join32(BACKUP_BASE, ts);
3517
+ try {
3518
+ mkdirSync7(backupRoot, { recursive: true });
3519
+ } catch (err) {
3520
+ die(`could not create backup dir: ${err.message}`);
3521
+ }
3522
+ }
3523
+ log(
3524
+ dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
3525
+ );
3526
+ gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", REPO_HOME);
3527
+ const mapPath = join32(REPO_HOME, "path-map.json");
3528
+ const map = existsSync27(mapPath) ? readPathMap(mapPath) : { projects: {} };
3529
+ divergenceCheckExtras(ts);
3530
+ if (dryRun) {
3531
+ const previewResult = computePreview(ts, map);
3532
+ log("dry-run complete; no mutation");
3533
+ emitSummary("pull", previewResult.unmapped);
3534
+ } else {
3535
+ applyWetPull(ts, map);
3536
+ }
3537
+ } catch (err) {
3538
+ if (err instanceof NomadFatal) {
3539
+ fail(err.message);
3540
+ process.exitCode = 1;
3541
+ } else {
3542
+ throw err;
3543
+ }
3544
+ } finally {
3545
+ releaseLock(handle);
3546
+ }
3547
+ }
3548
+
3549
+ // src/commands.push.ts
3550
+ init_config();
3551
+ import { existsSync as existsSync29 } from "node:fs";
3552
+ import { join as join36, relative as relative5 } from "node:path";
3553
+
3554
+ // src/commands.push.allowlist.ts
3555
+ init_config();
3556
+ init_config_sharedDirs_guard();
3557
+ init_utils();
3558
+ function isAllowed(path, allowed) {
3559
+ for (const entry of allowed) {
3560
+ if (path === entry) return true;
3561
+ if (entry === "hosts/") {
3562
+ if (/^hosts\/[^/]+\.json$/.test(path)) return true;
3563
+ continue;
3564
+ }
3565
+ if (entry.endsWith("/") && path.startsWith(entry)) return true;
3566
+ }
3567
+ return false;
3568
+ }
3569
+ function isNeverSync(path) {
3570
+ const blockSet = path.startsWith("shared/extras/") ? ALWAYS_NEVER_SYNC : NEVER_SYNC;
3571
+ for (const segment of path.split("/")) {
3572
+ if (blockSet.has(segment)) return true;
3573
+ }
3574
+ return false;
3575
+ }
3576
+ function parsePorcelainZ(statusPorcelain) {
3577
+ const records = statusPorcelain.split("\0");
3578
+ const paths = [];
3579
+ for (let i = 0; i < records.length; i++) {
3580
+ const rec = records[i];
3581
+ if (rec === void 0 || rec === "") continue;
3582
+ if (rec.length < 4) continue;
3583
+ const xy = rec.slice(0, 2);
3584
+ const newPath = rec.slice(3);
3585
+ paths.push(newPath);
3586
+ if (/[RC]/.test(xy)) {
3587
+ const oldPath = records[i + 1];
3588
+ if (oldPath !== void 0 && oldPath !== "") paths.push(oldPath);
3589
+ i++;
3590
+ }
3591
+ }
3592
+ return paths;
3593
+ }
3594
+ function enforceAllowList(statusPorcelain, map) {
3595
+ const extrasWhitelist = SUPPORTED_EXTRAS;
3596
+ const allowed = [
3597
+ ...PUSH_ALLOWED_STATIC,
3598
+ ...Object.keys(map.projects).map((l) => `shared/projects/${l}/`),
3599
+ ...Object.entries(map.extras ?? {}).flatMap(
3600
+ ([l, names]) => names.filter((n) => extrasWhitelist.includes(n)).flatMap((n) => [`shared/extras/${l}/${n}`, `shared/extras/${l}/${n}/`])
3601
+ ),
3602
+ ...(map.sharedDirs ?? []).filter((d) => isValidSharedDir(d)).map((d) => `shared/${d}/`)
3603
+ ];
3604
+ const neverSyncHits = [];
3605
+ const violations = [];
3606
+ for (const path of parsePorcelainZ(statusPorcelain)) {
3607
+ if (isNeverSync(path)) {
3608
+ neverSyncHits.push(path);
3609
+ } else if (!isAllowed(path, allowed)) {
3610
+ violations.push(path);
3611
+ }
3612
+ }
3613
+ if (neverSyncHits.length === 0 && violations.length === 0) return;
3614
+ for (const p of neverSyncHits) {
3615
+ fail(`${p} is in NEVER_SYNC and must never be pushed`);
3616
+ }
3617
+ for (const p of violations) {
3618
+ fail(`to sync ${p}, add to PUSH_ALLOWED in src/config.ts`);
3619
+ }
3620
+ throw new NomadFatal("push allow-list violations");
3621
+ }
3622
+
3623
+ // src/commands.push.recovery.ts
3624
+ init_config();
3625
+ import { createInterface } from "node:readline/promises";
3626
+
3627
+ // src/commands.push.recovery.redact.ts
3628
+ init_config();
3629
+ import { cpSync as cpSync5, readFileSync as readFileSync9, statSync as statSync7, writeFileSync as writeFileSync5 } from "node:fs";
3630
+ import { join as join33, sep as sep2 } from "node:path";
3631
+ init_push_gitleaks_scan();
3632
+ init_utils_fs();
3633
+ init_utils_json();
3634
+ init_utils();
3635
+
3636
+ // src/commands.push.recovery.seams.ts
3637
+ init_push_gitleaks();
3638
+ function findingKey(f) {
3639
+ return `${f.File}:${f.StartLine}:${f.StartColumn}:${f.RuleID}`;
3640
+ }
3641
+ var VALID_SID = /^[A-Za-z0-9_-]+$/;
3642
+ function sessionIdFromFinding(f) {
3643
+ const m = SESSION_PATH.exec(f.File) ?? /^shared\/projects\/[^/]+\/([^/]+)\//.exec(f.File);
3644
+ if (m === null) return null;
3645
+ const sid = m[1];
3646
+ return VALID_SID.test(sid) ? sid : null;
3647
+ }
3648
+ function parseAction(raw) {
3649
+ const t = raw.trim().toLowerCase();
3650
+ if (t === "r" || t === "redact") return "redact";
3651
+ if (t === "a" || t === "allow") return "allow";
3652
+ if (t === "d" || t === "drop") return "drop";
3653
+ return "skip";
3654
+ }
3655
+
3656
+ // src/commands.push.recovery.redact.ts
3657
+ function applyRedact(f, allFindings, ts, map, nowMs, scan = scanFile) {
3658
+ const refuse = (msg) => {
3659
+ log(msg);
3660
+ return false;
3661
+ };
3662
+ const sid = sessionIdFromFinding(f);
3663
+ if (sid === null) {
3664
+ return refuse(
3665
+ `could not locate the local transcript for this finding; choose Skip or Drop session.`
3666
+ );
3667
+ }
3668
+ const localPath = resolveLiveTranscript2(sid);
3669
+ if (localPath === null) {
3670
+ return refuse(
3671
+ `could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
3672
+ );
3673
+ }
3674
+ if (isRecentlyModified(statSync7(localPath).mtimeMs, nowMs())) {
3675
+ return refuse(
3676
+ `session ${sid} looks active (modified within the last 5 minutes); refusing to redact, no changes made.
3677
+ End the session and choose Redact again, or choose Drop session (holds this session back from the push, local copy kept) or Skip.`
3678
+ );
3679
+ }
3680
+ const realFindings = scan(localPath);
3681
+ if (realFindings === null) {
3682
+ return refuse(`re-scan of the transcript failed; choose Skip or Drop session.`);
3683
+ }
3684
+ if (realFindings.length === 0) {
3685
+ return refuse(
3686
+ `nothing to redact in the local transcript for session ${sid}; choose Skip or Drop session.`
3687
+ );
3688
+ }
3689
+ backupBeforeWrite(localPath, ts);
3690
+ writeFileSync5(localPath, applyRedactions(readFileSync9(localPath, "utf8"), realFindings), "utf8");
3691
+ let copied = false;
3692
+ for (const [logical, hostMap] of Object.entries(map.projects)) {
3693
+ const abs = hostMap[HOST];
3694
+ if (abs === void 0) continue;
3695
+ if (localPath.startsWith(join33(CLAUDE_HOME, "projects", encodePath(abs)) + sep2)) {
3696
+ cpSync5(localPath, join33(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`), {
3697
+ force: true
3698
+ });
3699
+ copied = true;
3700
+ break;
3701
+ }
3702
+ }
3703
+ if (!copied) {
3704
+ return refuse(
3705
+ `could not map the local transcript for session ${sid} to a staged copy; choose Drop session or Skip.`
3706
+ );
3707
+ }
3708
+ return true;
3709
+ }
3710
+
3711
+ // src/commands.push.recovery.drop.ts
3712
+ init_config();
3713
+ import { rmSync as rmSync9 } from "node:fs";
3714
+ import { join as join34 } from "node:path";
3715
+ function dropSessionFromStaged(sid, map) {
3716
+ const logicals = Object.keys(map.projects);
3717
+ if (logicals.length === 0) return false;
3718
+ for (const logical of logicals) {
3719
+ const jsonl = join34(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`);
3720
+ const dir = join34(REPO_HOME, "shared", "projects", logical, sid);
3721
+ rmSync9(jsonl, { force: true });
3722
+ rmSync9(dir, { recursive: true, force: true });
3723
+ }
3724
+ return true;
3725
+ }
3726
+
3727
+ // src/commands.push.recovery.actions.ts
3728
+ init_push_gitleaks_scan();
3729
+ init_utils();
3730
+ function applyAllow(f) {
3731
+ appendGitleaksIgnore(f.Fingerprint);
3732
+ }
3733
+ async function collectActions(findings, prompt) {
3734
+ const actions = /* @__PURE__ */ new Map();
3735
+ for (const f of findings) {
3736
+ const sid = sessionIdFromFinding(f);
3737
+ const header = `
3738
+ Finding: ${f.RuleID} in ${f.File} line ${f.StartLine}` + (sid === null ? "" : ` (session: ${sid})`) + "\n [R]edact [A]llow [D]rop session [S]kip (default)\n";
3739
+ actions.set(findingKey(f), parseAction(await prompt(header + "> ")));
3740
+ }
3741
+ return actions;
3742
+ }
3743
+ function dispatchOne(f, ctx) {
3744
+ const action = ctx.actions.get(findingKey(f)) ?? "skip";
3745
+ if (action === "skip") return;
3746
+ const sid = sessionIdFromFinding(f);
3747
+ if (sid !== null && ctx.droppedSids.has(sid)) return;
3748
+ if (action === "allow") {
3749
+ applyAllow(f);
3750
+ return;
3751
+ }
3752
+ if (sid === null) return;
3753
+ if (action === "drop") {
3754
+ ctx.droppedSids.add(sid);
3755
+ if (ctx.drop(sid, ctx.map)) {
3756
+ log(
3757
+ `dropped session ${sid} from this push (local transcript kept; the secret remains in your local copy)`
3758
+ );
3759
+ }
3760
+ return;
3761
+ }
3762
+ if (action === "redact" && !ctx.redactedSids.has(sid)) {
3763
+ if (applyRedact(f, ctx.findings, ctx.ts, ctx.map, ctx.nowMs, ctx.scan))
3764
+ ctx.redactedSids.add(sid);
3765
+ }
3766
+ }
3767
+ function dispatchActions(findings, actions, ts, map, nowMs, scan = scanFile, drop = dropSessionFromStaged) {
3768
+ const ctx = {
3769
+ findings,
3770
+ actions,
3771
+ ts,
3772
+ map,
3773
+ nowMs,
3774
+ scan,
3775
+ drop,
3776
+ redactedSids: /* @__PURE__ */ new Set(),
3777
+ droppedSids: /* @__PURE__ */ new Set()
3778
+ };
3779
+ for (const f of findings) {
3780
+ dispatchOne(f, ctx);
3781
+ }
3782
+ }
3783
+ function redactAllFindings(findings, ts, map, nowMs, scan = scanFile) {
3784
+ const redactedSids = /* @__PURE__ */ new Set();
3785
+ for (const f of findings) {
3786
+ const sid = sessionIdFromFinding(f);
3787
+ if (sid === null || redactedSids.has(sid)) continue;
3788
+ if (applyRedact(f, findings, ts, map, nowMs, scan)) redactedSids.add(sid);
3789
+ }
3790
+ }
3791
+
3792
+ // src/commands.push.recovery.ts
3793
+ init_push_gitleaks_scan();
3794
+ init_push_gitleaks();
3795
+ init_utils();
3796
+ function isTTY(stdin = process.stdin, stdout = process.stdout) {
3797
+ return stdin.isTTY === true && stdout.isTTY === true;
3798
+ }
3799
+ function hasUnresolved(actions) {
3800
+ for (const action of actions.values()) {
3801
+ if (action === "skip") return true;
3802
+ }
3803
+ return false;
3804
+ }
3805
+ function printRecoveryLegend(print = console.log) {
3806
+ print("");
3807
+ print("Recovery actions:");
3808
+ print(" Redact - scrub the secret from the local transcript, push the cleaned copy");
3809
+ print(" Allow - mark as false positive (adds a .gitleaksignore fingerprint), push as-is");
3810
+ print(" Drop session - exclude this session from this push (local transcript kept, running");
3811
+ print(" session is not stopped)");
3812
+ print(" Skip - leave unresolved (the push aborts)");
3813
+ print("");
3814
+ }
3815
+ function makeRealPrompt() {
3816
+ return async (prompt) => {
3817
+ const rl = createInterface({
3818
+ input: process.stdin,
3819
+ output: process.stdout,
3820
+ terminal: true
3821
+ });
3822
+ try {
3823
+ return await rl.question(prompt);
3824
+ } finally {
3825
+ rl.close();
3826
+ }
3827
+ };
3828
+ }
3829
+ async function resolveLeakFindings(verdict, ts, map, deps = {}) {
3830
+ const {
3831
+ isTTYCheck = isTTY,
3832
+ nowMs = Date.now,
3833
+ redactAll = false,
3834
+ makePrompt: makePromptFn = makeRealPrompt,
3835
+ scan = scanFile,
3836
+ printLegend = printRecoveryLegend
3837
+ } = deps;
3838
+ const scanVerdict = deps.scanVerdict ?? (await Promise.resolve().then(() => (init_push_leak_verdict(), push_leak_verdict_exports))).scanPushVerdict;
3839
+ let current = verdict;
3840
+ if (redactAll) {
3841
+ redactAllFindings(current.findings, ts, map, nowMs, scan);
3842
+ gitOrFatal(["add", "-A"], "git add", REPO_HOME);
3843
+ const next = scanVerdict();
3844
+ if (next.leak) {
3845
+ const { bySession, other } = partitionFindings(next.findings);
3846
+ throw new NomadFatal(buildSessionAwareFatal(bySession, other));
3847
+ }
3848
+ return next;
3849
+ }
3850
+ if (!isTTYCheck()) {
3851
+ throw new NomadFatal(current.recovery ?? "gitleaks detected secrets");
3852
+ }
3853
+ const prompt = makePromptFn();
3854
+ printLegend();
3855
+ while (current.leak && current.findings.length > 0) {
3856
+ const actions = await collectActions(current.findings, prompt);
3857
+ if (hasUnresolved(actions)) {
3858
+ const unresolved = current.findings.filter((f) => actions.get(findingKey(f)) === "skip");
3859
+ const { bySession, other } = partitionFindings(unresolved);
3860
+ throw new NomadFatal(buildSessionAwareFatal(bySession, other));
3861
+ }
3862
+ dispatchActions(current.findings, actions, ts, map, nowMs, scan);
3863
+ gitOrFatal(["add", "-A"], "git add", REPO_HOME);
3864
+ current = scanVerdict();
3865
+ }
3866
+ return current;
3867
+ }
3868
+
3869
+ // src/commands.push.ts
3870
+ init_push_leak_verdict();
3871
+ init_push_checks();
3872
+
3873
+ // src/push-preview.ts
3874
+ init_color();
3875
+ init_config();
3876
+ import { randomBytes as randomBytes2 } from "node:crypto";
3877
+ import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as readdirSync9, rmSync as rmSync10 } from "node:fs";
3878
+ import { homedir as homedir5 } from "node:os";
3879
+ import { join as join35 } from "node:path";
3880
+ init_push_leak_verdict();
3881
+ init_push_gitleaks();
3882
+ init_utils_fs();
3883
+ init_utils_json();
3884
+ var NOTHING_TO_SCAN_ROW = `${dim(infoGlyph)} nothing to scan, no leaks`;
3885
+ function stageSessions(tmpRoot, map) {
3886
+ if (typeof map.projects !== "object" || map.projects === null) return 0;
3887
+ const reverse = /* @__PURE__ */ new Map();
3888
+ for (const [logical, hosts] of Object.entries(map.projects)) {
3889
+ assertSafeLogical(logical);
3890
+ const p = hosts[HOST];
3891
+ if (!p || p === "TBD") continue;
3892
+ reverse.set(encodePath(p), logical);
3893
+ }
3894
+ const localProjects = join35(CLAUDE_HOME, "projects");
3895
+ if (!existsSync28(localProjects)) return 0;
3896
+ let staged = 0;
3897
+ for (const dir of readdirSync9(localProjects)) {
3898
+ const logical = reverse.get(dir);
3899
+ if (!logical) continue;
3900
+ copyDirJsonlOnly(join35(localProjects, dir), join35(tmpRoot, "shared", "projects", logical));
3901
+ staged++;
3902
+ }
3903
+ return staged;
3904
+ }
3905
+ function stageExtras(tmpRoot, map) {
3906
+ if (typeof map.projects !== "object" || map.projects === null) return 0;
3907
+ const extrasMap = map.extras ?? {};
3908
+ const whitelist = SUPPORTED_EXTRAS;
3909
+ let staged = 0;
3910
+ for (const [logical, dirnames] of Object.entries(extrasMap)) {
3911
+ assertSafeLogical(logical);
3912
+ const localRoot = map.projects[logical]?.[HOST];
3913
+ if (!localRoot || localRoot === "TBD") continue;
3914
+ for (const dirname4 of dirnames) {
3915
+ if (!whitelist.includes(dirname4)) continue;
3916
+ const src = join35(localRoot, dirname4);
3917
+ if (!existsSync28(src)) continue;
3918
+ const dst = join35(tmpRoot, "shared", "extras", logical, dirname4);
3919
+ copyExtras(src, dst);
3920
+ staged++;
3921
+ }
3922
+ }
3923
+ return staged;
3924
+ }
3925
+ function previewPushLeaks(map) {
3926
+ const cacheDir = join35(homedir5(), ".cache", "claude-nomad");
3927
+ mkdirSync8(cacheDir, { recursive: true });
3928
+ const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
3929
+ const tmpRoot = join35(cacheDir, `push-preview-tree-${stamp}`);
3930
+ try {
3931
+ const sessionCount = stageSessions(tmpRoot, map);
3932
+ const extrasCount = stageExtras(tmpRoot, map);
3933
+ if (sessionCount + extrasCount === 0) {
3934
+ return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
3935
+ }
3936
+ let findings;
3937
+ try {
3938
+ findings = scanStagedTree(tmpRoot);
3939
+ } catch (err) {
3940
+ if (err.code === "ENOENT") {
3941
+ return verdictScanError("scan error (git or gitleaks not on PATH)");
3942
+ }
3943
+ return verdictScanError(`scan error: ${err.message}`);
3944
+ }
3945
+ return verdictFromFindings(findings);
3946
+ } finally {
3947
+ rmSync10(tmpRoot, { recursive: true, force: true });
3948
+ }
3949
+ }
3950
+
3951
+ // src/commands.push.ts
3952
+ init_utils();
3953
+ init_utils_fs();
3954
+ init_utils_json();
3955
+ function guardGitlinks() {
3956
+ const gitlinks = findGitlinks(join36(REPO_HOME, "shared"));
3957
+ if (gitlinks.length === 0) return;
3958
+ for (const p of gitlinks) {
3959
+ const rel = relative5(REPO_HOME, p);
3960
+ fail(`gitlink: ${rel} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`);
3961
+ }
3962
+ const noun = gitlinks.length === 1 ? "entry" : "entries";
3963
+ throw new NomadFatal(
3964
+ `gitlink trap: ${gitlinks.length} nested .git ${noun} in shared/; remove before retry`
3965
+ );
3966
+ }
3967
+ async function commitAndPush(st, ts, map, redactAll) {
3968
+ gitOrFatal(["add", "-A"], "git add", REPO_HOME);
3969
+ let verdict = scanPushVerdict();
3970
+ if (verdict.leak) {
3971
+ renderPushTree(st, verdict);
3972
+ verdict = await resolveLeakFindings(verdict, ts, map, { redactAll });
3973
+ }
3974
+ gitOrFatal(["commit", "-m", `chore: sync from ${HOST}`], "git commit", REPO_HOME);
3975
+ gitOrFatal(["push"], "git push", REPO_HOME);
3976
+ renderPushTree(st, verdict);
3977
+ }
3978
+ function runDryRunPreview(st, map) {
3979
+ if (map === null) {
3980
+ renderNoScanTree(st, { noMapHint: true });
3981
+ return;
3982
+ }
3983
+ const verdict = previewPushLeaks(map);
3984
+ renderPushTree(st, verdict);
3985
+ if (verdict.recovery !== null) fail(verdict.recovery);
3986
+ }
3987
+ async function cmdPush(opts = {}) {
3988
+ const dryRun = opts.dryRun === true;
3989
+ const redactAll = opts.redactAll === true;
3990
+ if (!existsSync29(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
3991
+ const handle = acquireLock("push");
3992
+ if (handle === null) process.exit(0);
3993
+ try {
3994
+ console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
3995
+ probeGitleaks();
3996
+ rebaseBeforePush();
3997
+ const ts = freshBackupTs(BACKUP_BASE);
3998
+ const remap = remapPush(ts, { dryRun });
3999
+ const extras = remapExtrasPush(ts, { dryRun });
4000
+ const st = { dryRun, remap, extras };
4001
+ guardGitlinks();
4002
+ const status = gitStatusPorcelainZ(REPO_HOME, { untrackedAll: true });
4003
+ if (!dryRun && !status) {
4004
+ log("nothing to commit");
4005
+ renderNoScanTree(st);
4006
+ return;
4007
+ }
4008
+ const mapPath = join36(REPO_HOME, "path-map.json");
4009
+ if (!existsSync29(mapPath)) {
4010
+ if (dryRun) return runDryRunPreview(st, null);
4011
+ die("path-map.json missing, cannot enforce push allow-list");
4012
+ }
4013
+ const map = readPathMap(mapPath);
4014
+ if (status) enforceAllowList(status, map);
4015
+ if (dryRun) return runDryRunPreview(st, map);
4016
+ await commitAndPush(st, ts, map, redactAll);
4017
+ } catch (err) {
4018
+ if (err instanceof NomadFatal) {
4019
+ fail(err.message);
4020
+ process.exitCode = 1;
4021
+ } else {
4022
+ throw err;
4023
+ }
4024
+ } finally {
4025
+ releaseLock(handle);
4026
+ }
4027
+ }
4028
+
4029
+ // src/commands.update.ts
4030
+ import { execFileSync as execFileSync15 } from "node:child_process";
4031
+ init_utils();
4032
+ function cmdUpdate(run = execFileSync15) {
4033
+ try {
4034
+ run("npm", ["update", "-g", "claude-nomad"], { stdio: "inherit" });
4035
+ } catch (err) {
4036
+ const e = err;
4037
+ if (e.code === "ENOENT") {
4038
+ throw new NomadFatal("npm not found on PATH; install Node.js/npm and retry.");
4039
+ }
4040
+ throw new NomadFatal(`npm update -g claude-nomad failed: ${e.message}`);
4041
+ }
4042
+ }
4043
+
4044
+ // src/nomad.ts
4045
+ init_config();
4046
+
4047
+ // src/diff.ts
4048
+ init_config();
4049
+ import { existsSync as existsSync30 } from "node:fs";
4050
+ import { join as join37 } from "node:path";
4051
+ init_utils();
4052
+ init_utils_fs();
4053
+ init_utils_json();
4054
+ function cmdDiff() {
4055
+ try {
4056
+ if (!existsSync30(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4057
+ const ts = freshBackupTs(BACKUP_BASE);
4058
+ const mapPath = join37(REPO_HOME, "path-map.json");
4059
+ const map = existsSync30(mapPath) ? readPathMap(mapPath) : { projects: {} };
4060
+ const result = computePreview(ts, map);
4061
+ emitSummary("diff", result.unmapped);
4062
+ } catch (err) {
4063
+ if (err instanceof NomadFatal) {
4064
+ fail(err.message);
4065
+ process.exitCode = 1;
4066
+ return;
4067
+ }
4068
+ throw err;
4069
+ }
4070
+ }
4071
+
4072
+ // src/init.ts
4073
+ init_config();
4074
+ import { existsSync as existsSync32, mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "node:fs";
4075
+ import { join as join39 } from "node:path";
4076
+
4077
+ // src/init.gh-onboard.ts
4078
+ init_config();
4079
+ import { execFileSync as execFileSync16 } from "node:child_process";
4080
+ init_utils();
4081
+ var DEFAULT_REPO_NAME = "claude-nomad-config";
4082
+ function isValidRepoName(name) {
4083
+ return /^[A-Za-z0-9._-]{1,100}$/.test(name);
4084
+ }
4085
+ var GH_NETWORK_TIMEOUT_MS = 3e4;
4086
+ function ensureOriginRepo(repoName, run = execFileSync16) {
4087
+ if (!isValidRepoName(repoName)) {
4088
+ die(
4089
+ `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
4090
+ );
4091
+ }
4092
+ try {
4093
+ readOriginRemote(REPO_HOME, run);
4094
+ return;
4095
+ } catch {
4096
+ }
4097
+ const ghStatus = ghAuthStatus(run);
4098
+ if (ghStatus === "gh-not-installed") {
4099
+ die("gh CLI is required for nomad init. Install: https://cli.github.com");
4100
+ }
4101
+ if (ghStatus === "gh-probe-error") {
4102
+ die("could not verify gh CLI status (network issue?). Retry, or check `gh auth status`.");
4103
+ }
4104
+ if (ghStatus !== null) {
4105
+ die("gh CLI is not authenticated. Run `gh auth login` and retry.");
4106
+ }
4107
+ try {
4108
+ run("git", ["init", "-b", "main"], { cwd: REPO_HOME, stdio: ["ignore", "ignore", "pipe"] });
4109
+ } catch (err) {
4110
+ const e = err;
4111
+ throw new NomadFatal(`git init failed: ${e.message}`);
4112
+ }
4113
+ try {
4114
+ run("gh", ["repo", "create", repoName, "--private"], {
4115
+ stdio: ["ignore", "pipe", "pipe"],
4116
+ timeout: GH_NETWORK_TIMEOUT_MS
4117
+ });
4118
+ } catch (err) {
4119
+ const e = err;
4120
+ const detail = String(e.stderr ?? "") + e.message;
4121
+ if (!/already exists/i.test(detail)) {
4122
+ throw new NomadFatal(`gh repo create failed: ${e.message}`);
4123
+ }
4124
+ log(`repo ${repoName} already exists on your account; reusing it and wiring origin`);
4125
+ }
4126
+ let owner;
4127
+ try {
4128
+ owner = run("gh", ["api", "user", "--jq", ".login"], {
4129
+ stdio: ["ignore", "pipe", "ignore"],
4130
+ timeout: GH_NETWORK_TIMEOUT_MS
4131
+ }).toString().trim();
4132
+ } catch (err) {
4133
+ const e = err;
4134
+ throw new NomadFatal(`gh api user failed: ${e.message}`);
4135
+ }
4136
+ if (owner.length === 0 || owner === "null") {
4137
+ throw new NomadFatal("gh api user returned an empty login; cannot wire origin remote.");
4138
+ }
4139
+ try {
4140
+ run("git", ["remote", "add", "origin", `git@github.com:${owner}/${repoName}.git`], {
4141
+ cwd: REPO_HOME,
4142
+ stdio: ["ignore", "ignore", "pipe"]
4143
+ });
4144
+ } catch (err) {
4145
+ const e = err;
4146
+ throw new NomadFatal(`git remote add failed: ${e.message}`);
4147
+ }
4148
+ log(`created private repo ${owner}/${repoName} and wired origin`);
4149
+ }
4150
+
4151
+ // src/init.snapshot.ts
4152
+ init_config();
4153
+ init_utils();
4154
+ init_utils_fs();
4155
+ init_utils_json();
4156
+ import { copyFileSync, cpSync as cpSync6, existsSync as existsSync31, rmSync as rmSync11, statSync as statSync8 } from "node:fs";
4157
+ import { join as join38 } from "node:path";
4158
+ function snapshotIntoShared(map) {
4159
+ for (const name of allSharedLinks(map)) {
4160
+ const src = join38(CLAUDE_HOME, name);
4161
+ if (!existsSync31(src)) continue;
4162
+ const dst = join38(REPO_HOME, "shared", name);
4163
+ if (statSync8(src).isDirectory()) {
4164
+ const gk = join38(dst, ".gitkeep");
4165
+ if (existsSync31(gk)) rmSync11(gk);
4166
+ cpSync6(src, dst, { recursive: true, force: false, errorOnExist: true });
4167
+ } else {
4168
+ copyFileSync(src, dst);
4169
+ }
4170
+ log(`snapshotted shared/${name} from ${src}`);
4171
+ }
4172
+ const userSettings = join38(CLAUDE_HOME, "settings.json");
4173
+ if (existsSync31(userSettings)) {
4174
+ let parsed;
4175
+ try {
4176
+ parsed = readJson(userSettings);
4177
+ } catch (err) {
4178
+ return die(`malformed ${userSettings}: ${err.message}`);
4179
+ }
4180
+ const hostFile = join38(REPO_HOME, "hosts", `${HOST}.json`);
4181
+ writeJsonAtomic(hostFile, parsed);
4182
+ log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
4183
+ }
4184
+ }
4185
+
4186
+ // src/init.ts
4187
+ init_utils();
4188
+ init_utils_fs();
4189
+ var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.claude/CLAUDE.md by nomad pull -->\n";
4190
+ var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
4191
+ function preflightConflict(repoHome) {
4192
+ const candidates = [
4193
+ join39(repoHome, "shared", "settings.base.json"),
4194
+ join39(repoHome, "shared", "CLAUDE.md"),
4195
+ join39(repoHome, "path-map.json"),
4196
+ join39(repoHome, "hosts"),
4197
+ join39(repoHome, "shared")
4198
+ ];
4199
+ for (const c of candidates) {
4200
+ if (existsSync32(c)) return c;
4201
+ }
4202
+ return null;
4203
+ }
4204
+ function cmdInit(opts = {}) {
4205
+ const snapshot = opts.snapshot === true;
4206
+ const keepActions = opts.keepActions === true;
4207
+ mkdirSync9(REPO_HOME, { recursive: true });
4208
+ const conflict = preflightConflict(REPO_HOME);
4209
+ if (conflict !== null) {
4210
+ die(`already initialized; refusing to clobber ${conflict}`);
4211
+ }
4212
+ ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
4213
+ mkdirSync9(join39(REPO_HOME, "shared"), { recursive: true });
4214
+ mkdirSync9(join39(REPO_HOME, "hosts"), { recursive: true });
4215
+ for (const name of SHARED_KEEP_DIRS) {
4216
+ mkdirSync9(join39(REPO_HOME, "shared", name), { recursive: true });
4217
+ }
4218
+ const userClaudeMd = join39(CLAUDE_HOME, "CLAUDE.md");
4219
+ if (!snapshot || !existsSync32(userClaudeMd)) {
4220
+ writeFileSync6(join39(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
4221
+ log("created shared/CLAUDE.md");
4222
+ }
4223
+ for (const name of SHARED_KEEP_DIRS) {
4224
+ writeFileSync6(join39(REPO_HOME, "shared", name, ".gitkeep"), "");
4225
+ log(`created shared/${name}/.gitkeep`);
4226
+ }
4227
+ writeFileSync6(join39(REPO_HOME, "hosts", ".gitkeep"), "");
4228
+ log("created hosts/.gitkeep");
4229
+ writeJsonAtomic(join39(REPO_HOME, "shared", "settings.base.json"), {});
4230
+ log("created shared/settings.base.json");
4231
+ writeJsonAtomic(join39(REPO_HOME, "path-map.json"), { projects: {} });
4232
+ log("created path-map.json");
4233
+ if (snapshot) {
4234
+ snapshotIntoShared({ projects: {} });
4235
+ log(`snapshot staged in shared/; review, then 'nomad push' to share with other hosts.`);
4236
+ log("~/.claude/ originals were NOT removed.");
4237
+ }
4238
+ if (!keepActions) {
4239
+ maybeDisableMirrorActions(REPO_HOME, opts.run);
4240
+ }
4241
+ log("init complete");
4242
+ }
4243
+ function maybeDisableMirrorActions(repoHome, run) {
4244
+ let remote;
4245
+ try {
4246
+ remote = readOriginRemote(repoHome, run);
4247
+ } catch {
4248
+ return;
4249
+ }
4250
+ const ref = parseGitHubRemote(remote);
4251
+ if (ref === null) return;
4252
+ const ghStatus = ghAuthStatus(run);
4253
+ if (ghStatus === "gh-not-installed") {
4254
+ log(
4255
+ `tip: install gh CLI and run 'gh api -X PUT repos/${ref.owner}/${ref.repo}/actions/permissions -F enabled=false' to disable Actions on your private mirror.`
4256
+ );
4257
+ return;
4258
+ }
4259
+ if (ghStatus === "gh-not-authed") {
4260
+ log(
4261
+ `tip: run 'gh auth login' then 'gh api -X PUT repos/${ref.owner}/${ref.repo}/actions/permissions -F enabled=false' to disable Actions on your private mirror.`
4262
+ );
4263
+ return;
4264
+ }
4265
+ let isPrivate;
4266
+ try {
4267
+ isPrivate = isRepoPrivate(ref, run);
4268
+ } catch {
4269
+ log(
4270
+ `could not determine privacy for ${ref.owner}/${ref.repo}; run 'gh api -X PUT repos/${ref.owner}/${ref.repo}/actions/permissions -F enabled=false' manually if it is private.`
4271
+ );
4272
+ return;
4273
+ }
4274
+ if (!isPrivate) return;
4275
+ let alreadyDisabled = false;
4276
+ try {
4277
+ alreadyDisabled = !isActionsEnabled(ref, run);
4278
+ } catch {
4279
+ }
4280
+ if (alreadyDisabled) {
4281
+ log(`actions already disabled on ${ref.owner}/${ref.repo}`);
4282
+ return;
4283
+ }
4284
+ try {
4285
+ disableActions(ref, run);
4286
+ log(`disabled GitHub Actions on private mirror ${ref.owner}/${ref.repo}`);
4287
+ } catch {
4288
+ log(
4289
+ `could not auto-disable Actions on ${ref.owner}/${ref.repo}; run 'gh api -X PUT repos/${ref.owner}/${ref.repo}/actions/permissions -F enabled=false' manually.`
4290
+ );
4291
+ }
4292
+ }
4293
+
4294
+ // src/nomad.dispatch.ts
4295
+ function parseFlags(argv, known) {
4296
+ const seen = /* @__PURE__ */ new Set();
4297
+ for (let i = 3; i < argv.length; i++) {
4298
+ const flag = argv[i];
4299
+ if (!known.has(flag) || seen.has(flag)) {
4300
+ return null;
4301
+ }
4302
+ seen.add(flag);
4303
+ }
4304
+ return seen;
4305
+ }
4306
+ function extractFlagValue(argv, i) {
4307
+ const val = argv[i + 1];
4308
+ if (val === void 0 || val.startsWith("--")) return null;
4309
+ return val;
4310
+ }
4311
+ function applyInitToken(argv, i, st) {
4312
+ const token = argv[i];
4313
+ if (token === "--snapshot") {
4314
+ if (st.sawSnapshot) return { ok: false, advance: 0 };
4315
+ st.sawSnapshot = true;
4316
+ st.snapshot = true;
4317
+ return { ok: true, advance: 1 };
4318
+ }
4319
+ if (token === "--keep-actions") {
4320
+ if (st.sawKeepActions) return { ok: false, advance: 0 };
4321
+ st.sawKeepActions = true;
4322
+ st.keepActions = true;
4323
+ return { ok: true, advance: 1 };
4324
+ }
4325
+ if (token === "--repo") {
4326
+ if (st.sawRepo) return { ok: false, advance: 0 };
4327
+ st.sawRepo = true;
4328
+ const val = extractFlagValue(argv, i);
4329
+ if (val === null) return { ok: false, advance: 0 };
4330
+ st.repoName = val;
4331
+ return { ok: true, advance: 2 };
4332
+ }
4333
+ return { ok: false, advance: 0 };
4334
+ }
4335
+ function parseInitArgs(argv) {
4336
+ const st = {
4337
+ snapshot: false,
4338
+ keepActions: false,
4339
+ repoName: void 0,
4340
+ sawSnapshot: false,
4341
+ sawKeepActions: false,
4342
+ sawRepo: false
4343
+ };
4344
+ let i = 3;
4345
+ while (i < argv.length) {
4346
+ const { ok: ok2, advance } = applyInitToken(argv, i, st);
4347
+ if (!ok2) return null;
4348
+ i += advance;
4349
+ }
4350
+ return { snapshot: st.snapshot, keepActions: st.keepActions, repoName: st.repoName };
4351
+ }
4352
+ function parseRedactArgs(argv) {
4353
+ const id = argv[3];
4354
+ if (typeof id !== "string" || !/^\w[\w-]{0,127}$/.test(id)) {
4355
+ return null;
4356
+ }
4357
+ let rule;
4358
+ let dryRun = false;
4359
+ let sawRule = false;
4360
+ let sawDryRun = false;
4361
+ let i = 4;
4362
+ while (i < argv.length) {
4363
+ const token = argv[i];
4364
+ if (token === "--dry-run") {
4365
+ if (sawDryRun) return null;
4366
+ sawDryRun = true;
4367
+ dryRun = true;
4368
+ i++;
4369
+ } else if (token === "--rule") {
4370
+ if (sawRule) return null;
4371
+ sawRule = true;
4372
+ const val = argv[i + 1];
4373
+ if (val === void 0 || val.startsWith("--")) return null;
4374
+ rule = val;
4375
+ i += 2;
4376
+ } else {
4377
+ return null;
4378
+ }
4379
+ }
4380
+ return { id, rule, dryRun };
4381
+ }
4382
+
4383
+ // src/nomad.dispatch.clean.ts
4384
+ var REJECT = { ok: false, advance: 0 };
4385
+ function applyOlderThan(argv, i, st) {
4386
+ if (st.olderThan !== void 0) return REJECT;
4387
+ const val = extractFlagValue(argv, i);
4388
+ if (val === null) return REJECT;
4389
+ st.olderThan = val;
4390
+ return { ok: true, advance: 2 };
4391
+ }
4392
+ function applyKeep(argv, i, st) {
4393
+ if (st.keep !== void 0) return REJECT;
4394
+ const val = extractFlagValue(argv, i);
4395
+ if (val === null || !/^\d+$/.test(val)) return REJECT;
4396
+ st.keep = Number(val);
4397
+ return { ok: true, advance: 2 };
4398
+ }
4399
+ function applyBool(seen, set) {
4400
+ if (seen) return REJECT;
4401
+ set();
4402
+ return { ok: true, advance: 1 };
4403
+ }
4404
+ function applyCleanToken(argv, i, st) {
4405
+ switch (argv[i]) {
4406
+ case "--backups":
4407
+ return applyBool(st.backups, () => st.backups = true);
4408
+ case "--dry-run":
4409
+ return applyBool(st.dryRun, () => st.dryRun = true);
4410
+ case "--older-than":
4411
+ return applyOlderThan(argv, i, st);
4412
+ case "--keep":
4413
+ return applyKeep(argv, i, st);
4414
+ default:
4415
+ return REJECT;
4416
+ }
4417
+ }
4418
+ function parseCleanArgs(argv) {
4419
+ const st = {
4420
+ backups: false,
4421
+ dryRun: false,
4422
+ olderThan: void 0,
4423
+ keep: void 0
4424
+ };
4425
+ let i = 3;
4426
+ while (i < argv.length) {
4427
+ const { ok: ok2, advance } = applyCleanToken(argv, i, st);
4428
+ if (!ok2) return null;
4429
+ i += advance;
4430
+ }
4431
+ if (!st.backups) return null;
4432
+ if (st.olderThan !== void 0 && st.keep !== void 0) return null;
4433
+ return { dryRun: st.dryRun, olderThan: st.olderThan, keep: st.keep };
4434
+ }
4435
+
4436
+ // package.json
4437
+ var package_default = {
4438
+ name: "claude-nomad",
4439
+ version: "0.35.0",
4440
+ type: "module",
4441
+ description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
4442
+ keywords: [
4443
+ "claude",
4444
+ "claude-code",
4445
+ "sync",
4446
+ "dotfiles"
4447
+ ],
4448
+ repository: {
4449
+ type: "git",
4450
+ url: "git+https://github.com/funkadelic/claude-nomad.git"
4451
+ },
4452
+ homepage: "https://github.com/funkadelic/claude-nomad#readme",
4453
+ bugs: {
4454
+ url: "https://github.com/funkadelic/claude-nomad/issues"
4455
+ },
4456
+ license: "MIT",
4457
+ bin: {
4458
+ nomad: "./dist/nomad.mjs"
4459
+ },
4460
+ files: [
4461
+ "dist/",
4462
+ "shared/.gitignore",
4463
+ ".gitleaks.toml",
4464
+ "README.md",
4465
+ "CHANGELOG.md",
4466
+ "LICENSE"
4467
+ ],
4468
+ engines: {
4469
+ node: ">=22.22.1"
4470
+ },
4471
+ scripts: {
4472
+ pull: "node src/nomad.ts pull",
4473
+ push: "node src/nomad.ts push",
4474
+ doctor: "node src/nomad.ts doctor",
4475
+ update: "bash scripts/update.sh",
4476
+ build: "node scripts/build.mjs",
4477
+ test: "vitest run",
4478
+ coverage: "vitest run --coverage",
4479
+ typecheck: "tsc --noEmit",
4480
+ lint: "eslint .",
4481
+ "lint:fix": "eslint . --fix",
4482
+ "lint:md": "markdownlint-cli2",
4483
+ format: "prettier --write .",
4484
+ "format:check": "prettier --check .",
4485
+ prepack: "npm run build",
4486
+ prepublishOnly: "npm run lint && npm run typecheck && npm run test && npm run build && node scripts/verify-tarball.cjs",
4487
+ prepare: "husky"
4488
+ },
4489
+ "lint-staged": {
4490
+ "*.ts": [
4491
+ "eslint --fix",
4492
+ "prettier --write"
4493
+ ],
4494
+ "*.{js,mjs,cjs,json}": [
4495
+ "prettier --write"
4496
+ ],
4497
+ "*.md": [
4498
+ "markdownlint-cli2 --fix",
4499
+ "prettier --write"
4500
+ ]
4501
+ },
4502
+ devDependencies: {
4503
+ "@commitlint/cli": "^21.0.1",
4504
+ "@commitlint/config-conventional": "^21.0.1",
4505
+ "@eslint/js": "^10.0.1",
4506
+ "@types/node": "^22.0.0",
4507
+ "@vitest/coverage-v8": "^4.1.6",
4508
+ diff: "^9.0.0",
4509
+ esbuild: "^0.28.0",
4510
+ eslint: "^10.4.0",
4511
+ "eslint-config-prettier": "^10.1.8",
4512
+ "eslint-plugin-sonarjs": "^4.0.3",
4513
+ globals: "^17.6.0",
4514
+ husky: "^9.1.7",
4515
+ "lint-staged": "^17.0.5",
4516
+ "markdownlint-cli2": "^0.22.1",
4517
+ picocolors: "^1.1.1",
4518
+ prettier: "^3.8.3",
4519
+ typescript: "^6.0.3",
4520
+ "typescript-eslint": "^8.59.4",
4521
+ vitest: "^4.1.6"
4522
+ }
4523
+ };
4524
+
4525
+ // src/nomad.help.ts
4526
+ var DESC_COL = 26;
4527
+ var row = (label, desc) => label.padEnd(DESC_COL) + desc;
4528
+ var cont = (text) => " ".repeat(DESC_COL) + text;
4529
+ var DEFAULT_HELP = [
4530
+ `claude-nomad v${package_default.version}`,
4531
+ "",
4532
+ "usage: nomad <command> [flags]",
4533
+ "",
4534
+ "Commands:",
4535
+ row(" pull", "Sync ~/.claude/ from the shared repo (settings, symlinks, sessions)."),
4536
+ row(" --dry-run", "Run lock + git pull, then preview every mutation without writing."),
4537
+ "",
4538
+ row(" push", "Rebase, run safety checks (gitleaks, gitlinks, allow-list), commit, push."),
4539
+ row(" --dry-run", "Run pre-checks (rebase, gitleaks probe, gitlink scan) and preview"),
4540
+ cont("remap, without staging or pushing."),
4541
+ row(" --redact-all", "Redact all findings non-interactively (backup, no prompt); no TTY"),
4542
+ cont("required. Does not auto-Allow."),
4543
+ "",
4544
+ row(" diff", "Offline preview of what `pull` would change against local repo state."),
4545
+ cont("No git pull, no lock acquired."),
4546
+ "",
4547
+ row(
4548
+ " init",
4549
+ "Create a private GitHub repo via gh (if none exists), scaffold shared/, hosts/, path-map."
4550
+ ),
4551
+ row(" --snapshot", "Overlay the current ~/.claude/ into shared/ as the initial seed."),
4552
+ row(" --keep-actions", "Skip auto-disabling GitHub Actions on the private mirror."),
4553
+ row(
4554
+ " --repo <name>",
4555
+ "Name for the new GitHub repo (default: claude-nomad-config). No-op when origin exists."
4556
+ ),
4557
+ "",
4558
+ row(" doctor", "Read-only health check (symlinks, host file, path-map,"),
4559
+ cont("gitleaks, gitlinks)."),
4560
+ row(" --check-shared", "Preflight gitleaks scan of the session transcripts a"),
4561
+ cont("`nomad push` would stage (a temp copy, never the live dir)."),
4562
+ row(" --check-schema", "Flag settings.json keys absent from the live published"),
4563
+ cont("Claude Code settings schema (needs network; degrades offline)."),
4564
+ row(" --resume-cmd <id>", "Print `cd <abspath> && claude --resume <id>` for a session id"),
4565
+ cont("from ~/.claude/projects/."),
4566
+ "",
4567
+ row(
4568
+ " drop-session <id>",
4569
+ "Unstage shared/projects/<logical>/<id>.jsonl from the staged tree (local ~/.claude/projects is never touched)."
4570
+ ),
4571
+ "",
4572
+ row(
4573
+ " adopt <name>",
4574
+ "Move a pre-existing ~/.claude/<name> dir into shared/<name>, recreate the"
4575
+ ),
4576
+ cont("symlink, and stage for push. <name> must be in SHARED_LINKS or sharedDirs."),
4577
+ row(" --dry-run", "Preview backup, move, and git-add without writing."),
4578
+ "",
4579
+ row(
4580
+ " redact <session-id>",
4581
+ "Rewrite the secret span in the local source transcript for a session,"
4582
+ ),
4583
+ cont("backed up to ~/.cache/claude-nomad/backup/. Safe to re-run."),
4584
+ row(" --rule <id>", "Limit redaction to one gitleaks rule id."),
4585
+ row(" --dry-run", "Show what would change without writing."),
4586
+ "",
4587
+ row(" update", "Update the claude-nomad CLI to the latest npm release."),
4588
+ "",
4589
+ row(" clean", "Prune old backup snapshots under ~/.cache/claude-nomad/backup/."),
4590
+ row(" --backups", "Required: confirm backup pruning is the intended target."),
4591
+ row(" --dry-run", "List the snapshots that would be removed without deleting."),
4592
+ row(" --older-than <dur>", "Delete snapshots older than a duration (e.g. 14d, 24h, 30m)."),
4593
+ cont("Default when no retention flag is given: 14d."),
4594
+ row(" --keep <N>", "Keep the N most-recent snapshots, delete the rest. Mutually"),
4595
+ cont("exclusive with --older-than."),
4596
+ "",
4597
+ row(" --version", "Print the installed CLI version as bare semver to stdout; exits 0."),
4598
+ "",
4599
+ "Run `nomad doctor` to validate your setup. Edit shared/ or hosts/<HOST>.json",
4600
+ "in the repo, never ~/.claude/settings.json directly (it is regenerated on",
4601
+ "every pull)."
4602
+ ].join("\n");
4603
+
4604
+ // src/resume.ts
4605
+ init_config();
4606
+ init_utils();
4607
+ init_utils_json();
4608
+ import { existsSync as existsSync33, readFileSync as readFileSync10, readdirSync as readdirSync10 } from "node:fs";
4609
+ import { join as join40 } from "node:path";
4610
+ function resumeCmd(sessionId) {
4611
+ if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
4612
+ fail(`invalid session id: ${sessionId}`);
4613
+ process.exit(1);
4614
+ }
4615
+ const projectsRoot = join40(CLAUDE_HOME, "projects");
4616
+ if (!existsSync33(projectsRoot)) {
4617
+ fail(`${projectsRoot} does not exist`);
4618
+ process.exit(1);
4619
+ }
4620
+ const jsonlPath = findTranscriptPath(projectsRoot, sessionId);
4621
+ if (jsonlPath === null) {
4622
+ fail(`session ${sessionId} not found in any ~/.claude/projects/<encoded>/`);
4623
+ process.exit(1);
4624
+ }
4625
+ const recordedCwd = extractRecordedCwd(jsonlPath);
4626
+ if (recordedCwd === null) {
4627
+ fail(`no cwd field found in ${jsonlPath}`);
4628
+ process.exit(1);
4629
+ }
4630
+ const mapPath = join40(REPO_HOME, "path-map.json");
4631
+ if (!existsSync33(mapPath)) {
4632
+ fail("path-map.json missing");
4633
+ process.exit(1);
4634
+ }
4635
+ const map = readJson(mapPath);
4636
+ const schemaError = validatePathMap(map);
4637
+ if (schemaError !== null) {
4638
+ fail(schemaError);
4639
+ process.exit(1);
4640
+ }
4641
+ const hit = lookupLocalPath(map, recordedCwd);
4642
+ if (hit === null) {
4643
+ fail(`cwd ${recordedCwd} from session ${sessionId} not found in path-map.json`);
4644
+ process.exit(1);
4645
+ }
4646
+ if (hit.localPath === void 0) {
4647
+ fail(`session ${sessionId} not mapped on this host; add the logical to path-map.json`);
4648
+ process.exit(1);
4649
+ }
4650
+ console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
4651
+ }
4652
+ function findTranscriptPath(projectsRoot, sessionId) {
4653
+ for (const dir of readdirSync10(projectsRoot)) {
4654
+ const candidate = join40(projectsRoot, dir, `${sessionId}.jsonl`);
4655
+ if (existsSync33(candidate)) return candidate;
4656
+ }
4657
+ return null;
4658
+ }
4659
+ function extractRecordedCwd(jsonlPath) {
4660
+ for (const line of readFileSync10(jsonlPath, "utf8").split("\n")) {
4661
+ if (!line.trim()) continue;
4662
+ try {
4663
+ const obj = JSON.parse(line);
4664
+ if (obj.type === "file-history-snapshot") continue;
4665
+ if (typeof obj.cwd === "string" && obj.cwd.length > 0) return obj.cwd;
4666
+ } catch {
4667
+ }
4668
+ }
4669
+ return null;
4670
+ }
4671
+ function validatePathMap(raw) {
4672
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4673
+ return "path-map.json invalid schema: top-level value must be an object";
4674
+ }
4675
+ const projects = raw.projects;
4676
+ if (projects === null || typeof projects !== "object" || Array.isArray(projects)) {
4677
+ return 'path-map.json invalid schema: "projects" must be an object';
4678
+ }
4679
+ for (const [name, hosts] of Object.entries(projects)) {
4680
+ if (hosts === null || typeof hosts !== "object" || Array.isArray(hosts)) {
4681
+ return `path-map.json invalid schema: project "${name}" hosts must be an object`;
4682
+ }
4683
+ for (const [host, value] of Object.entries(hosts)) {
4684
+ if (typeof value !== "string") {
4685
+ return `path-map.json invalid schema: project "${name}" host "${host}" path must be a string`;
4686
+ }
4687
+ }
4688
+ }
4689
+ return null;
4690
+ }
4691
+ function lookupLocalPath(map, recordedCwd) {
4692
+ for (const [logical, hosts] of Object.entries(map.projects)) {
4693
+ const isUnderMappedPath = Object.values(hosts).some(
4694
+ (p) => recordedCwd === p || recordedCwd.startsWith(`${p}/`)
4695
+ );
4696
+ if (isUnderMappedPath) {
4697
+ const localPath = hosts[HOST];
4698
+ return { logical, localPath: localPath === "TBD" ? void 0 : localPath };
4699
+ }
4700
+ }
4701
+ return null;
4702
+ }
4703
+ function shQuote(s) {
4704
+ const escaped = s.replaceAll("'", String.raw`'\''`);
4705
+ return `'${escaped}'`;
4706
+ }
4707
+
4708
+ // src/nomad.ts
4709
+ init_utils();
4710
+ if (!HOME) {
4711
+ fail(
4712
+ "could not determine home directory (HOME env unset and no uid mapping). Set HOME and retry."
4713
+ );
4714
+ process.exit(1);
4715
+ }
4716
+ try {
4717
+ const cmd = process.argv[2];
4718
+ switch (cmd) {
4719
+ case "--version":
4720
+ if (process.argv.length !== 3) {
4721
+ console.error("usage: nomad --version (no extra arguments)");
4722
+ process.exit(1);
4723
+ }
4724
+ console.log(package_default.version);
4725
+ break;
4726
+ case "pull": {
4727
+ const sub = process.argv[3];
4728
+ if (sub === void 0) {
4729
+ cmdPull();
4730
+ } else if (sub === "--dry-run" && process.argv.length === 4) {
4731
+ cmdPull({ dryRun: true });
4732
+ } else {
4733
+ console.error("usage: nomad pull [--dry-run]");
4734
+ process.exit(1);
4735
+ }
4736
+ break;
4737
+ }
4738
+ case "push": {
4739
+ const seen = parseFlags(process.argv, /* @__PURE__ */ new Set(["--dry-run", "--redact-all"]));
4740
+ if (seen === null) {
4741
+ console.error("usage: nomad push [--dry-run] [--redact-all]");
4742
+ process.exit(1);
4743
+ }
4744
+ await cmdPush({ dryRun: seen.has("--dry-run"), redactAll: seen.has("--redact-all") });
4745
+ break;
4746
+ }
4747
+ case "init": {
4748
+ const initArgs = parseInitArgs(process.argv);
4749
+ if (initArgs === null) {
4750
+ console.error("usage: nomad init [--snapshot] [--keep-actions] [--repo <name>]");
4751
+ process.exit(1);
4752
+ }
4753
+ cmdInit({
4754
+ snapshot: initArgs.snapshot,
4755
+ keepActions: initArgs.keepActions,
4756
+ repoName: initArgs.repoName
4757
+ });
4758
+ break;
4759
+ }
4760
+ case "diff":
4761
+ if (process.argv.length > 3) {
4762
+ console.error("usage: nomad diff");
4763
+ process.exit(1);
4764
+ }
4765
+ cmdDiff();
4766
+ break;
4767
+ case "update": {
4768
+ if (process.argv.length !== 3) {
4769
+ console.error("usage: nomad update");
4770
+ process.exit(1);
4771
+ }
4772
+ cmdUpdate();
4773
+ break;
4774
+ }
4775
+ case "adopt": {
4776
+ const name = process.argv[3];
4777
+ const sub = process.argv[4];
4778
+ if (typeof name !== "string" || name.length === 0 || name.startsWith("-") || sub !== void 0 && (sub !== "--dry-run" || process.argv.length !== 5)) {
4779
+ console.error("usage: nomad adopt <name> [--dry-run]");
4780
+ process.exit(1);
4781
+ }
4782
+ cmdAdopt(name, { dryRun: sub === "--dry-run" });
4783
+ break;
4784
+ }
4785
+ case "doctor":
4786
+ if (process.argv[3] === void 0) {
4787
+ cmdDoctor();
4788
+ } else if (process.argv[3] === "--check-shared" && process.argv.length === 4) {
4789
+ cmdDoctor({ checkShared: true });
4790
+ } else if (process.argv[3] === "--check-schema" && process.argv.length === 4) {
4791
+ cmdDoctor({ checkSchema: true });
4792
+ } else if (process.argv[3] === "--resume-cmd") {
4793
+ const id = process.argv[4];
4794
+ if (process.argv.length !== 5 || typeof id !== "string" || id.length === 0) {
4795
+ console.error("usage: nomad doctor --resume-cmd <session-id>");
4796
+ process.exit(1);
4797
+ }
4798
+ resumeCmd(id);
4799
+ } else {
4800
+ console.error(
4801
+ "usage: nomad doctor [--check-shared | --check-schema | --resume-cmd <session-id>]"
4802
+ );
4803
+ process.exit(1);
4804
+ }
4805
+ break;
4806
+ case "drop-session": {
4807
+ const id = process.argv[3];
4808
+ if (process.argv.length !== 4 || typeof id !== "string" || !/^\w[\w-]{0,127}$/.test(id)) {
4809
+ console.error("usage: nomad drop-session <id>");
4810
+ process.exit(1);
4811
+ }
4812
+ cmdDropSession(id);
4813
+ break;
4814
+ }
4815
+ case "redact": {
4816
+ const redactArgs = parseRedactArgs(process.argv);
4817
+ if (redactArgs === null) {
4818
+ console.error("usage: nomad redact <session-id> [--rule <rule-id>] [--dry-run]");
4819
+ process.exit(1);
4820
+ }
4821
+ cmdRedact(redactArgs);
4822
+ break;
4823
+ }
4824
+ case "clean": {
4825
+ const cleanArgs = parseCleanArgs(process.argv);
4826
+ if (cleanArgs === null) {
4827
+ console.error("usage: nomad clean --backups [--dry-run] [--older-than <dur> | --keep <N>]");
4828
+ process.exit(1);
4829
+ }
4830
+ cmdClean(cleanArgs);
4831
+ break;
4832
+ }
4833
+ default:
4834
+ console.error(DEFAULT_HELP);
4835
+ process.exit(1);
4836
+ }
4837
+ } catch (err) {
4838
+ if (err instanceof NomadFatal) {
4839
+ fail(err.message);
4840
+ process.exit(1);
4841
+ }
4842
+ throw err;
4843
+ }