claude-nomad 0.44.1 → 0.46.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.
package/dist/nomad.mjs CHANGED
@@ -1400,8 +1400,8 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
1400
1400
  }
1401
1401
 
1402
1402
  // src/commands.doctor.ts
1403
- import { existsSync as existsSync22 } from "node:fs";
1404
- import { join as join26 } from "node:path";
1403
+ import { existsSync as existsSync27 } from "node:fs";
1404
+ import { join as join33 } from "node:path";
1405
1405
 
1406
1406
  // src/commands.doctor.checks.repo.ts
1407
1407
  init_color();
@@ -2686,17 +2686,150 @@ function reportPreserveSymlinksCheck(section2) {
2686
2686
  }
2687
2687
  }
2688
2688
 
2689
+ // src/commands.doctor.checks.settings-drift.ts
2690
+ init_color();
2691
+ import { existsSync as existsSync21, readFileSync as readFileSync7 } from "node:fs";
2692
+ import { join as join25 } from "node:path";
2693
+ init_config();
2694
+ init_utils_json();
2695
+ function arraysEqual(a, b) {
2696
+ if (a.length !== b.length) return false;
2697
+ for (let i = 0; i < a.length; i++) {
2698
+ if (!deepEqual(a[i], b[i])) return false;
2699
+ }
2700
+ return true;
2701
+ }
2702
+ function objectsEqual(a, b) {
2703
+ const aKeys = Object.keys(a);
2704
+ const bKeys = Object.keys(b);
2705
+ if (aKeys.length !== bKeys.length) return false;
2706
+ for (const k of aKeys) {
2707
+ if (!Object.hasOwn(b, k)) return false;
2708
+ if (!deepEqual(a[k], b[k])) return false;
2709
+ }
2710
+ return true;
2711
+ }
2712
+ function deepEqual(a, b) {
2713
+ if (a === b) return true;
2714
+ if (a === null || b === null) return false;
2715
+ if (Array.isArray(a) && Array.isArray(b)) return arraysEqual(a, b);
2716
+ if (Array.isArray(a) || Array.isArray(b)) return false;
2717
+ if (typeof a === "object" && typeof b === "object") {
2718
+ return objectsEqual(a, b);
2719
+ }
2720
+ return false;
2721
+ }
2722
+ function diffMergedSettings(merged, settings) {
2723
+ const missing = [];
2724
+ const changed = [];
2725
+ const extra = [];
2726
+ const settingsKeys = new Set(Object.keys(settings));
2727
+ for (const key of Object.keys(merged)) {
2728
+ if (!settingsKeys.has(key)) {
2729
+ missing.push(key);
2730
+ } else if (!deepEqual(merged[key], settings[key])) {
2731
+ changed.push(key);
2732
+ }
2733
+ }
2734
+ const mergedKeys = new Set(Object.keys(merged));
2735
+ for (const key of Object.keys(settings)) {
2736
+ if (!mergedKeys.has(key)) extra.push(key);
2737
+ }
2738
+ const collator = (a, b) => a.localeCompare(b, "en");
2739
+ return {
2740
+ missing: missing.toSorted(collator),
2741
+ changed: changed.toSorted(collator),
2742
+ extra: extra.toSorted(collator)
2743
+ };
2744
+ }
2745
+ function tryReadJson(filePath) {
2746
+ try {
2747
+ const raw = readFileSync7(filePath, "utf8");
2748
+ const parsed = JSON.parse(raw);
2749
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2750
+ return parsed;
2751
+ } catch {
2752
+ return null;
2753
+ }
2754
+ }
2755
+ function reportSettingsDriftCheck(section2) {
2756
+ const claude = claudeHome();
2757
+ const repo = repoHome();
2758
+ const host = HOST;
2759
+ const settingsPath = join25(claude, "settings.json");
2760
+ const basePath = join25(repo, "shared", "settings.base.json");
2761
+ const hostPath = join25(repo, "hosts", `${host}.json`);
2762
+ if (!existsSync21(settingsPath)) {
2763
+ addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping merge-drift check`);
2764
+ return;
2765
+ }
2766
+ if (!existsSync21(basePath)) {
2767
+ addItem(
2768
+ section2,
2769
+ `${dim(infoGlyph)} shared/settings.base.json missing; skipping merge-drift check`
2770
+ );
2771
+ return;
2772
+ }
2773
+ const base = tryReadJson(basePath);
2774
+ if (base === null) {
2775
+ addItem(
2776
+ section2,
2777
+ `${dim(infoGlyph)} shared/settings.base.json unparseable; skipping merge-drift check`
2778
+ );
2779
+ return;
2780
+ }
2781
+ const settings = tryReadJson(settingsPath);
2782
+ if (settings === null) {
2783
+ return;
2784
+ }
2785
+ const hostExists = existsSync21(hostPath);
2786
+ const hostObj = hostExists ? tryReadJson(hostPath) : null;
2787
+ if (hostExists && hostObj === null) {
2788
+ addItem(
2789
+ section2,
2790
+ `${yellow(warnGlyph)} hosts/${host}.json unparseable; 'nomad pull' will fail (fix the host file)`
2791
+ );
2792
+ return;
2793
+ }
2794
+ const merged = deepMerge(base, hostObj ?? {});
2795
+ const { missing, changed, extra } = diffMergedSettings(merged, settings);
2796
+ emitDriftRows(section2, missing, changed, extra, host, hostExists);
2797
+ }
2798
+ function emitDriftRows(section2, missing, changed, extra, host, hostFileExists) {
2799
+ if (missing.length > 0) {
2800
+ addItem(
2801
+ section2,
2802
+ `${yellow(warnGlyph)} settings.json drift: merged keys missing locally: ${missing.join(", ")} (external writer clobbered settings.json; run 'nomad pull')`
2803
+ );
2804
+ }
2805
+ if (changed.length > 0) {
2806
+ addItem(
2807
+ section2,
2808
+ `${yellow(warnGlyph)} settings.json drift: merged keys with changed values: ${changed.join(", ")} (run 'nomad pull')`
2809
+ );
2810
+ }
2811
+ if (extra.length > 0 && hostFileExists) {
2812
+ addItem(
2813
+ section2,
2814
+ `${dim(infoGlyph)} settings.json has ${extra.length} local-only key(s) not in base+host merge: ${extra.join(", ")} (promotion candidates for shared/settings.base.json or hosts/${host}.json)`
2815
+ );
2816
+ }
2817
+ if (missing.length === 0 && changed.length === 0 && extra.length === 0) {
2818
+ addItem(section2, `${green(okGlyph)} settings.json matches base+host merge`);
2819
+ }
2820
+ }
2821
+
2689
2822
  // src/commands.doctor.ts
2690
2823
  init_config();
2691
2824
 
2692
2825
  // src/commands.doctor.engine.ts
2693
2826
  init_color();
2694
- import { readFileSync as readFileSync8 } from "node:fs";
2827
+ import { readFileSync as readFileSync9 } from "node:fs";
2695
2828
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2696
2829
 
2697
2830
  // src/commands.doctor.version.ts
2698
2831
  init_color();
2699
- import { readFileSync as readFileSync7 } from "node:fs";
2832
+ import { readFileSync as readFileSync8 } from "node:fs";
2700
2833
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2701
2834
  init_config();
2702
2835
  var STRICT_SEMVER = /^\d+\.\d+\.\d+$/;
@@ -2713,7 +2846,7 @@ function compareSemver(a, b) {
2713
2846
  function readLocalVersion() {
2714
2847
  try {
2715
2848
  const pkgPath = fileURLToPath2(new URL("../package.json", import.meta.url));
2716
- const parsed = JSON.parse(readFileSync7(pkgPath, "utf8"));
2849
+ const parsed = JSON.parse(readFileSync8(pkgPath, "utf8"));
2717
2850
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
2718
2851
  return parsed.version;
2719
2852
  }
@@ -2772,7 +2905,7 @@ function parseMinVersion(spec) {
2772
2905
  function readEnginesNode() {
2773
2906
  try {
2774
2907
  const pkgPath = fileURLToPath3(new URL("../package.json", import.meta.url));
2775
- const parsed = JSON.parse(readFileSync8(pkgPath, "utf8"));
2908
+ const parsed = JSON.parse(readFileSync9(pkgPath, "utf8"));
2776
2909
  const node = parsed.engines?.node;
2777
2910
  if (typeof node === "string" && node.length > 0) return node;
2778
2911
  return null;
@@ -2798,398 +2931,123 @@ function reportNodeEngineCheck(section2) {
2798
2931
  addItem(section2, `${green(okGlyph)} node: ${process.version} (satisfies >=${min})`);
2799
2932
  }
2800
2933
 
2801
- // src/commands.doctor.gitleaks-version.ts
2934
+ // src/spinner.ts
2802
2935
  init_color();
2803
- import { execFileSync as execFileSync7 } from "node:child_process";
2804
- import { existsSync as existsSync21 } from "node:fs";
2805
- import { join as join25 } from "node:path";
2936
+ import { existsSync as existsSync25 } from "node:fs";
2937
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
2938
+ import { Worker } from "node:worker_threads";
2939
+
2940
+ // src/commands.push.recovery.ts
2806
2941
  init_config();
2807
- var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
2808
- var GITLEAKS_TIMEOUT_MS = 5e3;
2809
- function majorMinorOf(value) {
2810
- const m = SEMVER_MAJOR_MINOR.exec(value);
2811
- return m === null ? null : [m[1], m[2]];
2812
- }
2813
- function readGitleaksVersion(run, tomlExists) {
2814
- const tomlPath = join25(repoHome(), ".gitleaks.toml");
2815
- const args = ["version"];
2816
- if (tomlExists(tomlPath)) args.push("--config", tomlPath);
2817
- try {
2818
- return run("gitleaks", args, {
2819
- stdio: ["ignore", "pipe", "pipe"],
2820
- timeout: GITLEAKS_TIMEOUT_MS
2821
- }).toString().trim();
2822
- } catch {
2823
- return null;
2824
- }
2825
- }
2826
- function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync21) {
2827
- const raw = readGitleaksVersion(run, tomlExists);
2828
- if (raw === null) return;
2829
- const local = majorMinorOf(raw);
2830
- if (local === null) return;
2831
- const pin = majorMinorOf(GITLEAKS_PINNED_VERSION);
2832
- if (pin === null) return;
2833
- const sameMajorMinor = local[0] === pin[0] && local[1] === pin[1];
2834
- if (sameMajorMinor) {
2835
- addItem(section2, `${green(okGlyph)} gitleaks: ${raw} (matches pinned ${pin[0]}.${pin[1]})`);
2836
- return;
2837
- }
2838
- addItem(
2839
- section2,
2840
- `${yellow(warnGlyph)} gitleaks: ${raw} -> ${GITLEAKS_PINNED_VERSION} (CI pins this; local drift may change scan results)`
2841
- );
2842
- }
2942
+ import { readFileSync as readFileSync13, rmSync as rmSync9, writeFileSync as writeFileSync5 } from "node:fs";
2943
+ import { join as join31 } from "node:path";
2944
+ import { createInterface } from "node:readline/promises";
2843
2945
 
2844
- // src/commands.doctor.checks.deps.ts
2845
- init_color();
2846
- import { execFileSync as execFileSync8 } from "node:child_process";
2847
- var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
2848
- var PROBE_TIMEOUT_MS = 3e3;
2849
- var FETCHER_BASE = "HTTP fetcher";
2850
- function parseFirstVersion(line) {
2851
- const m = VERSION_TOKEN.exec(line);
2852
- return m ? m[1] : null;
2853
- }
2854
- function probeOptionalDep(bin, run) {
2855
- try {
2856
- const firstLine = run(bin, ["--version"], {
2857
- stdio: ["ignore", "pipe", "pipe"],
2858
- timeout: PROBE_TIMEOUT_MS
2859
- }).toString().split("\n")[0].trim();
2860
- const version = parseFirstVersion(firstLine);
2861
- return { status: "present", version };
2862
- } catch (err) {
2863
- if (err.code === "ENOENT") {
2864
- return { status: "not-installed" };
2946
+ // src/commands.push.recovery.actions.ts
2947
+ init_config();
2948
+ import { readFileSync as readFileSync12 } from "node:fs";
2949
+ import { isAbsolute, resolve as resolve3, sep as sep4 } from "node:path";
2950
+
2951
+ // src/commands.push.recovery.redact.ts
2952
+ init_config();
2953
+ init_config_sharedDirs_guard();
2954
+ import { cpSync as cpSync5, existsSync as existsSync24, mkdirSync as mkdirSync6, statSync as statSync7 } from "node:fs";
2955
+ import { dirname as dirname6, join as join29, sep as sep3 } from "node:path";
2956
+
2957
+ // src/commands.redact.ts
2958
+ init_config();
2959
+ import { existsSync as existsSync23, statSync as statSync6 } from "node:fs";
2960
+ import { dirname as dirname5, join as join28 } from "node:path";
2961
+
2962
+ // src/commands.redact.subtree.ts
2963
+ import { existsSync as existsSync22, lstatSync as lstatSync7, readFileSync as readFileSync10, readdirSync as readdirSync9, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
2964
+ import { join as join26 } from "node:path";
2965
+ init_utils_fs();
2966
+ function collectFiles(dir, out) {
2967
+ if (!existsSync22(dir)) return;
2968
+ const st = lstatSync7(dir);
2969
+ if (!st.isDirectory()) return;
2970
+ for (const entry of readdirSync9(dir)) {
2971
+ const abs = join26(dir, entry);
2972
+ const lst = lstatSync7(abs);
2973
+ if (lst.isSymbolicLink()) continue;
2974
+ if (lst.isDirectory()) {
2975
+ collectFiles(abs, out);
2976
+ continue;
2865
2977
  }
2866
- return { status: "present", version: null };
2978
+ if (lst.isFile()) out.push(abs);
2867
2979
  }
2868
2980
  }
2869
- function reportFetcherRow(section2, run) {
2870
- const curl = probeOptionalDep("curl", run);
2871
- const wget = probeOptionalDep("wget", run);
2872
- if (curl.status === "present") {
2873
- addItem(section2, `${green(okGlyph)} ${FETCHER_BASE}: curl ${curl.version ?? "(present)"}`);
2874
- } else if (wget.status === "present") {
2875
- addItem(section2, `${green(okGlyph)} ${FETCHER_BASE}: wget ${wget.version ?? "(present)"}`);
2876
- } else {
2877
- addItem(
2878
- section2,
2879
- `${yellow(warnGlyph)} ${FETCHER_BASE} (curl or wget): not installed (optional; needed for release-version staleness check + nomad doctor --check-schema)`
2880
- );
2981
+ function listSubtreeFiles(sessionDir) {
2982
+ const out = [];
2983
+ collectFiles(sessionDir, out);
2984
+ return out.sort((a, b) => a.localeCompare(b));
2985
+ }
2986
+ function newestSubtreeMtimeMs(mainPath, subtreeFiles, statMtime = (p) => statSync5(p).mtimeMs) {
2987
+ let newest = statMtime(mainPath);
2988
+ for (const filePath of subtreeFiles) {
2989
+ const t = statMtime(filePath);
2990
+ if (t > newest) newest = t;
2881
2991
  }
2992
+ return newest;
2882
2993
  }
2883
- function reportOptionalDeps(section2, run = execFileSync8) {
2884
- const gh = probeOptionalDep("gh", run);
2885
- if (gh.status === "present") {
2886
- addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
2887
- } else {
2888
- addItem(
2889
- section2,
2890
- `${yellow(warnGlyph)} gh: not installed (optional; needed for nomad init Actions auto-disable + the Actions-drift check)`
2891
- );
2994
+ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts, scan, dryRun) {
2995
+ const dirty = [];
2996
+ if (mainFindings.length > 0) dirty.push({ path: mainPath, findings: mainFindings });
2997
+ for (const filePath of subtreeFiles) {
2998
+ const raw = scan(filePath);
2999
+ if (raw === null || raw.length === 0) continue;
3000
+ const filtered = rule === void 0 ? raw : raw.filter((f) => f.RuleID === rule);
3001
+ if (filtered.length === 0) continue;
3002
+ dirty.push({ path: filePath, findings: filtered });
2892
3003
  }
2893
- reportFetcherRow(section2, run);
3004
+ const total = dirty.reduce((n, e) => n + e.findings.length, 0);
3005
+ if (!dryRun && total > 0) {
3006
+ for (const { path: filePath, findings } of dirty) {
3007
+ backupBeforeWrite(filePath, ts);
3008
+ writeFileSync3(filePath, applyRedactions(readFileSync10(filePath, "utf8"), findings), "utf8");
3009
+ }
3010
+ }
3011
+ return { total, dirty };
2894
3012
  }
2895
3013
 
2896
- // src/commands.doctor.actions-drift.ts
2897
- init_color();
2898
- import { execFileSync as execFileSync10 } from "node:child_process";
2899
- init_config();
3014
+ // src/commands.redact.ts
3015
+ init_push_gitleaks_scan();
3016
+ init_utils_fs();
3017
+ init_utils_json();
3018
+ init_utils();
2900
3019
 
2901
- // src/gh-actions.ts
2902
- import { execFileSync as execFileSync9 } from "node:child_process";
2903
- var GH_TIMEOUT_MS = 5e3;
2904
- function parseGitHubRemote(remoteUrl) {
2905
- const normalized = remoteUrl.trim().replace(/\/$/, "");
2906
- const m = /github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/.exec(normalized);
2907
- if (m === null) return null;
2908
- return { owner: m[1], repo: m[2] };
3020
+ // src/utils.lockfile.ts
3021
+ init_config();
3022
+ init_utils();
3023
+ import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync11, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
3024
+ import { dirname as dirname4, join as join27 } from "node:path";
3025
+ function lockFilePath() {
3026
+ return join27(home(), ".cache", "claude-nomad", "nomad.lock");
2909
3027
  }
2910
- function ghAuthStatus(run = execFileSync9) {
3028
+ function acquireLock(verb) {
3029
+ const lp = lockFilePath();
3030
+ mkdirSync5(dirname4(lp), { recursive: true });
2911
3031
  try {
2912
- run("gh", ["auth", "status"], {
2913
- stdio: ["ignore", "ignore", "ignore"],
2914
- timeout: GH_TIMEOUT_MS
2915
- });
2916
- return null;
3032
+ const fd = openSync3(lp, "wx");
3033
+ try {
3034
+ writeFileSync4(fd, String(process.pid));
3035
+ } catch (writeErr) {
3036
+ try {
3037
+ closeSync3(fd);
3038
+ } catch {
3039
+ }
3040
+ try {
3041
+ unlinkSync(lp);
3042
+ } catch {
3043
+ }
3044
+ throw writeErr;
3045
+ }
3046
+ return { fd, path: lp };
2917
3047
  } catch (err) {
2918
- const e = err;
2919
- if (e.code === "ENOENT") return "gh-not-installed";
2920
- if (typeof e.status === "number") return "gh-not-authed";
2921
- return "gh-probe-error";
2922
- }
2923
- }
2924
- function isRepoPrivate(ref, run = execFileSync9) {
2925
- const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
2926
- stdio: ["ignore", "pipe", "ignore"],
2927
- timeout: GH_TIMEOUT_MS
2928
- }).toString();
2929
- const parsed = JSON.parse(out);
2930
- return parsed.isPrivate === true;
2931
- }
2932
- function isActionsEnabled(ref, run = execFileSync9) {
2933
- const out = run(
2934
- "gh",
2935
- ["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
2936
- { stdio: ["ignore", "pipe", "ignore"], timeout: GH_TIMEOUT_MS }
2937
- ).toString().trim();
2938
- return out === "true";
2939
- }
2940
- function disableActions(ref, run = execFileSync9) {
2941
- run(
2942
- "gh",
2943
- [
2944
- "api",
2945
- "-X",
2946
- "PUT",
2947
- `repos/${ref.owner}/${ref.repo}/actions/permissions`,
2948
- "-F",
2949
- "enabled=false"
2950
- ],
2951
- { stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
2952
- );
2953
- }
2954
- function readOriginRemote(cwd, run = execFileSync9) {
2955
- return run("git", ["remote", "get-url", "origin"], {
2956
- cwd,
2957
- stdio: ["ignore", "pipe", "ignore"]
2958
- }).toString().trim();
2959
- }
2960
-
2961
- // src/commands.doctor.actions-drift.ts
2962
- function reportActionsDrift(section2, run = execFileSync10) {
2963
- let remote;
2964
- try {
2965
- remote = readOriginRemote(repoHome(), run);
2966
- } catch {
2967
- return;
2968
- }
2969
- const ref = parseGitHubRemote(remote);
2970
- if (ref === null) return;
2971
- const auth = ghAuthStatus(run);
2972
- if (auth === "gh-not-installed" || auth === "gh-not-authed") return;
2973
- let isPrivate;
2974
- try {
2975
- isPrivate = isRepoPrivate(ref, run);
2976
- } catch {
2977
- return;
2978
- }
2979
- if (!isPrivate) return;
2980
- let enabled2;
2981
- try {
2982
- enabled2 = isActionsEnabled(ref, run);
2983
- } catch {
2984
- return;
2985
- }
2986
- if (!enabled2) return;
2987
- addItem(
2988
- section2,
2989
- `${yellow(warnGlyph)} Actions: enabled on private repo ${ref.owner}/${ref.repo} (re-disable with 'gh api -X PUT repos/${ref.owner}/${ref.repo}/actions/permissions -F enabled=false')`
2990
- );
2991
- }
2992
-
2993
- // src/commands.doctor.verdict.ts
2994
- init_color();
2995
- function isFailLine(item2) {
2996
- return item2.includes(failGlyph);
2997
- }
2998
- function isWarnLine(item2) {
2999
- return !isFailLine(item2) && item2.includes(warnGlyph);
3000
- }
3001
- function buildVerdictSection(sections) {
3002
- const summary = section("Summary");
3003
- const lines = sections.flatMap((s) => s.items).map((item2) => item2.replace(/^\t/, ""));
3004
- const failures = lines.filter(isFailLine);
3005
- const warnings = lines.filter(isWarnLine);
3006
- for (const line of [...failures, ...warnings]) addItem(summary, line);
3007
- if (failures.length > 0) {
3008
- addItem(
3009
- summary,
3010
- `${red(failGlyph)} ${failures.length} failure(s), ${warnings.length} warning(s)`
3011
- );
3012
- } else if (warnings.length > 0) {
3013
- addItem(summary, `${yellow(warnGlyph)} ${warnings.length} warning(s)`);
3014
- } else {
3015
- addItem(summary, `${green(okGlyph)} healthy`);
3016
- }
3017
- return summary;
3018
- }
3019
-
3020
- // src/commands.doctor.ts
3021
- function cmdDoctor(opts = {}) {
3022
- const host = section("Environment");
3023
- reportHostAndPaths(host);
3024
- reportRepoState(host);
3025
- const links = section("Shared links");
3026
- const mapPath = join26(repoHome(), "path-map.json");
3027
- const rawMap = existsSync22(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
3028
- const map = rawMap ?? { projects: {} };
3029
- reportSharedLinks(links, map);
3030
- const hooksScan = section("Hook targets");
3031
- reportHooksTargetCheck(hooksScan);
3032
- reportHookScopeCheck(hooksScan);
3033
- reportPreserveSymlinksCheck(hooksScan);
3034
- const settings = section("Settings");
3035
- const base = loadBaseSettings(settings);
3036
- const parsedSettings = loadAndReportSettings(settings);
3037
- reportHostOverrides(settings, base, parsedSettings);
3038
- const pathMap = section("Path map");
3039
- reportPathMap(pathMap);
3040
- const neverSync = section("Never-sync");
3041
- reportNeverSync(neverSync);
3042
- const repository = section("Repository");
3043
- const gitleaksReady = reportGitleaksProbe(repository);
3044
- reportGitlinks(repository);
3045
- reportRemote(repository);
3046
- reportRebaseClean(repository);
3047
- reportRebaseState(repository);
3048
- reportActionsDrift(repository);
3049
- const nomadVersion = section("Nomad Version");
3050
- reportVersionCheck(nomadVersion);
3051
- const housekeeping = section("Housekeeping");
3052
- reportBackupsCheck(housekeeping);
3053
- const depVersions = section("Dependency Versions");
3054
- reportNodeEngineCheck(depVersions);
3055
- reportGitleaksVersionCheck(depVersions);
3056
- reportOptionalDeps(depVersions);
3057
- const sharedScan = section("Shared scan");
3058
- if (opts.checkShared === true) reportCheckShared(sharedScan, gitleaksReady);
3059
- const schemaScan = section("Schema scan");
3060
- if (opts.checkSchema === true) reportCheckSchema(schemaScan);
3061
- const body = [
3062
- nomadVersion,
3063
- depVersions,
3064
- host,
3065
- links,
3066
- hooksScan,
3067
- settings,
3068
- pathMap,
3069
- neverSync,
3070
- repository,
3071
- housekeeping,
3072
- sharedScan,
3073
- schemaScan
3074
- ];
3075
- renderDoctor([...body, buildVerdictSection(body)]);
3076
- }
3077
-
3078
- // src/commands.drop-session.ts
3079
- init_config();
3080
- import { execFileSync as execFileSync12 } from "node:child_process";
3081
- import { existsSync as existsSync24, readdirSync as readdirSync9, statSync as statSync5 } from "node:fs";
3082
- import { join as join29, relative as relative4 } from "node:path";
3083
-
3084
- // src/commands.drop-session.git.ts
3085
- import { execFileSync as execFileSync11 } from "node:child_process";
3086
- function expandStagedDir(dirRel, repo) {
3087
- try {
3088
- const out = execFileSync11("git", ["ls-files", "-z", "--", dirRel], {
3089
- cwd: repo,
3090
- stdio: ["ignore", "pipe", "pipe"]
3091
- });
3092
- return out.toString().split("\0").filter((p) => p !== "");
3093
- } catch {
3094
- return [];
3095
- }
3096
- }
3097
- function isTrackedInHead(rel, repo) {
3098
- try {
3099
- execFileSync11("git", ["cat-file", "-e", `HEAD:${rel}`], {
3100
- cwd: repo,
3101
- stdio: ["ignore", "pipe", "pipe"]
3102
- });
3103
- return true;
3104
- } catch {
3105
- return false;
3106
- }
3107
- }
3108
- function isInIndex(rel, repo) {
3109
- try {
3110
- const out = execFileSync11("git", ["ls-files", "--", rel], {
3111
- cwd: repo,
3112
- stdio: ["ignore", "pipe", "pipe"]
3113
- });
3114
- return out.toString().trim() !== "";
3115
- } catch {
3116
- return false;
3117
- }
3118
- }
3119
-
3120
- // src/commands.drop-session.scrub-hint.ts
3121
- init_config();
3122
- init_utils();
3123
- init_utils_json();
3124
- import { existsSync as existsSync23 } from "node:fs";
3125
- import { join as join27 } from "node:path";
3126
- var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
3127
- function reportScrubHint(id, matches) {
3128
- const live = resolveLiveTranscript(id, matches);
3129
- const target = live ?? `~/.claude/projects/<encoded>/${id}.jsonl`;
3130
- log(
3131
- `note: this only un-stages the session from the next push.
3132
- The local source still contains the secret, so nomad push re-stages it
3133
- on the next run and nomad doctor --check-shared keeps reporting it.
3134
- To fully remediate: rotate the credential, then run:
3135
- nomad redact ${id}
3136
- (or scrub ${target} manually)`
3137
- );
3138
- }
3139
- function resolveLiveTranscript(id, matches) {
3140
- try {
3141
- const mapPath = join27(repoHome(), "path-map.json");
3142
- if (!existsSync23(mapPath)) return null;
3143
- const projects = readJson(mapPath).projects;
3144
- const claude = claudeHome();
3145
- for (const rel of matches) {
3146
- const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
3147
- if (logical === void 0) continue;
3148
- const abs = projects[logical]?.[HOST];
3149
- if (abs === void 0) continue;
3150
- const live = join27(claude, "projects", encodePath(abs), `${id}.jsonl`);
3151
- if (existsSync23(live)) return live;
3152
- }
3153
- return null;
3154
- } catch {
3155
- return null;
3156
- }
3157
- }
3158
-
3159
- // src/commands.drop-session.ts
3160
- init_utils();
3161
-
3162
- // src/utils.lockfile.ts
3163
- init_config();
3164
- init_utils();
3165
- import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
3166
- import { dirname as dirname4, join as join28 } from "node:path";
3167
- function lockFilePath() {
3168
- return join28(home(), ".cache", "claude-nomad", "nomad.lock");
3169
- }
3170
- function acquireLock(verb) {
3171
- const lp = lockFilePath();
3172
- mkdirSync5(dirname4(lp), { recursive: true });
3173
- try {
3174
- const fd = openSync3(lp, "wx");
3175
- try {
3176
- writeFileSync3(fd, String(process.pid));
3177
- } catch (writeErr) {
3178
- try {
3179
- closeSync3(fd);
3180
- } catch {
3181
- }
3182
- try {
3183
- unlinkSync(lp);
3184
- } catch {
3185
- }
3186
- throw writeErr;
3187
- }
3188
- return { fd, path: lp };
3189
- } catch (err) {
3190
- const code = err.code;
3191
- if (code !== "EEXIST") throw err;
3192
- return checkStaleAndRetry(verb, lp);
3048
+ const code = err.code;
3049
+ if (code !== "EEXIST") throw err;
3050
+ return checkStaleAndRetry(verb, lp);
3193
3051
  }
3194
3052
  }
3195
3053
  function releaseLock(handle) {
@@ -3208,7 +3066,7 @@ function releaseLock(handle) {
3208
3066
  function unlinkIfSamePid(expectedPidStr, lp) {
3209
3067
  let current;
3210
3068
  try {
3211
- current = readFileSync9(lp, "utf8").trim();
3069
+ current = readFileSync11(lp, "utf8").trim();
3212
3070
  } catch {
3213
3071
  return false;
3214
3072
  }
@@ -3223,7 +3081,7 @@ function unlinkIfSamePid(expectedPidStr, lp) {
3223
3081
  function checkStaleAndRetry(verb, lp) {
3224
3082
  let pidStr;
3225
3083
  try {
3226
- pidStr = readFileSync9(lp, "utf8").trim();
3084
+ pidStr = readFileSync11(lp, "utf8").trim();
3227
3085
  } catch {
3228
3086
  pidStr = "";
3229
3087
  }
@@ -3252,7 +3110,7 @@ function retryOnce(verb, lp) {
3252
3110
  try {
3253
3111
  const fd = openSync3(lp, "wx");
3254
3112
  try {
3255
- writeFileSync3(fd, String(process.pid));
3113
+ writeFileSync4(fd, String(process.pid));
3256
3114
  } catch {
3257
3115
  try {
3258
3116
  closeSync3(fd);
@@ -3272,152 +3130,18 @@ function retryOnce(verb, lp) {
3272
3130
  }
3273
3131
  }
3274
3132
 
3275
- // src/commands.drop-session.ts
3276
- function cmdDropSession(id) {
3277
- if (id.length === 0 || id.length > 128 || !/^[A-Za-z0-9_-]+$/.test(id)) {
3278
- fail(`invalid session id: ${id}`);
3279
- process.exit(1);
3280
- }
3281
- const repo = repoHome();
3282
- if (!existsSync24(repo)) die(`repo not cloned at ${repo}`);
3283
- const handle = acquireLock("drop-session");
3284
- if (handle === null) process.exit(0);
3285
- try {
3286
- const repoProjects = join29(repo, "shared", "projects");
3287
- if (!existsSync24(repoProjects)) {
3288
- throw new NomadFatal(`no staged session matches ${id}`);
3289
- }
3290
- const matches = collectMatches(repoProjects, id, repo);
3291
- if (matches.length === 0) {
3292
- throw new NomadFatal(`no staged session matches ${id}`);
3293
- }
3294
- for (const rel of matches) unstageOne(rel, repo);
3295
- reportScrubHint(id, matches);
3296
- } catch (err) {
3297
- if (!(err instanceof NomadFatal)) {
3298
- throw err;
3299
- }
3300
- fail(err.message);
3301
- process.exitCode = 1;
3302
- } finally {
3303
- releaseLock(handle);
3304
- }
3305
- }
3306
- function collectMatches(repoProjects, id, repo) {
3307
- const matches = [];
3308
- for (const logical of readdirSync9(repoProjects)) {
3309
- const candidate = join29(repoProjects, logical, `${id}.jsonl`);
3310
- if (existsSync24(candidate)) {
3311
- matches.push(relative4(repo, candidate));
3312
- }
3313
- const dir = join29(repoProjects, logical, id);
3314
- if (existsSync24(dir) && statSync5(dir).isDirectory()) {
3315
- const dirRel = relative4(repo, dir);
3316
- const staged = expandStagedDir(dirRel, repo);
3317
- if (staged.length > 0) matches.push(...staged);
3318
- else matches.push(dirRel);
3319
- }
3320
- }
3321
- return matches;
3322
- }
3323
- function unstageOne(rel, repo) {
3324
- if (!isInIndex(rel, repo)) {
3325
- item(`dropped ${rel} (already absent from index)`);
3326
- return;
3327
- }
3328
- try {
3329
- if (isTrackedInHead(rel, repo)) {
3330
- execFileSync12("git", ["restore", "--staged", "--worktree", "--", rel], {
3331
- cwd: repo,
3332
- stdio: ["ignore", "pipe", "pipe"]
3333
- });
3334
- } else {
3335
- execFileSync12("git", ["rm", "--cached", "-f", "--", rel], {
3336
- cwd: repo,
3337
- stdio: ["ignore", "pipe", "pipe"]
3338
- });
3339
- }
3340
- } catch (err) {
3341
- const e = err;
3342
- const detail = e.stderr?.toString().trim() ?? e.message;
3343
- throw new NomadFatal(`git failed to unstage ${rel}: ${detail}`);
3344
- }
3345
- item(`dropped ${rel}`);
3346
- }
3347
-
3348
- // src/commands.redact.ts
3349
- init_config();
3350
- import { existsSync as existsSync26, statSync as statSync7 } from "node:fs";
3351
- import { dirname as dirname5, join as join31 } from "node:path";
3352
-
3353
- // src/commands.redact.subtree.ts
3354
- import { existsSync as existsSync25, lstatSync as lstatSync7, readFileSync as readFileSync10, readdirSync as readdirSync10, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
3355
- import { join as join30 } from "node:path";
3356
- init_utils_fs();
3357
- function collectFiles(dir, out) {
3358
- if (!existsSync25(dir)) return;
3359
- const st = lstatSync7(dir);
3360
- if (!st.isDirectory()) return;
3361
- for (const entry of readdirSync10(dir)) {
3362
- const abs = join30(dir, entry);
3363
- const lst = lstatSync7(abs);
3364
- if (lst.isSymbolicLink()) continue;
3365
- if (lst.isDirectory()) {
3366
- collectFiles(abs, out);
3367
- continue;
3368
- }
3369
- if (lst.isFile()) out.push(abs);
3370
- }
3371
- }
3372
- function listSubtreeFiles(sessionDir) {
3373
- const out = [];
3374
- collectFiles(sessionDir, out);
3375
- return out.sort((a, b) => a.localeCompare(b));
3376
- }
3377
- function newestSubtreeMtimeMs(mainPath, subtreeFiles, statMtime = (p) => statSync6(p).mtimeMs) {
3378
- let newest = statMtime(mainPath);
3379
- for (const filePath of subtreeFiles) {
3380
- const t = statMtime(filePath);
3381
- if (t > newest) newest = t;
3382
- }
3383
- return newest;
3384
- }
3385
- function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts, scan, dryRun) {
3386
- const dirty = [];
3387
- if (mainFindings.length > 0) dirty.push({ path: mainPath, findings: mainFindings });
3388
- for (const filePath of subtreeFiles) {
3389
- const raw = scan(filePath);
3390
- if (raw === null || raw.length === 0) continue;
3391
- const filtered = rule === void 0 ? raw : raw.filter((f) => f.RuleID === rule);
3392
- if (filtered.length === 0) continue;
3393
- dirty.push({ path: filePath, findings: filtered });
3394
- }
3395
- const total = dirty.reduce((n, e) => n + e.findings.length, 0);
3396
- if (!dryRun && total > 0) {
3397
- for (const { path: filePath, findings } of dirty) {
3398
- backupBeforeWrite(filePath, ts);
3399
- writeFileSync4(filePath, applyRedactions(readFileSync10(filePath, "utf8"), findings), "utf8");
3400
- }
3401
- }
3402
- return { total, dirty };
3403
- }
3404
-
3405
3133
  // src/commands.redact.ts
3406
- init_push_gitleaks_scan();
3407
- init_utils_fs();
3408
- init_utils_json();
3409
- init_utils();
3410
- function resolveLiveTranscript2(id) {
3134
+ function resolveLiveTranscript(id) {
3411
3135
  try {
3412
- const mapPath = join31(repoHome(), "path-map.json");
3413
- if (!existsSync26(mapPath)) return null;
3136
+ const mapPath = join28(repoHome(), "path-map.json");
3137
+ if (!existsSync23(mapPath)) return null;
3414
3138
  const projects = readJson(mapPath).projects;
3415
3139
  const claude = claudeHome();
3416
3140
  for (const hostMap of Object.values(projects)) {
3417
3141
  const abs = hostMap[HOST];
3418
3142
  if (abs === void 0) continue;
3419
- const live = join31(claude, "projects", encodePath(abs), `${id}.jsonl`);
3420
- if (existsSync26(live)) return live;
3143
+ const live = join28(claude, "projects", encodePath(abs), `${id}.jsonl`);
3144
+ if (existsSync23(live)) return live;
3421
3145
  }
3422
3146
  return null;
3423
3147
  } catch {
@@ -3437,19 +3161,19 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
3437
3161
  }
3438
3162
  const repo = repoHome();
3439
3163
  const backup = backupBase();
3440
- if (!existsSync26(repo)) die(`repo not cloned at ${repo}`);
3164
+ if (!existsSync23(repo)) die(`repo not cloned at ${repo}`);
3441
3165
  const handle = acquireLock("redact");
3442
3166
  if (handle === null) process.exit(0);
3443
3167
  try {
3444
- const localPath = resolveLiveTranscript2(id);
3445
- if (localPath === null || !existsSync26(localPath)) {
3168
+ const localPath = resolveLiveTranscript(id);
3169
+ if (localPath === null || !existsSync23(localPath)) {
3446
3170
  fail(`could not resolve local transcript for session ${id} on this host`);
3447
3171
  process.exitCode = 1;
3448
3172
  return;
3449
3173
  }
3450
- const sessionDir = join31(dirname5(localPath), id);
3174
+ const sessionDir = join28(dirname5(localPath), id);
3451
3175
  const subtreeFiles = listSubtreeFiles(sessionDir);
3452
- const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
3176
+ const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync6(p).mtimeMs);
3453
3177
  if (isRecentlyModified(subtreeMtime, nowMs())) {
3454
3178
  log(
3455
3179
  `session ${id} was modified recently and may be active.
@@ -3499,1142 +3223,1630 @@ ${lines}`);
3499
3223
  }
3500
3224
  }
3501
3225
 
3502
- // src/commands.pull.ts
3503
- import { existsSync as existsSync34, mkdirSync as mkdirSync8 } from "node:fs";
3504
- import { join as join40 } from "node:path";
3226
+ // src/commands.push.recovery.redact.ts
3227
+ init_push_gitleaks_scan();
3228
+ init_utils_json();
3229
+ init_utils();
3505
3230
 
3506
- // src/commands.push.sections.ts
3507
- init_color();
3231
+ // src/commands.push.recovery.seams.ts
3232
+ init_push_gitleaks();
3233
+ var MASK_LEAD = 4;
3234
+ var MASK_BODY = "************";
3235
+ var CONTEXT_WINDOW = 40;
3236
+ var CONTROL_CHARS = /[\x00-\x1f\x7f]/g;
3237
+ function findingKey(f) {
3238
+ return `${f.File}:${f.StartLine}:${f.StartColumn}:${f.RuleID}`;
3239
+ }
3240
+ var VALID_SID = /^[A-Za-z0-9_-]+$/;
3241
+ function sessionIdFromFinding(f) {
3242
+ const m = SESSION_PATH.exec(f.File) ?? /^shared\/projects\/[^/]+\/([^/]+)\//.exec(f.File);
3243
+ if (m === null) return null;
3244
+ const sid = m[1];
3245
+ return VALID_SID.test(sid) ? sid : null;
3246
+ }
3247
+ function parseAction(raw) {
3248
+ const t = raw.trim().toLowerCase();
3249
+ if (t === "r" || t === "redact") return "redact";
3250
+ if (t === "a" || t === "allow") return "allow";
3251
+ if (t === "d" || t === "drop") return "drop";
3252
+ return "skip";
3253
+ }
3254
+ function maskSecret(secret) {
3255
+ return secret.slice(0, MASK_LEAD) + MASK_BODY;
3256
+ }
3257
+ function buildFindingContext(finding, readLine) {
3258
+ const raw = readLine(finding.File, finding.StartLine);
3259
+ if (raw !== null) {
3260
+ const len = raw.length;
3261
+ const startCol = Math.max(1, Math.min(finding.StartColumn, len + 1));
3262
+ const endCol = Math.max(startCol, Math.min(finding.EndColumn, len));
3263
+ const spanStart = startCol - 1;
3264
+ const spanEnd = endCol;
3265
+ const secret = raw.slice(spanStart, spanEnd);
3266
+ const masked = maskSecret(secret);
3267
+ const fullPrefix = raw.slice(0, spanStart);
3268
+ const fullSuffix = raw.slice(spanEnd);
3269
+ const prefixTruncated = fullPrefix.length > CONTEXT_WINDOW;
3270
+ const suffixTruncated = fullSuffix.length > CONTEXT_WINDOW;
3271
+ const prefix = prefixTruncated ? fullPrefix.slice(fullPrefix.length - CONTEXT_WINDOW) : fullPrefix;
3272
+ const suffix = suffixTruncated ? fullSuffix.slice(0, CONTEXT_WINDOW) : fullSuffix;
3273
+ const excerpt = (prefixTruncated ? "..." : "") + prefix + masked + suffix + (suffixTruncated ? "..." : "");
3274
+ const stripped = excerpt.replace(CONTROL_CHARS, "");
3275
+ if (stripped.trim().length > 0) return stripped;
3276
+ }
3277
+ if (finding.Match.length > 0) {
3278
+ return maskSecret(finding.Match).replace(CONTROL_CHARS, "");
3279
+ }
3280
+ return null;
3281
+ }
3508
3282
 
3509
- // src/summary.ts
3510
- init_color();
3511
- init_utils();
3512
- function summaryText(verb, unmapped, collisions = 0, extrasSkipped = 0) {
3513
- const extras = extrasSkipped > 0 ? `, ${extrasSkipped} extras skipped` : "";
3514
- if (verb === "push") {
3515
- if (unmapped === 0 && collisions === 0 && extrasSkipped === 0) {
3516
- return { text: "summary: clean", clean: true };
3283
+ // src/commands.push.recovery.redact.ts
3284
+ function resolveStagedDir(localPath, map, claude, repo) {
3285
+ for (const [logical, hostMap] of Object.entries(map.projects)) {
3286
+ assertSafeLogical(logical);
3287
+ const abs = hostMap[HOST];
3288
+ if (abs === void 0) continue;
3289
+ if (localPath.startsWith(join29(claude, "projects", encodePath(abs)) + sep3)) {
3290
+ return join29(repo, "shared", "projects", logical);
3517
3291
  }
3518
- const base = `summary: ${unmapped} unmapped on push, ${collisions} collisions`;
3519
- return { text: `${base}${extras} (run nomad doctor to list)`, clean: false };
3520
- }
3521
- if (unmapped === 0 && extrasSkipped === 0) {
3522
- return { text: "summary: clean", clean: true };
3523
3292
  }
3524
- return {
3525
- text: `summary: ${unmapped} unmapped on ${verb}${extras} (run nomad doctor to list)`,
3526
- clean: false
3293
+ return null;
3294
+ }
3295
+ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3296
+ const refuse = (msg) => {
3297
+ log(msg);
3298
+ return false;
3527
3299
  };
3300
+ const claude = claudeHome();
3301
+ const repo = repoHome();
3302
+ const sid = sessionIdFromFinding(f);
3303
+ if (sid === null) {
3304
+ return refuse(
3305
+ `could not locate the local transcript for this finding; choose Skip or Drop session.`
3306
+ );
3307
+ }
3308
+ const localPath = resolveLiveTranscript(sid);
3309
+ if (localPath === null) {
3310
+ return refuse(
3311
+ `could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
3312
+ );
3313
+ }
3314
+ const sessionDir = join29(dirname6(localPath), sid);
3315
+ const subtreeFiles = listSubtreeFiles(sessionDir);
3316
+ const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
3317
+ if (isRecentlyModified(subtreeMtime, nowMs())) {
3318
+ return refuse(
3319
+ `session ${sid} looks active (modified within the last 5 minutes); refusing to redact, no changes made.
3320
+ End the session and choose Redact again, or choose Drop session (holds this session back from the push, local copy kept) or Skip.`
3321
+ );
3322
+ }
3323
+ const stagedProjectDir = resolveStagedDir(localPath, map, claude, repo);
3324
+ if (stagedProjectDir === null) {
3325
+ return refuse(
3326
+ `could not map the local transcript for session ${sid} to a staged copy; choose Drop session or Skip.`
3327
+ );
3328
+ }
3329
+ const mainFindings = scan(localPath);
3330
+ if (mainFindings === null) {
3331
+ return refuse(`re-scan of the transcript failed; choose Skip or Drop session.`);
3332
+ }
3333
+ const { total: anyTotal } = applySubtreeRedactions(
3334
+ localPath,
3335
+ mainFindings,
3336
+ subtreeFiles,
3337
+ void 0,
3338
+ ts,
3339
+ scan,
3340
+ false
3341
+ );
3342
+ if (anyTotal === 0) {
3343
+ return refuse(
3344
+ `nothing to redact in the local transcript for session ${sid}; choose Skip or Drop session.`
3345
+ );
3346
+ }
3347
+ mkdirSync6(stagedProjectDir, { recursive: true });
3348
+ cpSync5(localPath, join29(stagedProjectDir, `${sid}.jsonl`), { force: true });
3349
+ if (existsSync24(sessionDir)) {
3350
+ cpSync5(sessionDir, join29(stagedProjectDir, sid), { force: true, recursive: true });
3351
+ }
3352
+ return true;
3528
3353
  }
3529
- function summaryRow(verb, unmapped, collisions = 0, extrasSkipped = 0) {
3530
- const { text, clean } = summaryText(verb, unmapped, collisions, extrasSkipped);
3531
- return clean ? `${green(okGlyph)} ${text}` : `${yellow(warnGlyph)} ${text}`;
3354
+
3355
+ // src/commands.push.recovery.drop.ts
3356
+ init_config();
3357
+ import { rmSync as rmSync8 } from "node:fs";
3358
+ import { join as join30 } from "node:path";
3359
+ function dropSessionFromStaged(sid, map) {
3360
+ const logicals = Object.keys(map.projects);
3361
+ if (logicals.length === 0) return false;
3362
+ const repo = repoHome();
3363
+ for (const logical of logicals) {
3364
+ const jsonl = join30(repo, "shared", "projects", logical, `${sid}.jsonl`);
3365
+ const dir = join30(repo, "shared", "projects", logical, sid);
3366
+ rmSync8(jsonl, { force: true });
3367
+ rmSync8(dir, { recursive: true, force: true });
3368
+ }
3369
+ return true;
3532
3370
  }
3533
3371
 
3534
- // src/commands.push.sections.ts
3535
- function collapsedSkipRow(n, noun) {
3536
- if (n <= 0) return null;
3537
- return `${dim(infoGlyph)} ${n} ${noun}`;
3372
+ // src/commands.push.recovery.actions.ts
3373
+ init_push_gitleaks_scan();
3374
+ init_utils();
3375
+ function applyAllow(f, repo) {
3376
+ appendGitleaksIgnore(f.Fingerprint, repo);
3538
3377
  }
3539
- function buildSettingsSection(label) {
3540
- const s = section("Settings");
3541
- addItem(s, `${green(okGlyph)} settings.json (base + ${label})`);
3542
- return s;
3378
+ function allowAllFindings(findings, repo) {
3379
+ for (const f of findings) {
3380
+ appendGitleaksIgnore(f.Fingerprint, repo);
3381
+ }
3543
3382
  }
3544
- function buildSessionsSection(items, unmapped) {
3545
- const s = section("Sessions");
3546
- for (const logical of items) addItem(s, `${green(okGlyph)} ${logical}`);
3547
- const skip = collapsedSkipRow(unmapped, "not in path-map (run nomad doctor to list)");
3548
- if (skip !== null) addItem(s, skip);
3549
- return s;
3383
+ function allowFindingsByRule(findings, ruleId, repo) {
3384
+ let count = 0;
3385
+ for (const f of findings) {
3386
+ if (f.RuleID === ruleId) {
3387
+ appendGitleaksIgnore(f.Fingerprint, repo);
3388
+ count++;
3389
+ }
3390
+ }
3391
+ return count;
3550
3392
  }
3551
- function buildExtrasSection(items, extrasSkipped) {
3552
- const s = section("Extras");
3553
- for (const entry of items) addItem(s, `${green(okGlyph)} ${entry}`);
3554
- const skip = collapsedSkipRow(extrasSkipped, "extras skipped");
3555
- if (skip !== null) addItem(s, skip);
3556
- return s;
3393
+ function makeDefaultReadLine(repo) {
3394
+ return (file, line) => {
3395
+ try {
3396
+ const repoRoot = resolve3(repo);
3397
+ const target = resolve3(repoRoot, file);
3398
+ if (isAbsolute(file) || target !== repoRoot && !target.startsWith(repoRoot + sep4)) {
3399
+ return null;
3400
+ }
3401
+ const content = readFileSync12(target, "utf8");
3402
+ const lines = content.split(/\r?\n/);
3403
+ const idx = line - 1;
3404
+ if (idx < 0 || idx >= lines.length) return null;
3405
+ return lines[idx] ?? null;
3406
+ } catch {
3407
+ return null;
3408
+ }
3409
+ };
3557
3410
  }
3558
- function syncedSections(st) {
3559
- const sessions = st.dryRun ? st.remap.wouldPush : st.remap.pushed;
3560
- const extras = st.dryRun ? st.extras.wouldPush : st.extras.pushed;
3561
- return [
3562
- buildSessionsSection(sessions, st.remap.unmapped),
3563
- buildExtrasSection(extras, st.extras.skipped)
3564
- ];
3411
+ async function collectActions(findings, prompt, readLine) {
3412
+ const reader = readLine ?? makeDefaultReadLine(repoHome());
3413
+ const actions = /* @__PURE__ */ new Map();
3414
+ for (const f of findings) {
3415
+ const sid = sessionIdFromFinding(f);
3416
+ const ctx = buildFindingContext(f, reader);
3417
+ const header = `
3418
+ Finding: ${f.RuleID} in ${f.File} line ${f.StartLine}` + (sid === null ? "" : ` (session: ${sid})`) + (ctx === null ? "" : `
3419
+ context: ${ctx}`) + "\n [R]edact [A]llow [D]rop session [S]kip (default)\n";
3420
+ actions.set(findingKey(f), parseAction(await prompt(header + "> ")));
3421
+ }
3422
+ return actions;
3565
3423
  }
3566
- function summarySection(st) {
3567
- const s = section("Summary");
3568
- const unmapped = st.remap.unmapped + st.extras.unmapped;
3569
- addItem(s, summaryRow("push", unmapped, st.remap.collisions, st.extras.skipped));
3570
- return s;
3424
+ function dispatchOne(f, ctx) {
3425
+ const action = ctx.actions.get(findingKey(f)) ?? "skip";
3426
+ if (action === "skip") return;
3427
+ const sid = sessionIdFromFinding(f);
3428
+ if (sid !== null && ctx.droppedSids.has(sid)) return;
3429
+ if (action === "allow") {
3430
+ applyAllow(f, ctx.repo);
3431
+ return;
3432
+ }
3433
+ if (sid === null) return;
3434
+ if (action === "drop") {
3435
+ ctx.droppedSids.add(sid);
3436
+ if (ctx.drop(sid, ctx.map)) {
3437
+ log(
3438
+ `dropped session ${sid} from this push (local transcript kept; the secret remains in your local copy)`
3439
+ );
3440
+ }
3441
+ return;
3442
+ }
3443
+ if (action === "redact" && !ctx.redactedSids.has(sid)) {
3444
+ if (applyRedact(f, ctx.ts, ctx.map, ctx.nowMs, ctx.scan)) ctx.redactedSids.add(sid);
3445
+ }
3571
3446
  }
3572
- function renderPushTree(st, verdict) {
3573
- const leakScan = section("Leak scan");
3574
- addItem(leakScan, verdict.verdictRow);
3575
- renderTree([...syncedSections(st), leakScan, summarySection(st)]);
3447
+ function dispatchActions(findings, actions, opts) {
3448
+ const { ts, map, nowMs, repo, scan = scanFile, drop = dropSessionFromStaged } = opts;
3449
+ const ctx = {
3450
+ actions,
3451
+ ts,
3452
+ map,
3453
+ nowMs,
3454
+ repo,
3455
+ scan,
3456
+ drop,
3457
+ redactedSids: /* @__PURE__ */ new Set(),
3458
+ droppedSids: /* @__PURE__ */ new Set()
3459
+ };
3460
+ for (const f of findings) {
3461
+ dispatchOne(f, ctx);
3462
+ }
3576
3463
  }
3577
- function renderNoScanTree(st, opts = {}) {
3578
- const sections = [];
3579
- if (opts.noMapHint === true) {
3580
- const pathMap = section("Path map");
3581
- addItem(pathMap, `${dim(infoGlyph)} no path-map.json (nothing to preview)`);
3582
- sections.push(pathMap);
3464
+ function redactAllFindings(findings, ts, map, nowMs, scan = scanFile) {
3465
+ const redactedSids = /* @__PURE__ */ new Set();
3466
+ for (const f of findings) {
3467
+ const sid = sessionIdFromFinding(f);
3468
+ if (sid === null || redactedSids.has(sid)) continue;
3469
+ if (applyRedact(f, ts, map, nowMs, scan)) redactedSids.add(sid);
3583
3470
  }
3584
- renderTree([...sections, ...syncedSections(st), summarySection(st)]);
3585
3471
  }
3586
3472
 
3587
- // src/commands.pull.ts
3588
- init_config();
3589
-
3590
- // src/extras-sync.ts
3591
- init_config();
3592
- import { existsSync as existsSync29 } from "node:fs";
3593
- import { join as join34 } from "node:path";
3594
-
3595
- // src/extras-sync.diff.ts
3473
+ // src/commands.push.recovery.ts
3474
+ init_push_gitleaks_scan();
3475
+ init_push_gitleaks();
3596
3476
  init_utils();
3597
- import { execFileSync as execFileSync13 } from "node:child_process";
3598
- function labelDiffLine(line) {
3599
- const tab = line.indexOf(" ");
3600
- if (tab === -1) return line;
3601
- const status = line.slice(0, tab);
3602
- const path = line.slice(tab + 1);
3603
- if (status === "D") return `${path} (local only)`;
3604
- if (status === "A") return `${path} (repo only)`;
3605
- return path;
3477
+ function isTTY(stdin = process.stdin, stdout = process.stdout) {
3478
+ return stdin.isTTY === true && stdout.isTTY === true;
3606
3479
  }
3607
- function parseDiffOutput(stdout) {
3608
- return stdout.split("\n").filter((line) => line.length > 0).map(labelDiffLine);
3480
+ function hasUnresolved(actions) {
3481
+ for (const action of actions.values()) {
3482
+ if (action === "skip") return true;
3483
+ }
3484
+ return false;
3609
3485
  }
3610
- function listDivergingFiles(a, b) {
3486
+ function printRecoveryLegend(print = console.log) {
3487
+ print("");
3488
+ print("Recovery actions:");
3489
+ print(" Redact - scrub the secret from the local transcript, push the cleaned copy");
3490
+ print(" Allow - mark as false positive (adds a .gitleaksignore fingerprint), push as-is");
3491
+ print(" Drop session - exclude this session from this push (local transcript kept, running");
3492
+ print(" session is not stopped)");
3493
+ print(" Skip - leave unresolved (the push aborts)");
3494
+ print("");
3495
+ }
3496
+ function applyThenRescan(scanVerdict, repoHome2) {
3497
+ gitOrFatal(["add", "-A"], "git add", repoHome2);
3498
+ const next = scanVerdict(repoHome2);
3499
+ if (next.leak) {
3500
+ const { bySession, other } = partitionFindings(next.findings);
3501
+ throw new NomadFatal(buildSessionAwareFatal(bySession, other));
3502
+ }
3503
+ return next;
3504
+ }
3505
+ function allowThenRescan(append, scanVerdict, repoHome2) {
3506
+ const ignPath = join31(repoHome2, ".gitleaksignore");
3507
+ let before;
3611
3508
  try {
3612
- const stdout = execFileSync13("git", ["diff", "--no-index", "--name-status", a, b], {
3613
- stdio: ["ignore", "pipe", "pipe"]
3614
- }).toString();
3615
- return parseDiffOutput(stdout);
3509
+ before = readFileSync13(ignPath, "utf8");
3510
+ } catch {
3511
+ before = null;
3512
+ }
3513
+ append();
3514
+ try {
3515
+ return applyThenRescan(scanVerdict, repoHome2);
3616
3516
  } catch (err) {
3617
- const e = err;
3618
- if (e.status === 1 && e.stdout !== void 0) {
3619
- return parseDiffOutput(e.stdout.toString());
3620
- }
3621
- if (e.code === "ENOENT") {
3622
- warn(`git not on PATH; divergence check skipped for ${a}`);
3623
- return [];
3624
- }
3625
- warn(`divergence check failed for ${a}: ${e.message ?? String(err)}`);
3626
- return [];
3517
+ if (before === null) rmSync9(ignPath, { force: true });
3518
+ else writeFileSync5(ignPath, before, "utf8");
3519
+ throw err;
3627
3520
  }
3628
3521
  }
3629
-
3630
- // src/extras-sync.core.ts
3631
- init_config();
3632
- import { cpSync as cpSync5, existsSync as existsSync27, rmSync as rmSync8 } from "node:fs";
3633
- import { join as join32 } from "node:path";
3634
-
3635
- // src/extras-sync.guards.ts
3636
- init_utils();
3637
- init_config_sharedDirs_guard();
3638
- import { isAbsolute, normalize } from "node:path";
3639
- function assertSafeLocalRoot(localRoot, logical) {
3640
- if (!isAbsolute(localRoot)) {
3641
- throw new NomadFatal(
3642
- `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
3643
- );
3522
+ function makeRealPrompt() {
3523
+ return async (prompt) => {
3524
+ const rl = createInterface({
3525
+ input: process.stdin,
3526
+ output: process.stdout,
3527
+ terminal: true
3528
+ });
3529
+ try {
3530
+ return await rl.question(prompt);
3531
+ } finally {
3532
+ rl.close();
3533
+ }
3534
+ };
3535
+ }
3536
+ async function resolveLeakFindings(verdict, ts, map, deps = {}) {
3537
+ const {
3538
+ isTTYCheck = isTTY,
3539
+ nowMs = Date.now,
3540
+ redactAll = false,
3541
+ allowAll = false,
3542
+ allowRule,
3543
+ makePrompt: makePromptFn = makeRealPrompt,
3544
+ scan = scanFile,
3545
+ printLegend = printRecoveryLegend
3546
+ } = deps;
3547
+ const scanVerdict = deps.scanVerdict ?? (await Promise.resolve().then(() => (init_push_leak_verdict(), push_leak_verdict_exports))).scanPushVerdict;
3548
+ const repo = repoHome();
3549
+ let current = verdict;
3550
+ if (redactAll) {
3551
+ redactAllFindings(current.findings, ts, map, nowMs, scan);
3552
+ return applyThenRescan(scanVerdict, repo);
3644
3553
  }
3645
- if (localRoot !== normalize(localRoot)) {
3646
- throw new NomadFatal(
3647
- `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be already-normalized; no '..' or redundant segments)`
3554
+ if (allowAll) {
3555
+ return allowThenRescan(() => allowAllFindings(current.findings, repo), scanVerdict, repo);
3556
+ }
3557
+ if (allowRule !== void 0) {
3558
+ return allowThenRescan(
3559
+ () => {
3560
+ const matched = allowFindingsByRule(current.findings, allowRule, repo);
3561
+ if (matched === 0) log(`no findings matched rule ${allowRule}; re-scanning`);
3562
+ },
3563
+ scanVerdict,
3564
+ repo
3648
3565
  );
3649
3566
  }
3567
+ if (!isTTYCheck()) {
3568
+ throw new NomadFatal(current.recovery ?? "gitleaks detected secrets");
3569
+ }
3570
+ const prompt = makePromptFn();
3571
+ printLegend();
3572
+ while (current.leak && current.findings.length > 0) {
3573
+ const actions = await collectActions(current.findings, prompt);
3574
+ if (hasUnresolved(actions)) {
3575
+ const unresolved = current.findings.filter((f) => actions.get(findingKey(f)) === "skip");
3576
+ const { bySession, other } = partitionFindings(unresolved);
3577
+ throw new NomadFatal(buildSessionAwareFatal(bySession, other));
3578
+ }
3579
+ dispatchActions(current.findings, actions, { ts, map, nowMs, repo, scan });
3580
+ gitOrFatal(["add", "-A"], "git add", repo);
3581
+ current = scanVerdict(repo);
3582
+ }
3583
+ return current;
3650
3584
  }
3651
3585
 
3652
- // src/extras-sync.core.ts
3653
- init_utils();
3654
- init_utils_json();
3655
- function loadValidatedExtras(opts) {
3656
- const repo = repoHome();
3657
- const mapPath = join32(repo, "path-map.json");
3658
- const repoExtras = join32(repo, "shared", "extras");
3659
- if (!existsSync27(mapPath) || opts.requireRepoExtras === true && !existsSync27(repoExtras)) {
3660
- if (opts.missingMsg !== void 0) log(opts.missingMsg);
3661
- return null;
3662
- }
3663
- const map = readPathMap(mapPath);
3664
- const extrasMap = map.extras ?? {};
3665
- if (Object.keys(extrasMap).length === 0) return null;
3666
- for (const logical of Object.keys(extrasMap)) {
3667
- assertSafeLogical(logical);
3668
- const localRoot = map.projects[logical]?.[HOST];
3669
- if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
3670
- }
3671
- return { map, extrasMap };
3586
+ // src/spinner.ts
3587
+ function formatElapsed(ms) {
3588
+ return `${(ms / 1e3).toFixed(1)}s`;
3672
3589
  }
3673
- function* eachExtrasTarget(v, counts) {
3674
- const whitelist = SUPPORTED_EXTRAS;
3675
- for (const [logical, dirnames] of Object.entries(v.extrasMap)) {
3676
- const localRoot = v.map.projects[logical]?.[HOST];
3677
- if (!localRoot || localRoot === "TBD") {
3678
- counts.unmapped++;
3679
- continue;
3590
+ function writePlainStart(out, label) {
3591
+ out.write(`${label}...
3592
+ `);
3593
+ }
3594
+ function writePlainDone(out, label, ms) {
3595
+ out.write(`${label} done (${formatElapsed(ms)})
3596
+ `);
3597
+ }
3598
+ function writeAnimatedDone(out, label, ms, useTTY) {
3599
+ out.write("\r\x1B[K");
3600
+ const glyph = useTTY ? green(okGlyph) : okGlyph;
3601
+ out.write(`${glyph} ${label} (${formatElapsed(ms)})
3602
+ `);
3603
+ }
3604
+ function resolveWorkerPath(deps = {}) {
3605
+ const check = deps.existsSyncFn ?? existsSync25;
3606
+ const base = deps.baseUrl ?? import.meta.url;
3607
+ const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
3608
+ if (check(mjs)) return mjs;
3609
+ return fileURLToPath4(new URL("./spinner.worker.ts", base));
3610
+ }
3611
+ function makeRealWorker() {
3612
+ return new Worker(resolveWorkerPath());
3613
+ }
3614
+ function startSpinner(label, deps = {}) {
3615
+ const ttyCheck = deps.isTTYCheck ?? (() => isTTY());
3616
+ const env = deps.env ?? process.env;
3617
+ const out = deps.out ?? process.stderr;
3618
+ const now = deps.now ?? Date.now;
3619
+ const startMs = now();
3620
+ const animate = ttyCheck() && !env.CI;
3621
+ let worker = null;
3622
+ let degraded = false;
3623
+ let finalized = false;
3624
+ if (animate) {
3625
+ const factory = deps.makeWorker ?? makeRealWorker;
3626
+ try {
3627
+ worker = factory();
3628
+ worker.unref?.();
3629
+ worker.postMessage({ type: "start", label });
3630
+ } catch {
3631
+ degraded = true;
3632
+ worker = null;
3633
+ writePlainStart(out, label);
3680
3634
  }
3681
- for (const dirname7 of dirnames) {
3682
- if (!whitelist.includes(dirname7)) {
3683
- counts.skipped++;
3684
- continue;
3685
- }
3686
- yield { logical, localRoot, dirname: dirname7 };
3635
+ } else {
3636
+ writePlainStart(out, label);
3637
+ }
3638
+ function finalize(success, doneLabel) {
3639
+ if (finalized) return;
3640
+ finalized = true;
3641
+ const dl = doneLabel ?? label;
3642
+ const elapsed = now() - startMs;
3643
+ if (animate && !degraded && worker !== null) {
3644
+ worker.postMessage({ type: "pause" });
3645
+ worker.terminate();
3646
+ worker = null;
3647
+ if (success) writeAnimatedDone(out, dl, elapsed, ttyCheck());
3648
+ else out.write("\r\x1B[K");
3649
+ } else if (success) {
3650
+ writePlainDone(out, dl, elapsed);
3687
3651
  }
3688
3652
  }
3653
+ return {
3654
+ succeed: (doneLabel) => finalize(true, doneLabel),
3655
+ stop: () => finalize(false)
3656
+ };
3689
3657
  }
3690
- function copyExtras(src, dst) {
3691
- rmSync8(dst, { recursive: true, force: true });
3692
- cpSync5(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
3658
+ function withSpinner(label, fn, deps) {
3659
+ const sp = startSpinner(label, deps);
3660
+ try {
3661
+ const result = fn();
3662
+ sp.succeed();
3663
+ return result;
3664
+ } finally {
3665
+ sp.stop();
3666
+ }
3693
3667
  }
3694
3668
 
3695
- // src/extras-sync.ts
3696
- init_utils();
3697
- init_utils_json();
3698
-
3699
- // src/extras-sync.remap.ts
3669
+ // src/commands.doctor.gitleaks-version.ts
3670
+ init_color();
3671
+ import { execFileSync as execFileSync7 } from "node:child_process";
3672
+ import { existsSync as existsSync26 } from "node:fs";
3673
+ import { join as join32 } from "node:path";
3700
3674
  init_config();
3701
- import { existsSync as existsSync28, mkdirSync as mkdirSync6 } from "node:fs";
3702
- import { join as join33 } from "node:path";
3703
- init_utils_fs();
3704
- function runExtrasOp(v, dryRun, paths, backup) {
3705
- const counts = { unmapped: 0, skipped: 0 };
3706
- const done = [];
3707
- const would = [];
3708
- for (const t of eachExtrasTarget(v, counts)) {
3709
- const { src, dst } = paths(t);
3710
- if (!existsSync28(src)) continue;
3711
- const item2 = `${t.logical}/${t.dirname}`;
3712
- if (dryRun) {
3713
- would.push(item2);
3714
- continue;
3715
- }
3716
- backup(dst, t.localRoot);
3717
- copyExtras(src, dst);
3718
- done.push(item2);
3719
- }
3720
- return { ...counts, done, would };
3675
+ var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
3676
+ var GITLEAKS_TIMEOUT_MS = 5e3;
3677
+ function majorMinorOf(value) {
3678
+ const m = SEMVER_MAJOR_MINOR.exec(value);
3679
+ return m === null ? null : [m[1], m[2]];
3721
3680
  }
3722
- function remapExtrasPush(ts, opts = {}) {
3723
- const dryRun = opts.dryRun === true;
3724
- const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
3725
- if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
3726
- const repo = repoHome();
3727
- const repoExtras = join33(repo, "shared", "extras");
3728
- if (!dryRun) mkdirSync6(repoExtras, { recursive: true });
3729
- const { unmapped, skipped, done, would } = runExtrasOp(
3730
- v,
3731
- dryRun,
3732
- ({ localRoot, logical, dirname: dirname7 }) => ({
3733
- src: join33(localRoot, dirname7),
3734
- dst: join33(repoExtras, logical, dirname7)
3735
- }),
3736
- (dst) => backupRepoWrite(dst, ts, repo)
3737
- );
3738
- return { unmapped, skipped, pushed: done, wouldPush: would };
3681
+ function readGitleaksVersion(run, tomlExists) {
3682
+ const tomlPath = join32(repoHome(), ".gitleaks.toml");
3683
+ const args = ["version"];
3684
+ if (tomlExists(tomlPath)) args.push("--config", tomlPath);
3685
+ try {
3686
+ return run("gitleaks", args, {
3687
+ stdio: ["ignore", "pipe", "pipe"],
3688
+ timeout: GITLEAKS_TIMEOUT_MS
3689
+ }).toString().trim();
3690
+ } catch {
3691
+ return null;
3692
+ }
3739
3693
  }
3740
- function remapExtrasPull(ts, opts = {}) {
3741
- const dryRun = opts.dryRun === true;
3742
- const v = loadValidatedExtras({
3743
- requireRepoExtras: true,
3744
- missingMsg: "no path-map or repo extras dir; skipping extras remap"
3745
- });
3746
- if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
3747
- const repoExtras = join33(repoHome(), "shared", "extras");
3748
- const { unmapped, skipped, done, would } = runExtrasOp(
3749
- v,
3750
- dryRun,
3751
- ({ localRoot, logical, dirname: dirname7 }) => ({
3752
- src: join33(repoExtras, logical, dirname7),
3753
- dst: join33(localRoot, dirname7)
3754
- }),
3755
- // Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
3756
- // localRoot so the backup tree mirrors the project layout.
3757
- (dst, localRoot) => backupExtrasWrite(dst, ts, localRoot)
3694
+ function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync26) {
3695
+ const raw = readGitleaksVersion(run, tomlExists);
3696
+ if (raw === null) return;
3697
+ const local = majorMinorOf(raw);
3698
+ if (local === null) return;
3699
+ const pin = majorMinorOf(GITLEAKS_PINNED_VERSION);
3700
+ if (pin === null) return;
3701
+ const sameMajorMinor = local[0] === pin[0] && local[1] === pin[1];
3702
+ if (sameMajorMinor) {
3703
+ addItem(section2, `${green(okGlyph)} gitleaks: ${raw} (matches pinned ${pin[0]}.${pin[1]})`);
3704
+ return;
3705
+ }
3706
+ addItem(
3707
+ section2,
3708
+ `${yellow(warnGlyph)} gitleaks: ${raw} -> ${GITLEAKS_PINNED_VERSION} (CI pins this; local drift may change scan results)`
3758
3709
  );
3759
- return { unmapped, skipped, pulled: done, wouldPull: would };
3760
3710
  }
3761
3711
 
3762
- // src/extras-sync.ts
3763
- function divergenceCheckExtras(ts) {
3764
- const v = loadValidatedExtras({});
3765
- if (v === null) return;
3766
- const counts = { unmapped: 0, skipped: 0 };
3767
- const backupRoot = join34(backupBase(), ts, "extras");
3768
- const repo = repoHome();
3769
- for (const { logical, localRoot, dirname: dirname7 } of eachExtrasTarget(v, counts)) {
3770
- const local = join34(localRoot, dirname7);
3771
- const repoEntry = join34(repo, "shared", "extras", logical, dirname7);
3772
- if (!existsSync29(local) || !existsSync29(repoEntry)) continue;
3773
- const diff = listDivergingFiles(local, repoEntry);
3774
- if (diff.length === 0) continue;
3775
- const projectBackupRoot = join34(backupRoot, encodePath(localRoot));
3776
- warn(
3777
- `local ${dirname7} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
3778
- );
3779
- for (const f of diff) warn(` ${f}`);
3712
+ // src/commands.doctor.checks.deps.ts
3713
+ init_color();
3714
+ import { execFileSync as execFileSync8 } from "node:child_process";
3715
+ var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
3716
+ var PROBE_TIMEOUT_MS = 3e3;
3717
+ var FETCHER_BASE = "HTTP fetcher";
3718
+ function parseFirstVersion(line) {
3719
+ const m = VERSION_TOKEN.exec(line);
3720
+ return m ? m[1] : null;
3721
+ }
3722
+ function probeOptionalDep(bin, run) {
3723
+ try {
3724
+ const firstLine = run(bin, ["--version"], {
3725
+ stdio: ["ignore", "pipe", "pipe"],
3726
+ timeout: PROBE_TIMEOUT_MS
3727
+ }).toString().split("\n")[0].trim();
3728
+ const version = parseFirstVersion(firstLine);
3729
+ return { status: "present", version };
3730
+ } catch (err) {
3731
+ if (err.code === "ENOENT") {
3732
+ return { status: "not-installed" };
3733
+ }
3734
+ return { status: "present", version: null };
3780
3735
  }
3781
3736
  }
3782
-
3783
- // src/links.ts
3784
- init_config();
3785
- init_utils();
3786
- init_utils_fs();
3787
- init_utils_json();
3788
- import { existsSync as existsSync30, lstatSync as lstatSync8, rmSync as rmSync9 } from "node:fs";
3789
- import { join as join35 } from "node:path";
3790
- function emitAutoMove(onPreview, linkPath, ts, name) {
3791
- if (onPreview) {
3792
- onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
3737
+ function reportFetcherRow(section2, run) {
3738
+ const curl = probeOptionalDep("curl", run);
3739
+ const wget = probeOptionalDep("wget", run);
3740
+ if (curl.status === "present") {
3741
+ addItem(section2, `${green(okGlyph)} ${FETCHER_BASE}: curl ${curl.version ?? "(present)"}`);
3742
+ } else if (wget.status === "present") {
3743
+ addItem(section2, `${green(okGlyph)} ${FETCHER_BASE}: wget ${wget.version ?? "(present)"}`);
3793
3744
  } else {
3794
- log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
3745
+ addItem(
3746
+ section2,
3747
+ `${yellow(warnGlyph)} ${FETCHER_BASE} (curl or wget): not installed (optional; needed for release-version staleness check + nomad doctor --check-schema)`
3748
+ );
3795
3749
  }
3796
3750
  }
3797
- function emitCreate(onPreview, from, to) {
3798
- if (onPreview) {
3799
- onPreview({ kind: "create", from, to });
3751
+ function reportOptionalDeps(section2, run = execFileSync8) {
3752
+ const gh = probeOptionalDep("gh", run);
3753
+ if (gh.status === "present") {
3754
+ addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
3800
3755
  } else {
3801
- log(`would create symlink: ${from} -> ${to}`);
3756
+ addItem(
3757
+ section2,
3758
+ `${yellow(warnGlyph)} gh: not installed (optional; needed for nomad init Actions auto-disable + the Actions-drift check)`
3759
+ );
3802
3760
  }
3803
- }
3804
- function applySharedLinks(ts, map, opts = {}) {
3805
- const dryRun = opts.dryRun === true;
3806
- const claude = claudeHome();
3807
- const repo = repoHome();
3808
- const linkNames = allSharedLinks(map);
3809
- for (const name of linkNames) {
3810
- const linkPath = join35(claude, name);
3811
- const target = join35(repo, "shared", name);
3812
- if (!existsSync30(linkPath)) continue;
3813
- if (lstatSync8(linkPath).isSymbolicLink()) continue;
3814
- if (!existsSync30(target)) continue;
3815
- if (dryRun) {
3816
- emitAutoMove(opts.onPreview, linkPath, ts, name);
3817
- continue;
3818
- }
3819
- backupBeforeWrite(linkPath, ts);
3820
- rmSync9(linkPath, { recursive: true, force: true });
3821
- }
3822
- for (const name of linkNames) {
3823
- const target = join35(repo, "shared", name);
3824
- if (!existsSync30(target)) continue;
3825
- if (dryRun) {
3826
- emitCreate(opts.onPreview, join35(claude, name), target);
3827
- continue;
3828
- }
3829
- ensureSymlink(join35(claude, name), target);
3830
- }
3831
- }
3832
- function regenerateSettings(ts, opts = {}) {
3833
- const dryRun = opts.dryRun === true;
3834
- const repo = repoHome();
3835
- const claude = claudeHome();
3836
- const basePath = join35(repo, "shared", "settings.base.json");
3837
- const hostPath = join35(repo, "hosts", `${HOST}.json`);
3838
- if (!existsSync30(basePath)) {
3839
- die("repo not initialized; run 'nomad init' to scaffold");
3840
- }
3841
- const base = readJson(basePath);
3842
- const hasOverrides = existsSync30(hostPath);
3843
- const overrides = hasOverrides ? readJson(hostPath) : {};
3844
- const merged = deepMerge(base, overrides);
3845
- const settingsPath = join35(claude, "settings.json");
3846
- if (!hasOverrides && existsSync30(settingsPath)) {
3847
- try {
3848
- const existing = readJson(settingsPath);
3849
- const baseKeys = new Set(Object.keys(base));
3850
- const drift = Object.keys(existing).filter((k) => !baseKeys.has(k));
3851
- if (drift.length > 0) {
3852
- warn(
3853
- `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.`
3854
- );
3855
- }
3856
- } catch {
3857
- warn("existing settings.json is malformed; skipping drift-check and regenerating.");
3858
- }
3859
- }
3860
- const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
3861
- if (dryRun) {
3862
- log(`would write settings.json (base + ${overrideLabel})`);
3863
- return { label: overrideLabel };
3864
- }
3865
- backupBeforeWrite(settingsPath, ts);
3866
- writeJsonAtomic(settingsPath, merged);
3867
- return { label: overrideLabel };
3761
+ reportFetcherRow(section2, run);
3868
3762
  }
3869
3763
 
3870
- // src/preview.ts
3764
+ // src/commands.doctor.actions-drift.ts
3765
+ init_color();
3766
+ import { execFileSync as execFileSync10 } from "node:child_process";
3871
3767
  init_config();
3872
- import { existsSync as existsSync31 } from "node:fs";
3873
- import { join as join36 } from "node:path";
3874
3768
 
3875
- // node_modules/diff/libesm/diff/base.js
3876
- var Diff = class {
3877
- diff(oldStr, newStr, options = {}) {
3878
- let callback;
3879
- if (typeof options === "function") {
3880
- callback = options;
3881
- options = {};
3882
- } else if ("callback" in options) {
3883
- callback = options.callback;
3884
- }
3885
- const oldString = this.castInput(oldStr, options);
3886
- const newString = this.castInput(newStr, options);
3887
- const oldTokens = this.removeEmpty(this.tokenize(oldString, options));
3888
- const newTokens = this.removeEmpty(this.tokenize(newString, options));
3889
- return this.diffWithOptionsObj(oldTokens, newTokens, options, callback);
3890
- }
3891
- diffWithOptionsObj(oldTokens, newTokens, options, callback) {
3892
- var _a;
3893
- const done = (value) => {
3894
- value = this.postProcess(value, options);
3895
- if (callback) {
3896
- setTimeout(function() {
3897
- callback(value);
3898
- }, 0);
3899
- return void 0;
3900
- } else {
3901
- return value;
3902
- }
3903
- };
3904
- const newLen = newTokens.length, oldLen = oldTokens.length;
3905
- let editLength = 1;
3906
- let maxEditLength = newLen + oldLen;
3907
- if (options.maxEditLength != null) {
3908
- maxEditLength = Math.min(maxEditLength, options.maxEditLength);
3909
- }
3910
- const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity;
3911
- const abortAfterTimestamp = Date.now() + maxExecutionTime;
3912
- const bestPath = [{ oldPos: -1, lastComponent: void 0 }];
3913
- let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
3914
- if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
3915
- return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
3916
- }
3917
- let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
3918
- const execEditLength = () => {
3919
- for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
3920
- let basePath;
3921
- const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
3922
- if (removePath) {
3923
- bestPath[diagonalPath - 1] = void 0;
3924
- }
3925
- let canAdd = false;
3926
- if (addPath) {
3927
- const addPathNewPos = addPath.oldPos - diagonalPath;
3928
- canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
3929
- }
3930
- const canRemove = removePath && removePath.oldPos + 1 < oldLen;
3931
- if (!canAdd && !canRemove) {
3932
- bestPath[diagonalPath] = void 0;
3933
- continue;
3934
- }
3935
- if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) {
3936
- basePath = this.addToPath(addPath, true, false, 0, options);
3937
- } else {
3938
- basePath = this.addToPath(removePath, false, true, 1, options);
3939
- }
3940
- newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
3941
- if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
3942
- return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
3943
- } else {
3944
- bestPath[diagonalPath] = basePath;
3945
- if (basePath.oldPos + 1 >= oldLen) {
3946
- maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
3947
- }
3948
- if (newPos + 1 >= newLen) {
3949
- minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
3950
- }
3951
- }
3952
- }
3953
- editLength++;
3954
- };
3955
- if (callback) {
3956
- (function exec() {
3957
- setTimeout(function() {
3958
- if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
3959
- return callback(void 0);
3960
- }
3961
- if (!execEditLength()) {
3962
- exec();
3963
- }
3964
- }, 0);
3965
- })();
3966
- } else {
3967
- while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
3968
- const ret = execEditLength();
3969
- if (ret) {
3970
- return ret;
3971
- }
3972
- }
3973
- }
3974
- }
3975
- addToPath(path, added, removed, oldPosInc, options) {
3976
- const last = path.lastComponent;
3977
- if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
3978
- return {
3979
- oldPos: path.oldPos + oldPosInc,
3980
- lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
3981
- };
3982
- } else {
3983
- return {
3984
- oldPos: path.oldPos + oldPosInc,
3985
- lastComponent: { count: 1, added, removed, previousComponent: last }
3986
- };
3987
- }
3769
+ // src/gh-actions.ts
3770
+ import { execFileSync as execFileSync9 } from "node:child_process";
3771
+ var GH_TIMEOUT_MS = 5e3;
3772
+ function parseGitHubRemote(remoteUrl) {
3773
+ const normalized = remoteUrl.trim().replace(/\/$/, "");
3774
+ const m = /github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/.exec(normalized);
3775
+ if (m === null) return null;
3776
+ return { owner: m[1], repo: m[2] };
3777
+ }
3778
+ function ghAuthStatus(run = execFileSync9) {
3779
+ try {
3780
+ run("gh", ["auth", "status"], {
3781
+ stdio: ["ignore", "ignore", "ignore"],
3782
+ timeout: GH_TIMEOUT_MS
3783
+ });
3784
+ return null;
3785
+ } catch (err) {
3786
+ const e = err;
3787
+ if (e.code === "ENOENT") return "gh-not-installed";
3788
+ if (typeof e.status === "number") return "gh-not-authed";
3789
+ return "gh-probe-error";
3988
3790
  }
3989
- extractCommon(basePath, newTokens, oldTokens, diagonalPath, options) {
3990
- const newLen = newTokens.length, oldLen = oldTokens.length;
3991
- let oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
3992
- while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) {
3993
- newPos++;
3994
- oldPos++;
3995
- commonCount++;
3996
- if (options.oneChangePerToken) {
3997
- basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false };
3998
- }
3999
- }
4000
- if (commonCount && !options.oneChangePerToken) {
4001
- basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false };
4002
- }
4003
- basePath.oldPos = oldPos;
4004
- return newPos;
3791
+ }
3792
+ function isRepoPrivate(ref, run = execFileSync9) {
3793
+ const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
3794
+ stdio: ["ignore", "pipe", "ignore"],
3795
+ timeout: GH_TIMEOUT_MS
3796
+ }).toString();
3797
+ const parsed = JSON.parse(out);
3798
+ return parsed.isPrivate === true;
3799
+ }
3800
+ function isActionsEnabled(ref, run = execFileSync9) {
3801
+ const out = run(
3802
+ "gh",
3803
+ ["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
3804
+ { stdio: ["ignore", "pipe", "ignore"], timeout: GH_TIMEOUT_MS }
3805
+ ).toString().trim();
3806
+ return out === "true";
3807
+ }
3808
+ function disableActions(ref, run = execFileSync9) {
3809
+ run(
3810
+ "gh",
3811
+ [
3812
+ "api",
3813
+ "-X",
3814
+ "PUT",
3815
+ `repos/${ref.owner}/${ref.repo}/actions/permissions`,
3816
+ "-F",
3817
+ "enabled=false"
3818
+ ],
3819
+ { stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
3820
+ );
3821
+ }
3822
+ function readOriginRemote(cwd, run = execFileSync9) {
3823
+ return run("git", ["remote", "get-url", "origin"], {
3824
+ cwd,
3825
+ stdio: ["ignore", "pipe", "ignore"]
3826
+ }).toString().trim();
3827
+ }
3828
+
3829
+ // src/commands.doctor.actions-drift.ts
3830
+ function reportActionsDrift(section2, run = execFileSync10) {
3831
+ let remote;
3832
+ try {
3833
+ remote = readOriginRemote(repoHome(), run);
3834
+ } catch {
3835
+ return;
4005
3836
  }
4006
- equals(left, right, options) {
4007
- if (options.comparator) {
4008
- return options.comparator(left, right);
4009
- } else {
4010
- return left === right || !!options.ignoreCase && left.toLowerCase() === right.toLowerCase();
4011
- }
3837
+ const ref = parseGitHubRemote(remote);
3838
+ if (ref === null) return;
3839
+ const auth = ghAuthStatus(run);
3840
+ if (auth === "gh-not-installed" || auth === "gh-not-authed") return;
3841
+ let isPrivate;
3842
+ try {
3843
+ isPrivate = isRepoPrivate(ref, run);
3844
+ } catch {
3845
+ return;
4012
3846
  }
4013
- removeEmpty(array) {
4014
- const ret = [];
4015
- for (let i = 0; i < array.length; i++) {
4016
- if (array[i]) {
4017
- ret.push(array[i]);
4018
- }
4019
- }
4020
- return ret;
3847
+ if (!isPrivate) return;
3848
+ let enabled2;
3849
+ try {
3850
+ enabled2 = isActionsEnabled(ref, run);
3851
+ } catch {
3852
+ return;
4021
3853
  }
4022
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
4023
- castInput(value, options) {
4024
- return value;
3854
+ if (!enabled2) return;
3855
+ addItem(
3856
+ section2,
3857
+ `${yellow(warnGlyph)} Actions: enabled on private repo ${ref.owner}/${ref.repo} (re-disable with 'gh api -X PUT repos/${ref.owner}/${ref.repo}/actions/permissions -F enabled=false')`
3858
+ );
3859
+ }
3860
+
3861
+ // src/commands.doctor.verdict.ts
3862
+ init_color();
3863
+ function isFailLine(item2) {
3864
+ return item2.includes(failGlyph);
3865
+ }
3866
+ function isWarnLine(item2) {
3867
+ return !isFailLine(item2) && item2.includes(warnGlyph);
3868
+ }
3869
+ function buildVerdictSection(sections) {
3870
+ const summary = section("Summary");
3871
+ const lines = sections.flatMap((s) => s.items).map((item2) => item2.replace(/^\t/, ""));
3872
+ const failures = lines.filter(isFailLine);
3873
+ const warnings = lines.filter(isWarnLine);
3874
+ for (const line of [...failures, ...warnings]) addItem(summary, line);
3875
+ if (failures.length > 0) {
3876
+ addItem(
3877
+ summary,
3878
+ `${red(failGlyph)} ${failures.length} failure(s), ${warnings.length} warning(s)`
3879
+ );
3880
+ } else if (warnings.length > 0) {
3881
+ addItem(summary, `${yellow(warnGlyph)} ${warnings.length} warning(s)`);
3882
+ } else {
3883
+ addItem(summary, `${green(okGlyph)} healthy`);
4025
3884
  }
4026
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
4027
- tokenize(value, options) {
4028
- return Array.from(value);
3885
+ return summary;
3886
+ }
3887
+
3888
+ // src/commands.doctor.ts
3889
+ function gatherDoctorSections(opts) {
3890
+ const host = section("Environment");
3891
+ reportHostAndPaths(host);
3892
+ reportRepoState(host);
3893
+ const links = section("Shared links");
3894
+ const mapPath = join33(repoHome(), "path-map.json");
3895
+ const rawMap = existsSync27(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
3896
+ const map = rawMap ?? { projects: {} };
3897
+ reportSharedLinks(links, map);
3898
+ const hooksScan = section("Hook targets");
3899
+ reportHooksTargetCheck(hooksScan);
3900
+ reportHookScopeCheck(hooksScan);
3901
+ reportPreserveSymlinksCheck(hooksScan);
3902
+ const settings = section("Settings");
3903
+ const base = loadBaseSettings(settings);
3904
+ const parsedSettings = loadAndReportSettings(settings);
3905
+ reportHostOverrides(settings, base, parsedSettings);
3906
+ reportSettingsDriftCheck(settings);
3907
+ const pathMap = section("Path map");
3908
+ reportPathMap(pathMap);
3909
+ const neverSync = section("Never-sync");
3910
+ reportNeverSync(neverSync);
3911
+ const repository = section("Repository");
3912
+ const gitleaksReady = reportGitleaksProbe(repository);
3913
+ reportGitlinks(repository);
3914
+ reportRemote(repository);
3915
+ reportRebaseClean(repository);
3916
+ reportRebaseState(repository);
3917
+ reportActionsDrift(repository);
3918
+ const nomadVersion = section("Nomad Version");
3919
+ reportVersionCheck(nomadVersion);
3920
+ const housekeeping = section("Housekeeping");
3921
+ reportBackupsCheck(housekeeping);
3922
+ const depVersions = section("Dependency Versions");
3923
+ reportNodeEngineCheck(depVersions);
3924
+ reportGitleaksVersionCheck(depVersions);
3925
+ reportOptionalDeps(depVersions);
3926
+ const sharedScan = section("Shared scan");
3927
+ if (opts.checkShared === true) reportCheckShared(sharedScan, gitleaksReady);
3928
+ const schemaScan = section("Schema scan");
3929
+ if (opts.checkSchema === true) reportCheckSchema(schemaScan);
3930
+ const body = [
3931
+ nomadVersion,
3932
+ depVersions,
3933
+ host,
3934
+ links,
3935
+ hooksScan,
3936
+ settings,
3937
+ pathMap,
3938
+ neverSync,
3939
+ repository,
3940
+ housekeeping,
3941
+ sharedScan,
3942
+ schemaScan
3943
+ ];
3944
+ return [...body, buildVerdictSection(body)];
3945
+ }
3946
+ function cmdDoctor(opts = {}) {
3947
+ const makeSpinner = opts.startSpinner ?? startSpinner;
3948
+ const sp = makeSpinner("Running checks");
3949
+ let report;
3950
+ try {
3951
+ report = gatherDoctorSections(opts);
3952
+ } finally {
3953
+ sp.stop();
4029
3954
  }
4030
- join(chars) {
4031
- return chars.join("");
3955
+ renderDoctor(report);
3956
+ }
3957
+
3958
+ // src/commands.drop-session.ts
3959
+ init_config();
3960
+ import { execFileSync as execFileSync12 } from "node:child_process";
3961
+ import { existsSync as existsSync29, readdirSync as readdirSync10, statSync as statSync8 } from "node:fs";
3962
+ import { join as join35, relative as relative4 } from "node:path";
3963
+
3964
+ // src/commands.drop-session.git.ts
3965
+ import { execFileSync as execFileSync11 } from "node:child_process";
3966
+ function expandStagedDir(dirRel, repo) {
3967
+ try {
3968
+ const out = execFileSync11("git", ["ls-files", "-z", "--", dirRel], {
3969
+ cwd: repo,
3970
+ stdio: ["ignore", "pipe", "pipe"]
3971
+ });
3972
+ return out.toString().split("\0").filter((p) => p !== "");
3973
+ } catch {
3974
+ return [];
4032
3975
  }
4033
- postProcess(changeObjects, options) {
4034
- return changeObjects;
3976
+ }
3977
+ function isTrackedInHead(rel, repo) {
3978
+ try {
3979
+ execFileSync11("git", ["cat-file", "-e", `HEAD:${rel}`], {
3980
+ cwd: repo,
3981
+ stdio: ["ignore", "pipe", "pipe"]
3982
+ });
3983
+ return true;
3984
+ } catch {
3985
+ return false;
4035
3986
  }
4036
- get useLongestToken() {
3987
+ }
3988
+ function isInIndex(rel, repo) {
3989
+ try {
3990
+ const out = execFileSync11("git", ["ls-files", "--", rel], {
3991
+ cwd: repo,
3992
+ stdio: ["ignore", "pipe", "pipe"]
3993
+ });
3994
+ return out.toString().trim() !== "";
3995
+ } catch {
4037
3996
  return false;
4038
3997
  }
4039
- buildValues(lastComponent, newTokens, oldTokens) {
4040
- const components = [];
4041
- let nextComponent;
4042
- while (lastComponent) {
4043
- components.push(lastComponent);
4044
- nextComponent = lastComponent.previousComponent;
4045
- delete lastComponent.previousComponent;
4046
- lastComponent = nextComponent;
4047
- }
4048
- components.reverse();
4049
- const componentLen = components.length;
4050
- let componentPos = 0, newPos = 0, oldPos = 0;
4051
- for (; componentPos < componentLen; componentPos++) {
4052
- const component = components[componentPos];
4053
- if (!component.removed) {
4054
- if (!component.added && this.useLongestToken) {
4055
- let value = newTokens.slice(newPos, newPos + component.count);
4056
- value = value.map(function(value2, i) {
4057
- const oldValue = oldTokens[oldPos + i];
4058
- return oldValue.length > value2.length ? oldValue : value2;
4059
- });
4060
- component.value = this.join(value);
4061
- } else {
4062
- component.value = this.join(newTokens.slice(newPos, newPos + component.count));
4063
- }
4064
- newPos += component.count;
4065
- if (!component.added) {
4066
- oldPos += component.count;
4067
- }
4068
- } else {
4069
- component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count));
4070
- oldPos += component.count;
4071
- }
3998
+ }
3999
+
4000
+ // src/commands.drop-session.scrub-hint.ts
4001
+ init_config();
4002
+ init_utils();
4003
+ init_utils_json();
4004
+ import { existsSync as existsSync28 } from "node:fs";
4005
+ import { join as join34 } from "node:path";
4006
+ var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
4007
+ function reportScrubHint(id, matches) {
4008
+ const live = resolveLiveTranscript2(id, matches);
4009
+ const target = live ?? `~/.claude/projects/<encoded>/${id}.jsonl`;
4010
+ log(
4011
+ `note: this only un-stages the session from the next push.
4012
+ The local source still contains the secret, so nomad push re-stages it
4013
+ on the next run and nomad doctor --check-shared keeps reporting it.
4014
+ To fully remediate: rotate the credential, then run:
4015
+ nomad redact ${id}
4016
+ (or scrub ${target} manually)`
4017
+ );
4018
+ }
4019
+ function resolveLiveTranscript2(id, matches) {
4020
+ try {
4021
+ const mapPath = join34(repoHome(), "path-map.json");
4022
+ if (!existsSync28(mapPath)) return null;
4023
+ const projects = readJson(mapPath).projects;
4024
+ const claude = claudeHome();
4025
+ for (const rel of matches) {
4026
+ const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
4027
+ if (logical === void 0) continue;
4028
+ const abs = projects[logical]?.[HOST];
4029
+ if (abs === void 0) continue;
4030
+ const live = join34(claude, "projects", encodePath(abs), `${id}.jsonl`);
4031
+ if (existsSync28(live)) return live;
4072
4032
  }
4073
- return components;
4033
+ return null;
4034
+ } catch {
4035
+ return null;
4074
4036
  }
4075
- };
4037
+ }
4076
4038
 
4077
- // node_modules/diff/libesm/diff/line.js
4078
- var LineDiff = class extends Diff {
4079
- constructor() {
4080
- super(...arguments);
4081
- this.tokenize = tokenize;
4039
+ // src/commands.drop-session.ts
4040
+ init_utils();
4041
+ function cmdDropSession(id) {
4042
+ if (id.length === 0 || id.length > 128 || !/^[A-Za-z0-9_-]+$/.test(id)) {
4043
+ fail(`invalid session id: ${id}`);
4044
+ process.exit(1);
4082
4045
  }
4083
- equals(left, right, options) {
4084
- if (options.ignoreWhitespace) {
4085
- if (!options.newlineIsToken || !left.includes("\n")) {
4086
- left = left.trim();
4087
- }
4088
- if (!options.newlineIsToken || !right.includes("\n")) {
4089
- right = right.trim();
4090
- }
4091
- } else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
4092
- if (left.endsWith("\n")) {
4093
- left = left.slice(0, -1);
4094
- }
4095
- if (right.endsWith("\n")) {
4096
- right = right.slice(0, -1);
4097
- }
4046
+ const repo = repoHome();
4047
+ if (!existsSync29(repo)) die(`repo not cloned at ${repo}`);
4048
+ const handle = acquireLock("drop-session");
4049
+ if (handle === null) process.exit(0);
4050
+ try {
4051
+ const repoProjects = join35(repo, "shared", "projects");
4052
+ if (!existsSync29(repoProjects)) {
4053
+ throw new NomadFatal(`no staged session matches ${id}`);
4098
4054
  }
4099
- return super.equals(left, right, options);
4055
+ const matches = collectMatches(repoProjects, id, repo);
4056
+ if (matches.length === 0) {
4057
+ throw new NomadFatal(`no staged session matches ${id}`);
4058
+ }
4059
+ for (const rel of matches) unstageOne(rel, repo);
4060
+ reportScrubHint(id, matches);
4061
+ } catch (err) {
4062
+ if (!(err instanceof NomadFatal)) {
4063
+ throw err;
4064
+ }
4065
+ fail(err.message);
4066
+ process.exitCode = 1;
4067
+ } finally {
4068
+ releaseLock(handle);
4100
4069
  }
4101
- };
4102
- var lineDiff = new LineDiff();
4103
- function diffLines(oldStr, newStr, options) {
4104
- return lineDiff.diff(oldStr, newStr, options);
4105
4070
  }
4106
- function tokenize(value, options) {
4107
- if (options.stripTrailingCr) {
4108
- value = value.replace(/\r\n/g, "\n");
4071
+ function collectMatches(repoProjects, id, repo) {
4072
+ const matches = [];
4073
+ for (const logical of readdirSync10(repoProjects)) {
4074
+ const candidate = join35(repoProjects, logical, `${id}.jsonl`);
4075
+ if (existsSync29(candidate)) {
4076
+ matches.push(relative4(repo, candidate));
4077
+ }
4078
+ const dir = join35(repoProjects, logical, id);
4079
+ if (existsSync29(dir) && statSync8(dir).isDirectory()) {
4080
+ const dirRel = relative4(repo, dir);
4081
+ const staged = expandStagedDir(dirRel, repo);
4082
+ if (staged.length > 0) matches.push(...staged);
4083
+ else matches.push(dirRel);
4084
+ }
4109
4085
  }
4110
- const retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
4111
- if (!linesAndNewlines[linesAndNewlines.length - 1]) {
4112
- linesAndNewlines.pop();
4086
+ return matches;
4087
+ }
4088
+ function unstageOne(rel, repo) {
4089
+ if (!isInIndex(rel, repo)) {
4090
+ item(`dropped ${rel} (already absent from index)`);
4091
+ return;
4113
4092
  }
4114
- for (let i = 0; i < linesAndNewlines.length; i++) {
4115
- const line = linesAndNewlines[i];
4116
- if (i % 2 && !options.newlineIsToken) {
4117
- retLines[retLines.length - 1] += line;
4093
+ try {
4094
+ if (isTrackedInHead(rel, repo)) {
4095
+ execFileSync12("git", ["restore", "--staged", "--worktree", "--", rel], {
4096
+ cwd: repo,
4097
+ stdio: ["ignore", "pipe", "pipe"]
4098
+ });
4118
4099
  } else {
4119
- retLines.push(line);
4100
+ execFileSync12("git", ["rm", "--cached", "-f", "--", rel], {
4101
+ cwd: repo,
4102
+ stdio: ["ignore", "pipe", "pipe"]
4103
+ });
4120
4104
  }
4105
+ } catch (err) {
4106
+ const e = err;
4107
+ const detail = e.stderr?.toString().trim() ?? e.message;
4108
+ throw new NomadFatal(`git failed to unstage ${rel}: ${detail}`);
4121
4109
  }
4122
- return retLines;
4110
+ item(`dropped ${rel}`);
4123
4111
  }
4124
4112
 
4125
- // src/diff-lines.ts
4113
+ // src/commands.pull.ts
4114
+ import { existsSync as existsSync35, mkdirSync as mkdirSync8 } from "node:fs";
4115
+ import { join as join41 } from "node:path";
4116
+
4117
+ // src/commands.push.sections.ts
4126
4118
  init_color();
4127
- function diffLinesToUnified(oldStr, newStr) {
4128
- const parts = diffLines(oldStr, newStr);
4129
- const lines = [];
4130
- for (const part of parts) {
4131
- const partLines = part.value.split("\n");
4132
- if (partLines.at(-1) === "") {
4133
- partLines.pop();
4134
- }
4135
- let prefix;
4136
- if (part.removed) prefix = (line) => red(`-${line}`);
4137
- else if (part.added) prefix = (line) => green(`+${line}`);
4138
- else prefix = (line) => ` ${line}`;
4139
- for (const line of partLines) {
4140
- lines.push(prefix(line));
4119
+
4120
+ // src/summary.ts
4121
+ init_utils();
4122
+ function summaryText(verb, unmapped, collisions = 0, extrasSkipped = 0) {
4123
+ const extras = extrasSkipped > 0 ? `, ${extrasSkipped} extras skipped` : "";
4124
+ if (verb === "push") {
4125
+ if (unmapped === 0 && collisions === 0 && extrasSkipped === 0) {
4126
+ return { text: "summary: clean", clean: true };
4141
4127
  }
4128
+ const base = `summary: ${unmapped} unmapped on push, ${collisions} collisions`;
4129
+ return { text: `${base}${extras} (run nomad doctor to list)`, clean: false };
4142
4130
  }
4143
- return lines;
4131
+ if (unmapped === 0 && extrasSkipped === 0) {
4132
+ return { text: "summary: clean", clean: true };
4133
+ }
4134
+ return {
4135
+ text: `summary: ${unmapped} unmapped on ${verb}${extras} (run nomad doctor to list)`,
4136
+ clean: false
4137
+ };
4138
+ }
4139
+ function summaryRow(verb, unmapped, collisions = 0, extrasSkipped = 0) {
4140
+ const { text } = summaryText(verb, unmapped, collisions, extrasSkipped);
4141
+ return text.replace(/^summary: /, "");
4144
4142
  }
4145
4143
 
4146
- // src/preview.ts
4147
- init_utils_json();
4148
- var CANONICAL_ORDER_NOTE = "settings.json will be rewritten in canonical key order; no value changes";
4149
- function diffJsonStrings(currentJsonText, newJsonText) {
4150
- if (currentJsonText === newJsonText) return "";
4151
- const lines = [
4152
- "--- ~/.claude/settings.json",
4153
- "+++ would write",
4154
- ...diffLinesToUnified(currentJsonText, newJsonText)
4144
+ // src/commands.push.sections.ts
4145
+ function collapsedSkipRow(n, noun) {
4146
+ if (n <= 0) return null;
4147
+ return `${dim(infoGlyph)} ${n} ${noun}`;
4148
+ }
4149
+ function buildSettingsSection(label) {
4150
+ const s = section("Settings");
4151
+ addItem(s, `${green(okGlyph)} settings.json (base + ${label})`);
4152
+ return s;
4153
+ }
4154
+ function buildSessionsSection(items, unmapped) {
4155
+ const s = section("Sessions");
4156
+ for (const logical of items) addItem(s, `${green(okGlyph)} ${logical}`);
4157
+ const skip = collapsedSkipRow(unmapped, "not in path-map (run nomad doctor to list)");
4158
+ if (skip !== null) addItem(s, skip);
4159
+ return s;
4160
+ }
4161
+ function buildExtrasSection(items, extrasSkipped) {
4162
+ const s = section("Extras");
4163
+ for (const entry of items) addItem(s, `${green(okGlyph)} ${entry}`);
4164
+ const skip = collapsedSkipRow(extrasSkipped, "extras skipped");
4165
+ if (skip !== null) addItem(s, skip);
4166
+ return s;
4167
+ }
4168
+ function syncedSections(st) {
4169
+ const sessions = st.dryRun ? st.remap.wouldPush : st.remap.pushed;
4170
+ const extras = st.dryRun ? st.extras.wouldPush : st.extras.pushed;
4171
+ return [
4172
+ buildSessionsSection(sessions, st.remap.unmapped),
4173
+ buildExtrasSection(extras, st.extras.skipped)
4155
4174
  ];
4156
- return lines.join("\n");
4157
4175
  }
4158
- function readJsonOrNull(path) {
4159
- if (!existsSync31(path)) return null;
4176
+ function summarySection(st) {
4177
+ const s = section("Summary");
4178
+ const unmapped = st.remap.unmapped + st.extras.unmapped;
4179
+ addItem(s, summaryRow("push", unmapped, st.remap.collisions, st.extras.skipped));
4180
+ return s;
4181
+ }
4182
+ function renderPushTree(st, verdict) {
4183
+ const leakScan = section("Leak scan");
4184
+ addItem(leakScan, verdict.verdictRow);
4185
+ renderTree([...syncedSections(st), leakScan, summarySection(st)]);
4186
+ }
4187
+ function renderNoScanTree(st, opts = {}) {
4188
+ const sections = [];
4189
+ if (opts.noMapHint === true) {
4190
+ const pathMap = section("Path map");
4191
+ addItem(pathMap, `${dim(infoGlyph)} no path-map.json (nothing to preview)`);
4192
+ sections.push(pathMap);
4193
+ }
4194
+ renderTree([...sections, ...syncedSections(st), summarySection(st)]);
4195
+ }
4196
+
4197
+ // src/commands.pull.ts
4198
+ init_config();
4199
+
4200
+ // src/extras-sync.ts
4201
+ init_config();
4202
+ import { existsSync as existsSync32 } from "node:fs";
4203
+ import { join as join38 } from "node:path";
4204
+
4205
+ // src/extras-sync.diff.ts
4206
+ init_utils();
4207
+ import { execFileSync as execFileSync13 } from "node:child_process";
4208
+ function labelDiffLine(line) {
4209
+ const tab = line.indexOf(" ");
4210
+ if (tab === -1) return line;
4211
+ const status = line.slice(0, tab);
4212
+ const path = line.slice(tab + 1);
4213
+ if (status === "D") return `${path} (local only)`;
4214
+ if (status === "A") return `${path} (repo only)`;
4215
+ return path;
4216
+ }
4217
+ function parseDiffOutput(stdout) {
4218
+ return stdout.split("\n").filter((line) => line.length > 0).map(labelDiffLine);
4219
+ }
4220
+ function listDivergingFiles(a, b) {
4160
4221
  try {
4161
- return readJson(path);
4162
- } catch {
4163
- return null;
4222
+ const stdout = execFileSync13("git", ["diff", "--no-index", "--name-status", a, b], {
4223
+ stdio: ["ignore", "pipe", "pipe"]
4224
+ }).toString();
4225
+ return parseDiffOutput(stdout);
4226
+ } catch (err) {
4227
+ const e = err;
4228
+ if (e.status === 1 && e.stdout !== void 0) {
4229
+ return parseDiffOutput(e.stdout.toString());
4230
+ }
4231
+ if (e.code === "ENOENT") {
4232
+ warn(`git not on PATH; divergence check skipped for ${a}`);
4233
+ return [];
4234
+ }
4235
+ warn(`divergence check failed for ${a}: ${e.message ?? String(err)}`);
4236
+ return [];
4164
4237
  }
4165
4238
  }
4166
- function previewSettings(basePath, hostPath, settingsPath) {
4167
- const base = readJsonOrNull(basePath);
4168
- if (base === null) {
4169
- return { diff: "", notes: ["section skipped (base or current missing)"] };
4239
+
4240
+ // src/extras-sync.core.ts
4241
+ init_config();
4242
+ import { cpSync as cpSync6, existsSync as existsSync30, rmSync as rmSync10 } from "node:fs";
4243
+ import { join as join36 } from "node:path";
4244
+
4245
+ // src/extras-sync.guards.ts
4246
+ init_utils();
4247
+ init_config_sharedDirs_guard();
4248
+ import { isAbsolute as isAbsolute2, normalize } from "node:path";
4249
+ function assertSafeLocalRoot(localRoot, logical) {
4250
+ if (!isAbsolute2(localRoot)) {
4251
+ throw new NomadFatal(
4252
+ `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
4253
+ );
4170
4254
  }
4171
- const notes = [];
4172
- const hostOverrides = readJsonOrNull(hostPath);
4173
- if (hostOverrides === null && existsSync31(hostPath)) {
4174
- notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
4255
+ if (localRoot !== normalize(localRoot)) {
4256
+ throw new NomadFatal(
4257
+ `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be already-normalized; no '..' or redundant segments)`
4258
+ );
4175
4259
  }
4176
- const merged = deepMerge(base, hostOverrides ?? {});
4177
- const current = readJsonOrNull(settingsPath);
4178
- if (current === null && existsSync31(settingsPath)) {
4179
- return { diff: "", notes: [...notes, "malformed; skipping diff"] };
4260
+ }
4261
+
4262
+ // src/extras-sync.core.ts
4263
+ init_utils();
4264
+ init_utils_json();
4265
+ function loadValidatedExtras(opts) {
4266
+ const repo = repoHome();
4267
+ const mapPath = join36(repo, "path-map.json");
4268
+ const repoExtras = join36(repo, "shared", "extras");
4269
+ if (!existsSync30(mapPath) || opts.requireRepoExtras === true && !existsSync30(repoExtras)) {
4270
+ if (opts.missingMsg !== void 0) log(opts.missingMsg);
4271
+ return null;
4180
4272
  }
4181
- const rawEqual = JSON.stringify(current ?? {}, null, 2) === JSON.stringify(merged, null, 2);
4182
- const diff = diffJsonStrings(
4183
- JSON.stringify(sortKeysDeep(current ?? {}), null, 2),
4184
- JSON.stringify(sortKeysDeep(merged), null, 2)
4185
- );
4186
- if (diff === "" && !rawEqual) notes.push(CANONICAL_ORDER_NOTE);
4187
- return { diff, notes };
4273
+ const map = readPathMap(mapPath);
4274
+ const extrasMap = map.extras ?? {};
4275
+ if (Object.keys(extrasMap).length === 0) return null;
4276
+ for (const logical of Object.keys(extrasMap)) {
4277
+ assertSafeLogical(logical);
4278
+ const localRoot = map.projects[logical]?.[HOST];
4279
+ if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
4280
+ }
4281
+ return { map, extrasMap };
4188
4282
  }
4189
- function formatLinkRow(e) {
4190
- return `${e.kind} ${e.from} -> ${e.to}`;
4283
+ function* eachExtrasTarget(v, counts) {
4284
+ const whitelist = SUPPORTED_EXTRAS;
4285
+ for (const [logical, dirnames] of Object.entries(v.extrasMap)) {
4286
+ const localRoot = v.map.projects[logical]?.[HOST];
4287
+ if (!localRoot || localRoot === "TBD") {
4288
+ counts.unmapped++;
4289
+ continue;
4290
+ }
4291
+ for (const dirname7 of dirnames) {
4292
+ if (!whitelist.includes(dirname7)) {
4293
+ counts.skipped++;
4294
+ continue;
4295
+ }
4296
+ yield { logical, localRoot, dirname: dirname7 };
4297
+ }
4298
+ }
4191
4299
  }
4192
- function formatSessionRow(e) {
4193
- return e.kind === "overwrite" ? `overwrite ${e.dst} (from ${e.src})` : e.text;
4300
+ function copyExtras(src, dst) {
4301
+ rmSync10(dst, { recursive: true, force: true });
4302
+ cpSync6(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
4194
4303
  }
4195
- function buildSettingsSectionForPreview(result) {
4196
- const s = section("settings.json", true);
4197
- if (result.diff !== "") {
4198
- for (const line of result.diff.split("\n")) {
4199
- addItem(s, line);
4304
+
4305
+ // src/extras-sync.ts
4306
+ init_utils();
4307
+ init_utils_json();
4308
+
4309
+ // src/extras-sync.remap.ts
4310
+ init_config();
4311
+ import { existsSync as existsSync31, mkdirSync as mkdirSync7 } from "node:fs";
4312
+ import { join as join37 } from "node:path";
4313
+ init_utils_fs();
4314
+ function runExtrasOp(v, dryRun, paths, backup) {
4315
+ const counts = { unmapped: 0, skipped: 0 };
4316
+ const done = [];
4317
+ const would = [];
4318
+ for (const t of eachExtrasTarget(v, counts)) {
4319
+ const { src, dst } = paths(t);
4320
+ if (!existsSync31(src)) continue;
4321
+ const item2 = `${t.logical}/${t.dirname}`;
4322
+ if (dryRun) {
4323
+ would.push(item2);
4324
+ continue;
4200
4325
  }
4326
+ backup(dst, t.localRoot);
4327
+ copyExtras(src, dst);
4328
+ done.push(item2);
4201
4329
  }
4202
- for (const note of result.notes) {
4203
- addItem(s, `note: ${note}`);
4204
- }
4205
- return s;
4330
+ return { ...counts, done, would };
4331
+ }
4332
+ function remapExtrasPush(ts, opts = {}) {
4333
+ const dryRun = opts.dryRun === true;
4334
+ const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
4335
+ if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
4336
+ const repo = repoHome();
4337
+ const repoExtras = join37(repo, "shared", "extras");
4338
+ if (!dryRun) mkdirSync7(repoExtras, { recursive: true });
4339
+ const { unmapped, skipped, done, would } = runExtrasOp(
4340
+ v,
4341
+ dryRun,
4342
+ ({ localRoot, logical, dirname: dirname7 }) => ({
4343
+ src: join37(localRoot, dirname7),
4344
+ dst: join37(repoExtras, logical, dirname7)
4345
+ }),
4346
+ (dst) => backupRepoWrite(dst, ts, repo)
4347
+ );
4348
+ return { unmapped, skipped, pushed: done, wouldPush: would };
4206
4349
  }
4207
- function computePreview(ts, map, verb = "pull") {
4208
- const repo = repoHome();
4209
- const claude = claudeHome();
4210
- console.log(`would pull on host=${HOST} (dry-run; no mutation)`);
4211
- console.log("");
4212
- const links = section("Symlinks");
4213
- applySharedLinks(ts, map, {
4214
- dryRun: true,
4215
- onPreview: (e) => addItem(links, formatLinkRow(e))
4350
+ function remapExtrasPull(ts, opts = {}) {
4351
+ const dryRun = opts.dryRun === true;
4352
+ const v = loadValidatedExtras({
4353
+ requireRepoExtras: true,
4354
+ missingMsg: "no path-map or repo extras dir; skipping extras remap"
4216
4355
  });
4217
- const settingsResult = previewSettings(
4218
- join36(repo, "shared", "settings.base.json"),
4219
- join36(repo, "hosts", `${HOST}.json`),
4220
- join36(claude, "settings.json")
4356
+ if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
4357
+ const repoExtras = join37(repoHome(), "shared", "extras");
4358
+ const { unmapped, skipped, done, would } = runExtrasOp(
4359
+ v,
4360
+ dryRun,
4361
+ ({ localRoot, logical, dirname: dirname7 }) => ({
4362
+ src: join37(repoExtras, logical, dirname7),
4363
+ dst: join37(localRoot, dirname7)
4364
+ }),
4365
+ // Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
4366
+ // localRoot so the backup tree mirrors the project layout.
4367
+ (dst, localRoot) => backupExtrasWrite(dst, ts, localRoot)
4221
4368
  );
4222
- const settingsSection = buildSettingsSectionForPreview(settingsResult);
4223
- const sessions = section("Sessions");
4224
- const remapResult = remapPull(ts, {
4225
- dryRun: true,
4226
- onPreview: (e) => addItem(sessions, formatSessionRow(e))
4227
- });
4228
- const summary = section("Summary");
4229
- addItem(summary, summaryRow(verb, remapResult.unmapped));
4230
- renderTree([links, settingsSection, sessions, summary]);
4231
- return { unmapped: remapResult.unmapped, collisions: 0 };
4369
+ return { unmapped, skipped, pulled: done, wouldPull: would };
4232
4370
  }
4233
4371
 
4234
- // src/spinner.ts
4235
- init_color();
4236
- import { existsSync as existsSync33 } from "node:fs";
4237
- import { fileURLToPath as fileURLToPath4 } from "node:url";
4238
- import { Worker } from "node:worker_threads";
4239
-
4240
- // src/commands.push.recovery.ts
4241
- init_config();
4242
- import { readFileSync as readFileSync11, rmSync as rmSync11, writeFileSync as writeFileSync5 } from "node:fs";
4243
- import { join as join39 } from "node:path";
4244
- import { createInterface } from "node:readline/promises";
4372
+ // src/extras-sync.ts
4373
+ function divergenceCheckExtras(ts) {
4374
+ const v = loadValidatedExtras({});
4375
+ if (v === null) return;
4376
+ const counts = { unmapped: 0, skipped: 0 };
4377
+ const backupRoot = join38(backupBase(), ts, "extras");
4378
+ const repo = repoHome();
4379
+ for (const { logical, localRoot, dirname: dirname7 } of eachExtrasTarget(v, counts)) {
4380
+ const local = join38(localRoot, dirname7);
4381
+ const repoEntry = join38(repo, "shared", "extras", logical, dirname7);
4382
+ if (!existsSync32(local) || !existsSync32(repoEntry)) continue;
4383
+ const diff = listDivergingFiles(local, repoEntry);
4384
+ if (diff.length === 0) continue;
4385
+ const projectBackupRoot = join38(backupRoot, encodePath(localRoot));
4386
+ warn(
4387
+ `local ${dirname7} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
4388
+ );
4389
+ for (const f of diff) warn(` ${f}`);
4390
+ }
4391
+ }
4245
4392
 
4246
- // src/commands.push.recovery.redact.ts
4393
+ // src/links.ts
4247
4394
  init_config();
4248
- init_config_sharedDirs_guard();
4249
- import { cpSync as cpSync6, existsSync as existsSync32, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
4250
- import { dirname as dirname6, join as join37, sep as sep3 } from "node:path";
4251
- init_push_gitleaks_scan();
4252
- init_utils_json();
4253
4395
  init_utils();
4254
-
4255
- // src/commands.push.recovery.seams.ts
4256
- init_push_gitleaks();
4257
- function findingKey(f) {
4258
- return `${f.File}:${f.StartLine}:${f.StartColumn}:${f.RuleID}`;
4396
+ init_utils_fs();
4397
+ init_utils_json();
4398
+ import { existsSync as existsSync33, lstatSync as lstatSync8, rmSync as rmSync11 } from "node:fs";
4399
+ import { join as join39 } from "node:path";
4400
+ function emitAutoMove(onPreview, linkPath, ts, name) {
4401
+ if (onPreview) {
4402
+ onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
4403
+ } else {
4404
+ log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
4405
+ }
4259
4406
  }
4260
- var VALID_SID = /^[A-Za-z0-9_-]+$/;
4261
- function sessionIdFromFinding(f) {
4262
- const m = SESSION_PATH.exec(f.File) ?? /^shared\/projects\/[^/]+\/([^/]+)\//.exec(f.File);
4263
- if (m === null) return null;
4264
- const sid = m[1];
4265
- return VALID_SID.test(sid) ? sid : null;
4407
+ function emitCreate(onPreview, from, to) {
4408
+ if (onPreview) {
4409
+ onPreview({ kind: "create", from, to });
4410
+ } else {
4411
+ log(`would create symlink: ${from} -> ${to}`);
4412
+ }
4266
4413
  }
4267
- function parseAction(raw) {
4268
- const t = raw.trim().toLowerCase();
4269
- if (t === "r" || t === "redact") return "redact";
4270
- if (t === "a" || t === "allow") return "allow";
4271
- if (t === "d" || t === "drop") return "drop";
4272
- return "skip";
4414
+ function isAlreadySymlink(linkPath) {
4415
+ return existsSync33(linkPath) && lstatSync8(linkPath).isSymbolicLink();
4273
4416
  }
4274
-
4275
- // src/commands.push.recovery.redact.ts
4276
- function resolveStagedDir(localPath, map, claude, repo) {
4277
- for (const [logical, hostMap] of Object.entries(map.projects)) {
4278
- assertSafeLogical(logical);
4279
- const abs = hostMap[HOST];
4280
- if (abs === void 0) continue;
4281
- if (localPath.startsWith(join37(claude, "projects", encodePath(abs)) + sep3)) {
4282
- return join37(repo, "shared", "projects", logical);
4417
+ function runAutoMovePasses(linkNames, claude, repo, ts, dryRun, onPreview) {
4418
+ for (const name of linkNames) {
4419
+ const linkPath = join39(claude, name);
4420
+ const target = join39(repo, "shared", name);
4421
+ if (!existsSync33(linkPath)) continue;
4422
+ if (lstatSync8(linkPath).isSymbolicLink()) continue;
4423
+ if (!existsSync33(target)) continue;
4424
+ if (dryRun) {
4425
+ emitAutoMove(onPreview, linkPath, ts, name);
4426
+ continue;
4283
4427
  }
4428
+ backupBeforeWrite(linkPath, ts);
4429
+ rmSync11(linkPath, { recursive: true, force: true });
4284
4430
  }
4285
- return null;
4286
4431
  }
4287
- function applyRedact(f, ts, map, nowMs, scan = scanFile) {
4288
- const refuse = (msg) => {
4289
- log(msg);
4290
- return false;
4291
- };
4432
+ function applySharedLinks(ts, map, opts = {}) {
4433
+ const dryRun = opts.dryRun === true;
4292
4434
  const claude = claudeHome();
4293
4435
  const repo = repoHome();
4294
- const sid = sessionIdFromFinding(f);
4295
- if (sid === null) {
4296
- return refuse(
4297
- `could not locate the local transcript for this finding; choose Skip or Drop session.`
4298
- );
4299
- }
4300
- const localPath = resolveLiveTranscript2(sid);
4301
- if (localPath === null) {
4302
- return refuse(
4303
- `could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
4304
- );
4305
- }
4306
- const sessionDir = join37(dirname6(localPath), sid);
4307
- const subtreeFiles = listSubtreeFiles(sessionDir);
4308
- const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
4309
- if (isRecentlyModified(subtreeMtime, nowMs())) {
4310
- return refuse(
4311
- `session ${sid} looks active (modified within the last 5 minutes); refusing to redact, no changes made.
4312
- End the session and choose Redact again, or choose Drop session (holds this session back from the push, local copy kept) or Skip.`
4313
- );
4314
- }
4315
- const stagedProjectDir = resolveStagedDir(localPath, map, claude, repo);
4316
- if (stagedProjectDir === null) {
4317
- return refuse(
4318
- `could not map the local transcript for session ${sid} to a staged copy; choose Drop session or Skip.`
4319
- );
4436
+ const linkNames = allSharedLinks(map);
4437
+ runAutoMovePasses(linkNames, claude, repo, ts, dryRun, opts.onPreview);
4438
+ for (const name of linkNames) {
4439
+ const target = join39(repo, "shared", name);
4440
+ if (!existsSync33(target)) continue;
4441
+ const linkPath = join39(claude, name);
4442
+ if (isAlreadySymlink(linkPath)) continue;
4443
+ if (dryRun) {
4444
+ emitCreate(opts.onPreview, linkPath, target);
4445
+ continue;
4446
+ }
4447
+ ensureSymlink(linkPath, target);
4320
4448
  }
4321
- const mainFindings = scan(localPath);
4322
- if (mainFindings === null) {
4323
- return refuse(`re-scan of the transcript failed; choose Skip or Drop session.`);
4449
+ }
4450
+ function regenerateSettings(ts, opts = {}) {
4451
+ const dryRun = opts.dryRun === true;
4452
+ const repo = repoHome();
4453
+ const claude = claudeHome();
4454
+ const basePath = join39(repo, "shared", "settings.base.json");
4455
+ const hostPath = join39(repo, "hosts", `${HOST}.json`);
4456
+ if (!existsSync33(basePath)) {
4457
+ die("repo not initialized; run 'nomad init' to scaffold");
4324
4458
  }
4325
- const { total: anyTotal } = applySubtreeRedactions(
4326
- localPath,
4327
- mainFindings,
4328
- subtreeFiles,
4329
- void 0,
4330
- ts,
4331
- scan,
4332
- false
4333
- );
4334
- if (anyTotal === 0) {
4335
- return refuse(
4336
- `nothing to redact in the local transcript for session ${sid}; choose Skip or Drop session.`
4337
- );
4459
+ const base = readJson(basePath);
4460
+ const hasOverrides = existsSync33(hostPath);
4461
+ const overrides = hasOverrides ? readJson(hostPath) : {};
4462
+ const merged = deepMerge(base, overrides);
4463
+ const settingsPath = join39(claude, "settings.json");
4464
+ if (!hasOverrides && existsSync33(settingsPath)) {
4465
+ try {
4466
+ const existing = readJson(settingsPath);
4467
+ const baseKeys = new Set(Object.keys(base));
4468
+ const drift = Object.keys(existing).filter((k) => !baseKeys.has(k));
4469
+ if (drift.length > 0) {
4470
+ warn(
4471
+ `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.`
4472
+ );
4473
+ }
4474
+ } catch {
4475
+ warn("existing settings.json is malformed; skipping drift-check and regenerating.");
4476
+ }
4338
4477
  }
4339
- mkdirSync7(stagedProjectDir, { recursive: true });
4340
- cpSync6(localPath, join37(stagedProjectDir, `${sid}.jsonl`), { force: true });
4341
- if (existsSync32(sessionDir)) {
4342
- cpSync6(sessionDir, join37(stagedProjectDir, sid), { force: true, recursive: true });
4478
+ const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
4479
+ if (dryRun) {
4480
+ log(`would write settings.json (base + ${overrideLabel})`);
4481
+ return { label: overrideLabel };
4343
4482
  }
4344
- return true;
4483
+ backupBeforeWrite(settingsPath, ts);
4484
+ writeJsonAtomic(settingsPath, merged);
4485
+ return { label: overrideLabel };
4345
4486
  }
4346
4487
 
4347
- // src/commands.push.recovery.drop.ts
4488
+ // src/preview.ts
4348
4489
  init_config();
4349
- import { rmSync as rmSync10 } from "node:fs";
4350
- import { join as join38 } from "node:path";
4351
- function dropSessionFromStaged(sid, map) {
4352
- const logicals = Object.keys(map.projects);
4353
- if (logicals.length === 0) return false;
4354
- const repo = repoHome();
4355
- for (const logical of logicals) {
4356
- const jsonl = join38(repo, "shared", "projects", logical, `${sid}.jsonl`);
4357
- const dir = join38(repo, "shared", "projects", logical, sid);
4358
- rmSync10(jsonl, { force: true });
4359
- rmSync10(dir, { recursive: true, force: true });
4360
- }
4361
- return true;
4362
- }
4490
+ import { existsSync as existsSync34 } from "node:fs";
4491
+ import { join as join40 } from "node:path";
4363
4492
 
4364
- // src/commands.push.recovery.actions.ts
4365
- init_push_gitleaks_scan();
4366
- init_utils();
4367
- function applyAllow(f, repo) {
4368
- appendGitleaksIgnore(f.Fingerprint, repo);
4369
- }
4370
- function allowAllFindings(findings, repo) {
4371
- for (const f of findings) {
4372
- appendGitleaksIgnore(f.Fingerprint, repo);
4493
+ // node_modules/diff/libesm/diff/base.js
4494
+ var Diff = class {
4495
+ diff(oldStr, newStr, options = {}) {
4496
+ let callback;
4497
+ if (typeof options === "function") {
4498
+ callback = options;
4499
+ options = {};
4500
+ } else if ("callback" in options) {
4501
+ callback = options.callback;
4502
+ }
4503
+ const oldString = this.castInput(oldStr, options);
4504
+ const newString = this.castInput(newStr, options);
4505
+ const oldTokens = this.removeEmpty(this.tokenize(oldString, options));
4506
+ const newTokens = this.removeEmpty(this.tokenize(newString, options));
4507
+ return this.diffWithOptionsObj(oldTokens, newTokens, options, callback);
4373
4508
  }
4374
- }
4375
- function allowFindingsByRule(findings, ruleId, repo) {
4376
- let count = 0;
4377
- for (const f of findings) {
4378
- if (f.RuleID === ruleId) {
4379
- appendGitleaksIgnore(f.Fingerprint, repo);
4380
- count++;
4509
+ diffWithOptionsObj(oldTokens, newTokens, options, callback) {
4510
+ var _a;
4511
+ const done = (value) => {
4512
+ value = this.postProcess(value, options);
4513
+ if (callback) {
4514
+ setTimeout(function() {
4515
+ callback(value);
4516
+ }, 0);
4517
+ return void 0;
4518
+ } else {
4519
+ return value;
4520
+ }
4521
+ };
4522
+ const newLen = newTokens.length, oldLen = oldTokens.length;
4523
+ let editLength = 1;
4524
+ let maxEditLength = newLen + oldLen;
4525
+ if (options.maxEditLength != null) {
4526
+ maxEditLength = Math.min(maxEditLength, options.maxEditLength);
4527
+ }
4528
+ const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity;
4529
+ const abortAfterTimestamp = Date.now() + maxExecutionTime;
4530
+ const bestPath = [{ oldPos: -1, lastComponent: void 0 }];
4531
+ let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
4532
+ if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
4533
+ return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
4534
+ }
4535
+ let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
4536
+ const execEditLength = () => {
4537
+ for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
4538
+ let basePath;
4539
+ const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
4540
+ if (removePath) {
4541
+ bestPath[diagonalPath - 1] = void 0;
4542
+ }
4543
+ let canAdd = false;
4544
+ if (addPath) {
4545
+ const addPathNewPos = addPath.oldPos - diagonalPath;
4546
+ canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
4547
+ }
4548
+ const canRemove = removePath && removePath.oldPos + 1 < oldLen;
4549
+ if (!canAdd && !canRemove) {
4550
+ bestPath[diagonalPath] = void 0;
4551
+ continue;
4552
+ }
4553
+ if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) {
4554
+ basePath = this.addToPath(addPath, true, false, 0, options);
4555
+ } else {
4556
+ basePath = this.addToPath(removePath, false, true, 1, options);
4557
+ }
4558
+ newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
4559
+ if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
4560
+ return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
4561
+ } else {
4562
+ bestPath[diagonalPath] = basePath;
4563
+ if (basePath.oldPos + 1 >= oldLen) {
4564
+ maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
4565
+ }
4566
+ if (newPos + 1 >= newLen) {
4567
+ minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
4568
+ }
4569
+ }
4570
+ }
4571
+ editLength++;
4572
+ };
4573
+ if (callback) {
4574
+ (function exec() {
4575
+ setTimeout(function() {
4576
+ if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
4577
+ return callback(void 0);
4578
+ }
4579
+ if (!execEditLength()) {
4580
+ exec();
4581
+ }
4582
+ }, 0);
4583
+ })();
4584
+ } else {
4585
+ while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
4586
+ const ret = execEditLength();
4587
+ if (ret) {
4588
+ return ret;
4589
+ }
4590
+ }
4381
4591
  }
4382
4592
  }
4383
- return count;
4384
- }
4385
- async function collectActions(findings, prompt) {
4386
- const actions = /* @__PURE__ */ new Map();
4387
- for (const f of findings) {
4388
- const sid = sessionIdFromFinding(f);
4389
- const header = `
4390
- 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";
4391
- actions.set(findingKey(f), parseAction(await prompt(header + "> ")));
4392
- }
4393
- return actions;
4394
- }
4395
- function dispatchOne(f, ctx) {
4396
- const action = ctx.actions.get(findingKey(f)) ?? "skip";
4397
- if (action === "skip") return;
4398
- const sid = sessionIdFromFinding(f);
4399
- if (sid !== null && ctx.droppedSids.has(sid)) return;
4400
- if (action === "allow") {
4401
- applyAllow(f, ctx.repo);
4402
- return;
4593
+ addToPath(path, added, removed, oldPosInc, options) {
4594
+ const last = path.lastComponent;
4595
+ if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
4596
+ return {
4597
+ oldPos: path.oldPos + oldPosInc,
4598
+ lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
4599
+ };
4600
+ } else {
4601
+ return {
4602
+ oldPos: path.oldPos + oldPosInc,
4603
+ lastComponent: { count: 1, added, removed, previousComponent: last }
4604
+ };
4605
+ }
4403
4606
  }
4404
- if (sid === null) return;
4405
- if (action === "drop") {
4406
- ctx.droppedSids.add(sid);
4407
- if (ctx.drop(sid, ctx.map)) {
4408
- log(
4409
- `dropped session ${sid} from this push (local transcript kept; the secret remains in your local copy)`
4410
- );
4607
+ extractCommon(basePath, newTokens, oldTokens, diagonalPath, options) {
4608
+ const newLen = newTokens.length, oldLen = oldTokens.length;
4609
+ let oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
4610
+ while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) {
4611
+ newPos++;
4612
+ oldPos++;
4613
+ commonCount++;
4614
+ if (options.oneChangePerToken) {
4615
+ basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false };
4616
+ }
4411
4617
  }
4412
- return;
4618
+ if (commonCount && !options.oneChangePerToken) {
4619
+ basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false };
4620
+ }
4621
+ basePath.oldPos = oldPos;
4622
+ return newPos;
4413
4623
  }
4414
- if (action === "redact" && !ctx.redactedSids.has(sid)) {
4415
- if (applyRedact(f, ctx.ts, ctx.map, ctx.nowMs, ctx.scan)) ctx.redactedSids.add(sid);
4624
+ equals(left, right, options) {
4625
+ if (options.comparator) {
4626
+ return options.comparator(left, right);
4627
+ } else {
4628
+ return left === right || !!options.ignoreCase && left.toLowerCase() === right.toLowerCase();
4629
+ }
4416
4630
  }
4417
- }
4418
- function dispatchActions(findings, actions, opts) {
4419
- const { ts, map, nowMs, repo, scan = scanFile, drop = dropSessionFromStaged } = opts;
4420
- const ctx = {
4421
- actions,
4422
- ts,
4423
- map,
4424
- nowMs,
4425
- repo,
4426
- scan,
4427
- drop,
4428
- redactedSids: /* @__PURE__ */ new Set(),
4429
- droppedSids: /* @__PURE__ */ new Set()
4430
- };
4431
- for (const f of findings) {
4432
- dispatchOne(f, ctx);
4631
+ removeEmpty(array) {
4632
+ const ret = [];
4633
+ for (let i = 0; i < array.length; i++) {
4634
+ if (array[i]) {
4635
+ ret.push(array[i]);
4636
+ }
4637
+ }
4638
+ return ret;
4433
4639
  }
4434
- }
4435
- function redactAllFindings(findings, ts, map, nowMs, scan = scanFile) {
4436
- const redactedSids = /* @__PURE__ */ new Set();
4437
- for (const f of findings) {
4438
- const sid = sessionIdFromFinding(f);
4439
- if (sid === null || redactedSids.has(sid)) continue;
4440
- if (applyRedact(f, ts, map, nowMs, scan)) redactedSids.add(sid);
4640
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4641
+ castInput(value, options) {
4642
+ return value;
4441
4643
  }
4442
- }
4443
-
4444
- // src/commands.push.recovery.ts
4445
- init_push_gitleaks_scan();
4446
- init_push_gitleaks();
4447
- init_utils();
4448
- function isTTY(stdin = process.stdin, stdout = process.stdout) {
4449
- return stdin.isTTY === true && stdout.isTTY === true;
4450
- }
4451
- function hasUnresolved(actions) {
4452
- for (const action of actions.values()) {
4453
- if (action === "skip") return true;
4644
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4645
+ tokenize(value, options) {
4646
+ return Array.from(value);
4454
4647
  }
4455
- return false;
4456
- }
4457
- function printRecoveryLegend(print = console.log) {
4458
- print("");
4459
- print("Recovery actions:");
4460
- print(" Redact - scrub the secret from the local transcript, push the cleaned copy");
4461
- print(" Allow - mark as false positive (adds a .gitleaksignore fingerprint), push as-is");
4462
- print(" Drop session - exclude this session from this push (local transcript kept, running");
4463
- print(" session is not stopped)");
4464
- print(" Skip - leave unresolved (the push aborts)");
4465
- print("");
4466
- }
4467
- function applyThenRescan(scanVerdict, repoHome2) {
4468
- gitOrFatal(["add", "-A"], "git add", repoHome2);
4469
- const next = scanVerdict(repoHome2);
4470
- if (next.leak) {
4471
- const { bySession, other } = partitionFindings(next.findings);
4472
- throw new NomadFatal(buildSessionAwareFatal(bySession, other));
4648
+ join(chars) {
4649
+ return chars.join("");
4473
4650
  }
4474
- return next;
4475
- }
4476
- function allowThenRescan(append, scanVerdict, repoHome2) {
4477
- const ignPath = join39(repoHome2, ".gitleaksignore");
4478
- let before;
4479
- try {
4480
- before = readFileSync11(ignPath, "utf8");
4481
- } catch {
4482
- before = null;
4651
+ postProcess(changeObjects, options) {
4652
+ return changeObjects;
4483
4653
  }
4484
- append();
4485
- try {
4486
- return applyThenRescan(scanVerdict, repoHome2);
4487
- } catch (err) {
4488
- if (before === null) rmSync11(ignPath, { force: true });
4489
- else writeFileSync5(ignPath, before, "utf8");
4490
- throw err;
4654
+ get useLongestToken() {
4655
+ return false;
4491
4656
  }
4492
- }
4493
- function makeRealPrompt() {
4494
- return async (prompt) => {
4495
- const rl = createInterface({
4496
- input: process.stdin,
4497
- output: process.stdout,
4498
- terminal: true
4499
- });
4500
- try {
4501
- return await rl.question(prompt);
4502
- } finally {
4503
- rl.close();
4657
+ buildValues(lastComponent, newTokens, oldTokens) {
4658
+ const components = [];
4659
+ let nextComponent;
4660
+ while (lastComponent) {
4661
+ components.push(lastComponent);
4662
+ nextComponent = lastComponent.previousComponent;
4663
+ delete lastComponent.previousComponent;
4664
+ lastComponent = nextComponent;
4504
4665
  }
4505
- };
4506
- }
4507
- async function resolveLeakFindings(verdict, ts, map, deps = {}) {
4508
- const {
4509
- isTTYCheck = isTTY,
4510
- nowMs = Date.now,
4511
- redactAll = false,
4512
- allowAll = false,
4513
- allowRule,
4514
- makePrompt: makePromptFn = makeRealPrompt,
4515
- scan = scanFile,
4516
- printLegend = printRecoveryLegend
4517
- } = deps;
4518
- const scanVerdict = deps.scanVerdict ?? (await Promise.resolve().then(() => (init_push_leak_verdict(), push_leak_verdict_exports))).scanPushVerdict;
4519
- const repo = repoHome();
4520
- let current = verdict;
4521
- if (redactAll) {
4522
- redactAllFindings(current.findings, ts, map, nowMs, scan);
4523
- return applyThenRescan(scanVerdict, repo);
4666
+ components.reverse();
4667
+ const componentLen = components.length;
4668
+ let componentPos = 0, newPos = 0, oldPos = 0;
4669
+ for (; componentPos < componentLen; componentPos++) {
4670
+ const component = components[componentPos];
4671
+ if (!component.removed) {
4672
+ if (!component.added && this.useLongestToken) {
4673
+ let value = newTokens.slice(newPos, newPos + component.count);
4674
+ value = value.map(function(value2, i) {
4675
+ const oldValue = oldTokens[oldPos + i];
4676
+ return oldValue.length > value2.length ? oldValue : value2;
4677
+ });
4678
+ component.value = this.join(value);
4679
+ } else {
4680
+ component.value = this.join(newTokens.slice(newPos, newPos + component.count));
4681
+ }
4682
+ newPos += component.count;
4683
+ if (!component.added) {
4684
+ oldPos += component.count;
4685
+ }
4686
+ } else {
4687
+ component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count));
4688
+ oldPos += component.count;
4689
+ }
4690
+ }
4691
+ return components;
4524
4692
  }
4525
- if (allowAll) {
4526
- return allowThenRescan(() => allowAllFindings(current.findings, repo), scanVerdict, repo);
4693
+ };
4694
+
4695
+ // node_modules/diff/libesm/diff/line.js
4696
+ var LineDiff = class extends Diff {
4697
+ constructor() {
4698
+ super(...arguments);
4699
+ this.tokenize = tokenize;
4527
4700
  }
4528
- if (allowRule !== void 0) {
4529
- return allowThenRescan(
4530
- () => {
4531
- const matched = allowFindingsByRule(current.findings, allowRule, repo);
4532
- if (matched === 0) log(`no findings matched rule ${allowRule}; re-scanning`);
4533
- },
4534
- scanVerdict,
4535
- repo
4536
- );
4701
+ equals(left, right, options) {
4702
+ if (options.ignoreWhitespace) {
4703
+ if (!options.newlineIsToken || !left.includes("\n")) {
4704
+ left = left.trim();
4705
+ }
4706
+ if (!options.newlineIsToken || !right.includes("\n")) {
4707
+ right = right.trim();
4708
+ }
4709
+ } else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
4710
+ if (left.endsWith("\n")) {
4711
+ left = left.slice(0, -1);
4712
+ }
4713
+ if (right.endsWith("\n")) {
4714
+ right = right.slice(0, -1);
4715
+ }
4716
+ }
4717
+ return super.equals(left, right, options);
4537
4718
  }
4538
- if (!isTTYCheck()) {
4539
- throw new NomadFatal(current.recovery ?? "gitleaks detected secrets");
4719
+ };
4720
+ var lineDiff = new LineDiff();
4721
+ function diffLines(oldStr, newStr, options) {
4722
+ return lineDiff.diff(oldStr, newStr, options);
4723
+ }
4724
+ function tokenize(value, options) {
4725
+ if (options.stripTrailingCr) {
4726
+ value = value.replace(/\r\n/g, "\n");
4540
4727
  }
4541
- const prompt = makePromptFn();
4542
- printLegend();
4543
- while (current.leak && current.findings.length > 0) {
4544
- const actions = await collectActions(current.findings, prompt);
4545
- if (hasUnresolved(actions)) {
4546
- const unresolved = current.findings.filter((f) => actions.get(findingKey(f)) === "skip");
4547
- const { bySession, other } = partitionFindings(unresolved);
4548
- throw new NomadFatal(buildSessionAwareFatal(bySession, other));
4728
+ const retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
4729
+ if (!linesAndNewlines[linesAndNewlines.length - 1]) {
4730
+ linesAndNewlines.pop();
4731
+ }
4732
+ for (let i = 0; i < linesAndNewlines.length; i++) {
4733
+ const line = linesAndNewlines[i];
4734
+ if (i % 2 && !options.newlineIsToken) {
4735
+ retLines[retLines.length - 1] += line;
4736
+ } else {
4737
+ retLines.push(line);
4549
4738
  }
4550
- dispatchActions(current.findings, actions, { ts, map, nowMs, repo, scan });
4551
- gitOrFatal(["add", "-A"], "git add", repo);
4552
- current = scanVerdict(repo);
4553
4739
  }
4554
- return current;
4740
+ return retLines;
4555
4741
  }
4556
4742
 
4557
- // src/spinner.ts
4558
- function formatElapsed(ms) {
4559
- return `${(ms / 1e3).toFixed(1)}s`;
4743
+ // src/diff-lines.ts
4744
+ init_color();
4745
+ function diffLinesToUnified(oldStr, newStr) {
4746
+ const parts = diffLines(oldStr, newStr);
4747
+ const lines = [];
4748
+ for (const part of parts) {
4749
+ const partLines = part.value.split("\n");
4750
+ if (partLines.at(-1) === "") {
4751
+ partLines.pop();
4752
+ }
4753
+ let prefix;
4754
+ if (part.removed) prefix = (line) => red(`-${line}`);
4755
+ else if (part.added) prefix = (line) => green(`+${line}`);
4756
+ else prefix = (line) => ` ${line}`;
4757
+ for (const line of partLines) {
4758
+ lines.push(prefix(line));
4759
+ }
4760
+ }
4761
+ return lines;
4560
4762
  }
4561
- function writePlainStart(out, label) {
4562
- out.write(`${label}...
4563
- `);
4763
+
4764
+ // src/preview.ts
4765
+ init_utils_json();
4766
+ var CANONICAL_ORDER_NOTE = "settings.json will be rewritten in canonical key order; no value changes";
4767
+ function diffJsonStrings(currentJsonText, newJsonText) {
4768
+ if (currentJsonText === newJsonText) return "";
4769
+ const lines = [
4770
+ "--- ~/.claude/settings.json",
4771
+ "+++ would write",
4772
+ ...diffLinesToUnified(currentJsonText, newJsonText)
4773
+ ];
4774
+ return lines.join("\n");
4564
4775
  }
4565
- function writePlainDone(out, label, ms) {
4566
- out.write(`${label} done (${formatElapsed(ms)})
4567
- `);
4776
+ function readJsonOrNull(path) {
4777
+ if (!existsSync34(path)) return null;
4778
+ try {
4779
+ return readJson(path);
4780
+ } catch {
4781
+ return null;
4782
+ }
4568
4783
  }
4569
- function writeAnimatedDone(out, label, ms, useTTY) {
4570
- out.write("\r\x1B[K");
4571
- const glyph = useTTY ? green(okGlyph) : okGlyph;
4572
- out.write(`${glyph} ${label} (${formatElapsed(ms)})
4573
- `);
4784
+ function previewSettings(basePath, hostPath, settingsPath) {
4785
+ const base = readJsonOrNull(basePath);
4786
+ if (base === null) {
4787
+ return { diff: "", notes: ["section skipped (base or current missing)"] };
4788
+ }
4789
+ const notes = [];
4790
+ const hostOverrides = readJsonOrNull(hostPath);
4791
+ if (hostOverrides === null && existsSync34(hostPath)) {
4792
+ notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
4793
+ }
4794
+ const merged = deepMerge(base, hostOverrides ?? {});
4795
+ const current = readJsonOrNull(settingsPath);
4796
+ if (current === null && existsSync34(settingsPath)) {
4797
+ return { diff: "", notes: [...notes, "malformed; skipping diff"] };
4798
+ }
4799
+ const rawEqual = JSON.stringify(current ?? {}, null, 2) === JSON.stringify(merged, null, 2);
4800
+ const diff = diffJsonStrings(
4801
+ JSON.stringify(sortKeysDeep(current ?? {}), null, 2),
4802
+ JSON.stringify(sortKeysDeep(merged), null, 2)
4803
+ );
4804
+ if (diff === "" && !rawEqual) notes.push(CANONICAL_ORDER_NOTE);
4805
+ return { diff, notes };
4574
4806
  }
4575
- function resolveWorkerPath(deps = {}) {
4576
- const check = deps.existsSyncFn ?? existsSync33;
4577
- const base = deps.baseUrl ?? import.meta.url;
4578
- const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
4579
- if (check(mjs)) return mjs;
4580
- return fileURLToPath4(new URL("./spinner.worker.ts", base));
4807
+ function formatLinkRow(e) {
4808
+ return `${e.kind} ${e.from} -> ${e.to}`;
4581
4809
  }
4582
- function makeRealWorker() {
4583
- return new Worker(resolveWorkerPath());
4810
+ function formatSessionRow(e) {
4811
+ return e.kind === "overwrite" ? `overwrite ${e.dst} (from ${e.src})` : e.text;
4584
4812
  }
4585
- function startSpinner(label, deps = {}) {
4586
- const ttyCheck = deps.isTTYCheck ?? (() => isTTY());
4587
- const env = deps.env ?? process.env;
4588
- const out = deps.out ?? process.stderr;
4589
- const now = deps.now ?? Date.now;
4590
- const startMs = now();
4591
- const animate = ttyCheck() && !env.CI;
4592
- let worker = null;
4593
- let degraded = false;
4594
- let finalized = false;
4595
- if (animate) {
4596
- const factory = deps.makeWorker ?? makeRealWorker;
4597
- try {
4598
- worker = factory();
4599
- worker.unref?.();
4600
- worker.postMessage({ type: "start", label });
4601
- } catch {
4602
- degraded = true;
4603
- worker = null;
4604
- writePlainStart(out, label);
4813
+ function buildSettingsSectionForPreview(result) {
4814
+ const s = section("settings.json", true);
4815
+ if (result.diff !== "") {
4816
+ for (const line of result.diff.split("\n")) {
4817
+ addItem(s, line);
4605
4818
  }
4606
- } else {
4607
- writePlainStart(out, label);
4608
4819
  }
4609
- function finalize(success, doneLabel) {
4610
- if (finalized) return;
4611
- finalized = true;
4612
- const dl = doneLabel ?? label;
4613
- const elapsed = now() - startMs;
4614
- if (animate && !degraded && worker !== null) {
4615
- worker.postMessage({ type: "pause" });
4616
- worker.terminate();
4617
- worker = null;
4618
- if (success) writeAnimatedDone(out, dl, elapsed, ttyCheck());
4619
- else out.write("\r\x1B[K");
4620
- } else if (success) {
4621
- writePlainDone(out, dl, elapsed);
4622
- }
4820
+ for (const note of result.notes) {
4821
+ addItem(s, `note: ${note}`);
4623
4822
  }
4624
- return {
4625
- succeed: (doneLabel) => finalize(true, doneLabel),
4626
- stop: () => finalize(false)
4627
- };
4823
+ return s;
4628
4824
  }
4629
- function withSpinner(label, fn, deps) {
4630
- const sp = startSpinner(label, deps);
4631
- try {
4632
- const result = fn();
4633
- sp.succeed();
4634
- return result;
4635
- } finally {
4636
- sp.stop();
4637
- }
4825
+ function computePreview(ts, map, verb = "pull") {
4826
+ const repo = repoHome();
4827
+ const claude = claudeHome();
4828
+ console.log(`would pull on host=${HOST} (dry-run; no mutation)`);
4829
+ console.log("");
4830
+ const links = section("Symlinks");
4831
+ applySharedLinks(ts, map, {
4832
+ dryRun: true,
4833
+ onPreview: (e) => addItem(links, formatLinkRow(e))
4834
+ });
4835
+ const settingsResult = previewSettings(
4836
+ join40(repo, "shared", "settings.base.json"),
4837
+ join40(repo, "hosts", `${HOST}.json`),
4838
+ join40(claude, "settings.json")
4839
+ );
4840
+ const settingsSection = buildSettingsSectionForPreview(settingsResult);
4841
+ const sessions = section("Sessions");
4842
+ const remapResult = remapPull(ts, {
4843
+ dryRun: true,
4844
+ onPreview: (e) => addItem(sessions, formatSessionRow(e))
4845
+ });
4846
+ const summary = section("Summary");
4847
+ addItem(summary, summaryRow(verb, remapResult.unmapped));
4848
+ renderTree([links, settingsSection, sessions, summary]);
4849
+ return { unmapped: remapResult.unmapped, collisions: 0 };
4638
4850
  }
4639
4851
 
4640
4852
  // src/commands.pull.recovery.ts
@@ -4781,8 +4993,8 @@ function cmdPull(opts = {}) {
4781
4993
  const forceRemote = opts.forceRemote === true;
4782
4994
  const repo = repoHome();
4783
4995
  const backup = backupBase();
4784
- if (!existsSync34(repo)) die(`repo not cloned at ${repo}`);
4785
- if (!existsSync34(join40(repo, "shared", "settings.base.json"))) {
4996
+ if (!existsSync35(repo)) die(`repo not cloned at ${repo}`);
4997
+ if (!existsSync35(join41(repo, "shared", "settings.base.json"))) {
4786
4998
  die("repo not initialized; run 'nomad init' to scaffold");
4787
4999
  }
4788
5000
  const handle = acquireLock("pull");
@@ -4791,7 +5003,7 @@ function cmdPull(opts = {}) {
4791
5003
  const ts = freshBackupTs(backup);
4792
5004
  handleWedge(repo, forceRemote);
4793
5005
  if (!dryRun) {
4794
- const backupRoot = join40(backup, ts);
5006
+ const backupRoot = join41(backup, ts);
4795
5007
  try {
4796
5008
  mkdirSync8(backupRoot, { recursive: true });
4797
5009
  } catch (err) {
@@ -4802,8 +5014,8 @@ function cmdPull(opts = {}) {
4802
5014
  dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
4803
5015
  );
4804
5016
  gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
4805
- const mapPath = join40(repo, "path-map.json");
4806
- const map = existsSync34(mapPath) ? readPathMap(mapPath) : { projects: {} };
5017
+ const mapPath = join41(repo, "path-map.json");
5018
+ const map = existsSync35(mapPath) ? readPathMap(mapPath) : { projects: {} };
4807
5019
  divergenceCheckExtras(ts);
4808
5020
  if (dryRun) {
4809
5021
  computePreview(ts, map, "pull");
@@ -4825,8 +5037,8 @@ function cmdPull(opts = {}) {
4825
5037
 
4826
5038
  // src/commands.push.ts
4827
5039
  init_config();
4828
- import { existsSync as existsSync36 } from "node:fs";
4829
- import { join as join42, relative as relative5 } from "node:path";
5040
+ import { existsSync as existsSync37 } from "node:fs";
5041
+ import { join as join43, relative as relative5 } from "node:path";
4830
5042
 
4831
5043
  // src/commands.push.allowlist.ts
4832
5044
  init_config();
@@ -4906,9 +5118,9 @@ init_color();
4906
5118
  init_config();
4907
5119
  init_config_sharedDirs_guard();
4908
5120
  import { randomBytes as randomBytes2 } from "node:crypto";
4909
- import { copyFileSync, existsSync as existsSync35, mkdirSync as mkdirSync9, readdirSync as readdirSync11, rmSync as rmSync12 } from "node:fs";
5121
+ import { copyFileSync, existsSync as existsSync36, mkdirSync as mkdirSync9, readdirSync as readdirSync11, rmSync as rmSync12 } from "node:fs";
4910
5122
  import { homedir as homedir5 } from "node:os";
4911
- import { join as join41 } from "node:path";
5123
+ import { join as join42 } from "node:path";
4912
5124
  init_push_leak_verdict();
4913
5125
  init_push_gitleaks();
4914
5126
  init_utils_fs();
@@ -4923,13 +5135,13 @@ function stageSessions(tmpRoot, map) {
4923
5135
  if (!p || p === "TBD") continue;
4924
5136
  reverse.set(encodePath(p), logical);
4925
5137
  }
4926
- const localProjects = join41(claudeHome(), "projects");
4927
- if (!existsSync35(localProjects)) return 0;
5138
+ const localProjects = join42(claudeHome(), "projects");
5139
+ if (!existsSync36(localProjects)) return 0;
4928
5140
  let staged = 0;
4929
5141
  for (const dir of readdirSync11(localProjects)) {
4930
5142
  const logical = reverse.get(dir);
4931
5143
  if (!logical) continue;
4932
- copyDirJsonlOnly(join41(localProjects, dir), join41(tmpRoot, "shared", "projects", logical));
5144
+ copyDirJsonlOnly(join42(localProjects, dir), join42(tmpRoot, "shared", "projects", logical));
4933
5145
  staged++;
4934
5146
  }
4935
5147
  return staged;
@@ -4945,9 +5157,9 @@ function stageExtras(tmpRoot, map) {
4945
5157
  if (!localRoot || localRoot === "TBD") continue;
4946
5158
  for (const dirname7 of dirnames) {
4947
5159
  if (!whitelist.includes(dirname7)) continue;
4948
- const src = join41(localRoot, dirname7);
4949
- if (!existsSync35(src)) continue;
4950
- const dst = join41(tmpRoot, "shared", "extras", logical, dirname7);
5160
+ const src = join42(localRoot, dirname7);
5161
+ if (!existsSync36(src)) continue;
5162
+ const dst = join42(tmpRoot, "shared", "extras", logical, dirname7);
4951
5163
  copyExtras(src, dst);
4952
5164
  staged++;
4953
5165
  }
@@ -4955,19 +5167,19 @@ function stageExtras(tmpRoot, map) {
4955
5167
  return staged;
4956
5168
  }
4957
5169
  function previewPushLeaks(map) {
4958
- const cacheDir = join41(homedir5(), ".cache", "claude-nomad");
5170
+ const cacheDir = join42(homedir5(), ".cache", "claude-nomad");
4959
5171
  mkdirSync9(cacheDir, { recursive: true });
4960
5172
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
4961
- const tmpRoot = join41(cacheDir, `push-preview-tree-${stamp}`);
5173
+ const tmpRoot = join42(cacheDir, `push-preview-tree-${stamp}`);
4962
5174
  try {
4963
5175
  const sessionCount = stageSessions(tmpRoot, map);
4964
5176
  const extrasCount = stageExtras(tmpRoot, map);
4965
5177
  if (sessionCount + extrasCount === 0) {
4966
5178
  return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
4967
5179
  }
4968
- const ignoreFile = join41(repoHome(), ".gitleaksignore");
4969
- if (existsSync35(ignoreFile)) {
4970
- copyFileSync(ignoreFile, join41(tmpRoot, ".gitleaksignore"));
5180
+ const ignoreFile = join42(repoHome(), ".gitleaksignore");
5181
+ if (existsSync36(ignoreFile)) {
5182
+ copyFileSync(ignoreFile, join42(tmpRoot, ".gitleaksignore"));
4971
5183
  }
4972
5184
  let findings;
4973
5185
  try {
@@ -4989,7 +5201,7 @@ init_utils();
4989
5201
  init_utils_fs();
4990
5202
  init_utils_json();
4991
5203
  function guardGitlinks(repo) {
4992
- const gitlinks = findGitlinks(join42(repo, "shared"));
5204
+ const gitlinks = findGitlinks(join43(repo, "shared"));
4993
5205
  if (gitlinks.length === 0) return;
4994
5206
  for (const p of gitlinks) {
4995
5207
  const rel = relative5(repo, p);
@@ -5043,7 +5255,7 @@ async function cmdPush(opts = {}) {
5043
5255
  guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
5044
5256
  const repo = repoHome();
5045
5257
  const backup = backupBase();
5046
- if (!existsSync36(repo)) die(`repo not cloned at ${repo}`);
5258
+ if (!existsSync37(repo)) die(`repo not cloned at ${repo}`);
5047
5259
  const handle = acquireLock("push");
5048
5260
  if (handle === null) process.exit(0);
5049
5261
  try {
@@ -5061,8 +5273,8 @@ async function cmdPush(opts = {}) {
5061
5273
  renderNoScanTree(st);
5062
5274
  return;
5063
5275
  }
5064
- const mapPath = join42(repo, "path-map.json");
5065
- if (!existsSync36(mapPath)) {
5276
+ const mapPath = join43(repo, "path-map.json");
5277
+ if (!existsSync37(mapPath)) {
5066
5278
  if (dryRun) return runDryRunPreview(st, null);
5067
5279
  die("path-map.json missing, cannot enforce push allow-list");
5068
5280
  }
@@ -5102,18 +5314,18 @@ init_config();
5102
5314
 
5103
5315
  // src/diff.ts
5104
5316
  init_config();
5105
- import { existsSync as existsSync37 } from "node:fs";
5106
- import { join as join43 } from "node:path";
5317
+ import { existsSync as existsSync38 } from "node:fs";
5318
+ import { join as join44 } from "node:path";
5107
5319
  init_utils();
5108
5320
  init_utils_fs();
5109
5321
  init_utils_json();
5110
5322
  function cmdDiff() {
5111
5323
  try {
5112
5324
  const repo = repoHome();
5113
- if (!existsSync37(repo)) die(`repo not cloned at ${repo}`);
5325
+ if (!existsSync38(repo)) die(`repo not cloned at ${repo}`);
5114
5326
  const ts = freshBackupTs(backupBase());
5115
- const mapPath = join43(repo, "path-map.json");
5116
- const map = existsSync37(mapPath) ? readPathMap(mapPath) : { projects: {} };
5327
+ const mapPath = join44(repo, "path-map.json");
5328
+ const map = existsSync38(mapPath) ? readPathMap(mapPath) : { projects: {} };
5117
5329
  computePreview(ts, map, "diff");
5118
5330
  } catch (err) {
5119
5331
  if (err instanceof NomadFatal) {
@@ -5127,8 +5339,8 @@ function cmdDiff() {
5127
5339
 
5128
5340
  // src/init.ts
5129
5341
  init_config();
5130
- import { existsSync as existsSync39, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
5131
- import { join as join45 } from "node:path";
5342
+ import { existsSync as existsSync40, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
5343
+ import { join as join46 } from "node:path";
5132
5344
 
5133
5345
  // src/init.gh-onboard.ts
5134
5346
  init_config();
@@ -5210,33 +5422,33 @@ init_config();
5210
5422
  init_utils();
5211
5423
  init_utils_fs();
5212
5424
  init_utils_json();
5213
- import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync38, rmSync as rmSync13, statSync as statSync9 } from "node:fs";
5214
- import { join as join44 } from "node:path";
5425
+ import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync39, rmSync as rmSync13, statSync as statSync9 } from "node:fs";
5426
+ import { join as join45 } from "node:path";
5215
5427
  function snapshotIntoShared(map) {
5216
5428
  const repo = repoHome();
5217
5429
  const claude = claudeHome();
5218
5430
  for (const name of allSharedLinks(map)) {
5219
- const src = join44(claude, name);
5220
- if (!existsSync38(src)) continue;
5221
- const dst = join44(repo, "shared", name);
5431
+ const src = join45(claude, name);
5432
+ if (!existsSync39(src)) continue;
5433
+ const dst = join45(repo, "shared", name);
5222
5434
  if (statSync9(src).isDirectory()) {
5223
- const gk = join44(dst, ".gitkeep");
5224
- if (existsSync38(gk)) rmSync13(gk);
5435
+ const gk = join45(dst, ".gitkeep");
5436
+ if (existsSync39(gk)) rmSync13(gk);
5225
5437
  cpSync7(src, dst, { recursive: true, force: false, errorOnExist: true });
5226
5438
  } else {
5227
5439
  copyFileSync2(src, dst);
5228
5440
  }
5229
5441
  log(`snapshotted shared/${name} from ${src}`);
5230
5442
  }
5231
- const userSettings = join44(claude, "settings.json");
5232
- if (existsSync38(userSettings)) {
5443
+ const userSettings = join45(claude, "settings.json");
5444
+ if (existsSync39(userSettings)) {
5233
5445
  let parsed;
5234
5446
  try {
5235
5447
  parsed = readJson(userSettings);
5236
5448
  } catch (err) {
5237
5449
  return die(`malformed ${userSettings}: ${err.message}`);
5238
5450
  }
5239
- const hostFile = join44(repo, "hosts", `${HOST}.json`);
5451
+ const hostFile = join45(repo, "hosts", `${HOST}.json`);
5240
5452
  writeJsonAtomic(hostFile, parsed);
5241
5453
  log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
5242
5454
  }
@@ -5249,14 +5461,14 @@ var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.cl
5249
5461
  var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
5250
5462
  function preflightConflict(repoHome2) {
5251
5463
  const candidates = [
5252
- join45(repoHome2, "shared", "settings.base.json"),
5253
- join45(repoHome2, "shared", "CLAUDE.md"),
5254
- join45(repoHome2, "path-map.json"),
5255
- join45(repoHome2, "hosts"),
5256
- join45(repoHome2, "shared")
5464
+ join46(repoHome2, "shared", "settings.base.json"),
5465
+ join46(repoHome2, "shared", "CLAUDE.md"),
5466
+ join46(repoHome2, "path-map.json"),
5467
+ join46(repoHome2, "hosts"),
5468
+ join46(repoHome2, "shared")
5257
5469
  ];
5258
5470
  for (const c of candidates) {
5259
- if (existsSync39(c)) return c;
5471
+ if (existsSync40(c)) return c;
5260
5472
  }
5261
5473
  return null;
5262
5474
  }
@@ -5271,25 +5483,25 @@ function cmdInit(opts = {}) {
5271
5483
  die(`already initialized; refusing to clobber ${conflict}`);
5272
5484
  }
5273
5485
  ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
5274
- mkdirSync10(join45(repo, "shared"), { recursive: true });
5275
- mkdirSync10(join45(repo, "hosts"), { recursive: true });
5486
+ mkdirSync10(join46(repo, "shared"), { recursive: true });
5487
+ mkdirSync10(join46(repo, "hosts"), { recursive: true });
5276
5488
  for (const name of SHARED_KEEP_DIRS) {
5277
- mkdirSync10(join45(repo, "shared", name), { recursive: true });
5489
+ mkdirSync10(join46(repo, "shared", name), { recursive: true });
5278
5490
  }
5279
- const userClaudeMd = join45(claude, "CLAUDE.md");
5280
- if (!snapshot || !existsSync39(userClaudeMd)) {
5281
- writeFileSync6(join45(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
5491
+ const userClaudeMd = join46(claude, "CLAUDE.md");
5492
+ if (!snapshot || !existsSync40(userClaudeMd)) {
5493
+ writeFileSync6(join46(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
5282
5494
  item("created shared/CLAUDE.md");
5283
5495
  }
5284
5496
  for (const name of SHARED_KEEP_DIRS) {
5285
- writeFileSync6(join45(repo, "shared", name, ".gitkeep"), "");
5497
+ writeFileSync6(join46(repo, "shared", name, ".gitkeep"), "");
5286
5498
  item(`created shared/${name}/.gitkeep`);
5287
5499
  }
5288
- writeFileSync6(join45(repo, "hosts", ".gitkeep"), "");
5500
+ writeFileSync6(join46(repo, "hosts", ".gitkeep"), "");
5289
5501
  item("created hosts/.gitkeep");
5290
- writeJsonAtomic(join45(repo, "shared", "settings.base.json"), {});
5502
+ writeJsonAtomic(join46(repo, "shared", "settings.base.json"), {});
5291
5503
  item("created shared/settings.base.json");
5292
- writeJsonAtomic(join45(repo, "path-map.json"), { projects: {} });
5504
+ writeJsonAtomic(join46(repo, "path-map.json"), { projects: {} });
5293
5505
  item("created path-map.json");
5294
5506
  if (snapshot) {
5295
5507
  snapshotIntoShared({ projects: {} });
@@ -5590,7 +5802,7 @@ function parsePushArgs(argv) {
5590
5802
  // package.json
5591
5803
  var package_default = {
5592
5804
  name: "claude-nomad",
5593
- version: "0.44.1",
5805
+ version: "0.46.0",
5594
5806
  type: "module",
5595
5807
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
5596
5808
  keywords: [
@@ -5792,15 +6004,15 @@ var DEFAULT_HELP = [
5792
6004
  init_config();
5793
6005
  init_utils();
5794
6006
  init_utils_json();
5795
- import { existsSync as existsSync40, readFileSync as readFileSync12, readdirSync as readdirSync12 } from "node:fs";
5796
- import { join as join46 } from "node:path";
6007
+ import { existsSync as existsSync41, readFileSync as readFileSync14, readdirSync as readdirSync12 } from "node:fs";
6008
+ import { join as join47 } from "node:path";
5797
6009
  function resumeCmd(sessionId) {
5798
6010
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
5799
6011
  fail(`invalid session id: ${sessionId}`);
5800
6012
  process.exit(1);
5801
6013
  }
5802
- const projectsRoot = join46(claudeHome(), "projects");
5803
- if (!existsSync40(projectsRoot)) {
6014
+ const projectsRoot = join47(claudeHome(), "projects");
6015
+ if (!existsSync41(projectsRoot)) {
5804
6016
  fail(`${projectsRoot} does not exist`);
5805
6017
  process.exit(1);
5806
6018
  }
@@ -5814,8 +6026,8 @@ function resumeCmd(sessionId) {
5814
6026
  fail(`no cwd field found in ${jsonlPath}`);
5815
6027
  process.exit(1);
5816
6028
  }
5817
- const mapPath = join46(repoHome(), "path-map.json");
5818
- if (!existsSync40(mapPath)) {
6029
+ const mapPath = join47(repoHome(), "path-map.json");
6030
+ if (!existsSync41(mapPath)) {
5819
6031
  fail("path-map.json missing");
5820
6032
  process.exit(1);
5821
6033
  }
@@ -5838,13 +6050,13 @@ function resumeCmd(sessionId) {
5838
6050
  }
5839
6051
  function findTranscriptPath(projectsRoot, sessionId) {
5840
6052
  for (const dir of readdirSync12(projectsRoot)) {
5841
- const candidate = join46(projectsRoot, dir, `${sessionId}.jsonl`);
5842
- if (existsSync40(candidate)) return candidate;
6053
+ const candidate = join47(projectsRoot, dir, `${sessionId}.jsonl`);
6054
+ if (existsSync41(candidate)) return candidate;
5843
6055
  }
5844
6056
  return null;
5845
6057
  }
5846
6058
  function extractRecordedCwd(jsonlPath) {
5847
- for (const line of readFileSync12(jsonlPath, "utf8").split("\n")) {
6059
+ for (const line of readFileSync14(jsonlPath, "utf8").split("\n")) {
5848
6060
  if (!line.trim()) continue;
5849
6061
  try {
5850
6062
  const obj = JSON.parse(line);