claude-nomad 0.26.2 → 0.27.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/CHANGELOG.md +7 -0
- package/README.md +80 -12
- package/package.json +1 -1
- package/src/commands.doctor.format.ts +2 -81
- package/src/commands.pull.ts +69 -13
- package/src/commands.push.sections.ts +171 -0
- package/src/commands.push.ts +136 -71
- package/src/extras-sync.core.ts +96 -0
- package/src/extras-sync.remap.ts +138 -0
- package/src/extras-sync.ts +22 -168
- package/src/links.ts +14 -3
- package/src/output-tree.ts +91 -0
- package/src/push-leak-verdict.ts +154 -0
- package/src/push-preview.ts +160 -0
- package/src/remap.ts +46 -27
- package/src/summary.ts +75 -27
package/src/remap.ts
CHANGED
|
@@ -49,22 +49,31 @@ export function copyDirJsonlOnly(src: string, dst: string): void {
|
|
|
49
49
|
/**
|
|
50
50
|
* Pull: copy from repo's logical project names into local path-encoded dirs.
|
|
51
51
|
*
|
|
52
|
-
* Returns `{ unmapped
|
|
53
|
-
* this host (
|
|
54
|
-
*
|
|
52
|
+
* Returns `{ unmapped, pulled, wouldPull }`. `unmapped` counts path-map entries
|
|
53
|
+
* skipped for this host (`'TBD'` placeholder or no entry for `HOST`); `pulled`
|
|
54
|
+
* holds logical names copied (wet), `wouldPull` those that would copy under
|
|
55
|
+
* `dryRun`. The arrays let cmdPull render a grouped tree; the wet path no longer
|
|
56
|
+
* logs per-project `pulled X -> Y` / `skip ...` inline. The dry-run path KEEPS
|
|
57
|
+
* its `would overwrite:` line because `computePreview` renders those as the
|
|
58
|
+
* Projects section of `nomad diff` and the dry-run pull preview. The degenerate
|
|
59
|
+
* early-return `log(...)` (not a per-project skip) is preserved.
|
|
55
60
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* computed identically in both modes.
|
|
61
|
+
* @param ts - backup timestamp namespace.
|
|
62
|
+
* @param opts.dryRun - when `true`, collect `wouldPull` and log would-overwrite.
|
|
59
63
|
*/
|
|
60
|
-
export function remapPull(
|
|
64
|
+
export function remapPull(
|
|
65
|
+
ts: string,
|
|
66
|
+
opts: { dryRun?: boolean } = {},
|
|
67
|
+
): { unmapped: number; pulled: string[]; wouldPull: string[] } {
|
|
61
68
|
const dryRun = opts.dryRun === true;
|
|
62
69
|
let unmapped = 0;
|
|
70
|
+
const pulled: string[] = [];
|
|
71
|
+
const wouldPull: string[] = [];
|
|
63
72
|
const mapPath = join(REPO_HOME, 'path-map.json');
|
|
64
73
|
const repoProjects = join(REPO_HOME, 'shared', 'projects');
|
|
65
74
|
if (!existsSync(mapPath) || !existsSync(repoProjects)) {
|
|
66
75
|
log('no path-map or repo projects dir; skipping session remap');
|
|
67
|
-
return { unmapped: 0 };
|
|
76
|
+
return { unmapped: 0, pulled, wouldPull };
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
const map = readJson<PathMap>(mapPath);
|
|
@@ -73,29 +82,28 @@ export function remapPull(ts: string, opts: { dryRun?: boolean } = {}): { unmapp
|
|
|
73
82
|
|
|
74
83
|
for (const [logical, hosts] of Object.entries(map.projects)) {
|
|
75
84
|
const localPath = hosts[HOST];
|
|
76
|
-
if (localPath === 'TBD') {
|
|
85
|
+
if (!localPath || localPath === 'TBD') {
|
|
77
86
|
unmapped++;
|
|
78
|
-
log(`skip ${logical}: placeholder path for ${HOST}`);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (!localPath) {
|
|
82
|
-
unmapped++;
|
|
83
|
-
log(`skip ${logical}: no path for ${HOST}`);
|
|
84
87
|
continue;
|
|
85
88
|
}
|
|
86
89
|
const src = join(repoProjects, logical);
|
|
87
90
|
if (!existsSync(src)) continue;
|
|
88
91
|
const dst = join(localProjects, encodePath(localPath));
|
|
89
92
|
if (dryRun) {
|
|
93
|
+
// KEEP this would-overwrite log: computePreview (backing both `nomad
|
|
94
|
+
// diff` and the dry-run pull preview) renders these lines as its
|
|
95
|
+
// Projects section, so removing them regresses that output. The grouped
|
|
96
|
+
// tree is built only on the WET path, which consumes `pulled` instead.
|
|
97
|
+
wouldPull.push(logical);
|
|
90
98
|
log(`would overwrite: ${dst} (from ${src})`);
|
|
91
99
|
continue;
|
|
92
100
|
}
|
|
93
101
|
// Snapshot prior encoded-path-dir state BEFORE copyDir overwrites it.
|
|
94
102
|
backupBeforeWrite(dst, ts);
|
|
95
103
|
copyDir(src, dst);
|
|
96
|
-
|
|
104
|
+
pulled.push(logical);
|
|
97
105
|
}
|
|
98
|
-
return { unmapped };
|
|
106
|
+
return { unmapped, pulled, wouldPull };
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
/**
|
|
@@ -156,20 +164,32 @@ function buildReverseMap(map: PathMap): Map<string, string> {
|
|
|
156
164
|
* refuse the push before any `shared/projects/` content is written. Detection
|
|
157
165
|
* runs during the reverse-map build, so it fires under `dryRun` too.
|
|
158
166
|
*
|
|
159
|
-
* `opts.dryRun` (default `false`): when `true`,
|
|
160
|
-
*
|
|
161
|
-
*
|
|
167
|
+
* `opts.dryRun` (default `false`): when `true`, collect `wouldPush` without
|
|
168
|
+
* calling `backupRepoWrite` + `copyDir`. Collision detection runs identically
|
|
169
|
+
* in both modes.
|
|
170
|
+
*
|
|
171
|
+
* Returns `pushed` (logical names actually copied, wet mode) and `wouldPush`
|
|
172
|
+
* (logical names under `dryRun`) alongside the counts so cmdPush can render a
|
|
173
|
+
* grouped tree. This function no longer logs per-project `pushed X -> Y` /
|
|
174
|
+
* `skip ... not in path-map` / `would push:` lines inline; the
|
|
175
|
+
* `copyDirJsonlOnly` extension-skip log is a separate file-filter concern and
|
|
176
|
+
* is preserved, as are the degenerate early-return `log(...)` lines.
|
|
177
|
+
*
|
|
178
|
+
* @param ts - backup timestamp namespace.
|
|
179
|
+
* @param opts.dryRun - when `true`, collect `wouldPush` without mutating.
|
|
162
180
|
*/
|
|
163
181
|
export function remapPush(
|
|
164
182
|
ts: string,
|
|
165
183
|
opts: { dryRun?: boolean } = {},
|
|
166
|
-
): { unmapped: number; collisions: number } {
|
|
184
|
+
): { unmapped: number; collisions: number; pushed: string[]; wouldPush: string[] } {
|
|
167
185
|
const dryRun = opts.dryRun === true;
|
|
168
186
|
let unmapped = 0;
|
|
187
|
+
const pushed: string[] = [];
|
|
188
|
+
const wouldPush: string[] = [];
|
|
169
189
|
const mapPath = join(REPO_HOME, 'path-map.json');
|
|
170
190
|
if (!existsSync(mapPath)) {
|
|
171
191
|
log('no path-map.json; skipping session export');
|
|
172
|
-
return { unmapped: 0, collisions: 0 };
|
|
192
|
+
return { unmapped: 0, collisions: 0, pushed, wouldPush };
|
|
173
193
|
}
|
|
174
194
|
|
|
175
195
|
const map = readJson<PathMap>(mapPath);
|
|
@@ -177,7 +197,7 @@ export function remapPush(
|
|
|
177
197
|
const repoProjects = join(REPO_HOME, 'shared', 'projects');
|
|
178
198
|
|
|
179
199
|
const reverse = buildReverseMap(map);
|
|
180
|
-
if (!existsSync(localProjects)) return { unmapped, collisions: 0 };
|
|
200
|
+
if (!existsSync(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
|
|
181
201
|
// Create the repo destination only after collision detection passes and we
|
|
182
202
|
// know there is something to push, so a failing or no-op push is fully
|
|
183
203
|
// side-effect-free (no empty shared/projects/ left behind).
|
|
@@ -187,12 +207,11 @@ export function remapPush(
|
|
|
187
207
|
const logical = reverse.get(dir);
|
|
188
208
|
if (!logical) {
|
|
189
209
|
unmapped++;
|
|
190
|
-
log(`skip ${dir}: not in path-map for ${HOST}`);
|
|
191
210
|
continue;
|
|
192
211
|
}
|
|
193
212
|
const repoDst = join(repoProjects, logical);
|
|
194
213
|
if (dryRun) {
|
|
195
|
-
|
|
214
|
+
wouldPush.push(logical);
|
|
196
215
|
continue;
|
|
197
216
|
}
|
|
198
217
|
// Snapshot repo-side destination before copyDir clobbers it. Git
|
|
@@ -201,7 +220,7 @@ export function remapPush(
|
|
|
201
220
|
// path. Symmetric with remapPull's backupBeforeWrite on the local dst.
|
|
202
221
|
backupRepoWrite(repoDst, ts, REPO_HOME);
|
|
203
222
|
copyDirJsonlOnly(join(localProjects, dir), repoDst);
|
|
204
|
-
|
|
223
|
+
pushed.push(logical);
|
|
205
224
|
}
|
|
206
|
-
return { unmapped, collisions: 0 };
|
|
225
|
+
return { unmapped, collisions: 0, pushed, wouldPush };
|
|
207
226
|
}
|
package/src/summary.ts
CHANGED
|
@@ -1,51 +1,99 @@
|
|
|
1
|
+
import { green, okGlyph, warnGlyph, yellow } from './color.ts';
|
|
1
2
|
import { ok, warn } from './utils.ts';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
* cmdDiff.
|
|
5
|
+
* Pure phrasing core for the end-of-run summary line shared by cmdPull,
|
|
6
|
+
* cmdPush, and cmdDiff. Returns the message `text` (without any status glyph)
|
|
7
|
+
* plus a `clean` flag so callers can pick the right glyph/stream. Canonical
|
|
8
|
+
* phrasing:
|
|
6
9
|
* - `summary: clean` when nothing was unmapped (and, for push, no
|
|
7
|
-
* collisions or extras skipped).
|
|
8
|
-
* terminator and can spot when behavior changes.
|
|
10
|
+
* collisions or extras skipped).
|
|
9
11
|
* - `summary: <N> unmapped on pull (run nomad doctor to list)`
|
|
10
12
|
* - `summary: <N> unmapped on pull, <X> extras skipped (run nomad doctor to list)`
|
|
11
13
|
* - `summary: <N> unmapped on diff (run nomad doctor to list)`
|
|
12
14
|
* - `summary: <N> unmapped on push, <M> collisions (run nomad doctor to list)`
|
|
13
15
|
* - `summary: <N> unmapped on push, <M> collisions, <X> extras skipped (run nomad doctor to list)`
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* contract). This module is the SINGLE source of truth for the phrasing,
|
|
27
|
-
* eliminating drift risk across the three callers by construction.
|
|
17
|
+
* `collisions` is meaningful only for `'push'`; for `'pull'` / `'diff'` it is
|
|
18
|
+
* ignored and defaults to 0. `extrasSkipped` counts dirnames that the
|
|
19
|
+
* per-project whitelist (`SUPPORTED_EXTRAS`) declined to sync. This function is
|
|
20
|
+
* the SINGLE source of truth for the phrasing, so `emitSummary` (standalone
|
|
21
|
+
* line) and `summaryRow` (tree row) cannot drift apart.
|
|
22
|
+
*
|
|
23
|
+
* @param verb - the originating command.
|
|
24
|
+
* @param unmapped - count of path-map entries skipped for this host.
|
|
25
|
+
* @param collisions - push-only collision count (ignored for pull/diff).
|
|
26
|
+
* @param extrasSkipped - count of extras dirnames the whitelist declined.
|
|
27
|
+
* @returns `{ text, clean }` where `clean` is true on the no-warning outcome.
|
|
28
28
|
*/
|
|
29
|
-
export function
|
|
29
|
+
export function summaryText(
|
|
30
30
|
verb: 'pull' | 'push' | 'diff',
|
|
31
31
|
unmapped: number,
|
|
32
32
|
collisions = 0,
|
|
33
33
|
extrasSkipped = 0,
|
|
34
|
-
):
|
|
34
|
+
): { text: string; clean: boolean } {
|
|
35
|
+
const extras = extrasSkipped > 0 ? `, ${extrasSkipped} extras skipped` : '';
|
|
35
36
|
if (verb === 'push') {
|
|
36
37
|
if (unmapped === 0 && collisions === 0 && extrasSkipped === 0) {
|
|
37
|
-
|
|
38
|
-
return;
|
|
38
|
+
return { text: 'summary: clean', clean: true };
|
|
39
39
|
}
|
|
40
40
|
const base = `summary: ${unmapped} unmapped on push, ${collisions} collisions`;
|
|
41
|
-
|
|
42
|
-
warn(`${base}${extras} (run nomad doctor to list)`);
|
|
43
|
-
return;
|
|
41
|
+
return { text: `${base}${extras} (run nomad doctor to list)`, clean: false };
|
|
44
42
|
}
|
|
45
43
|
if (unmapped === 0 && extrasSkipped === 0) {
|
|
46
|
-
|
|
44
|
+
return { text: 'summary: clean', clean: true };
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
text: `summary: ${unmapped} unmapped on ${verb}${extras} (run nomad doctor to list)`,
|
|
48
|
+
clean: false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build the fully-rendered Summary-section row (status glyph embedded) for the
|
|
54
|
+
* grouped push/pull tree. Delegates phrasing to `summaryText` so the row text
|
|
55
|
+
* matches `emitSummary` byte-for-byte. A clean outcome renders
|
|
56
|
+
* `${green(okGlyph)} <text>`; any warning outcome renders
|
|
57
|
+
* `${yellow(warnGlyph)} <text>`.
|
|
58
|
+
*
|
|
59
|
+
* @param verb - the originating command.
|
|
60
|
+
* @param unmapped - count of path-map entries skipped for this host.
|
|
61
|
+
* @param collisions - push-only collision count (ignored for pull/diff).
|
|
62
|
+
* @param extrasSkipped - count of extras dirnames the whitelist declined.
|
|
63
|
+
* @returns the rendered row string for the Summary section.
|
|
64
|
+
*/
|
|
65
|
+
export function summaryRow(
|
|
66
|
+
verb: 'pull' | 'push' | 'diff',
|
|
67
|
+
unmapped: number,
|
|
68
|
+
collisions = 0,
|
|
69
|
+
extrasSkipped = 0,
|
|
70
|
+
): string {
|
|
71
|
+
const { text, clean } = summaryText(verb, unmapped, collisions, extrasSkipped);
|
|
72
|
+
return clean ? `${green(okGlyph)} ${text}` : `${yellow(warnGlyph)} ${text}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Emit the single end-of-run summary line shared by cmdPull, cmdPush, and
|
|
77
|
+
* cmdDiff. Delegates phrasing to `summaryText` so the wording cannot drift from
|
|
78
|
+
* `summaryRow`. Clean outcomes go through `ok()` (green `✓` glyph, stdout) and
|
|
79
|
+
* unmapped / collision / extras-skipped outcomes go through `warn()` (yellow
|
|
80
|
+
* `⚠︎` glyph, stderr). The status glyph carries the success/warn semantics;
|
|
81
|
+
* users see e.g. `✓ summary: clean` or `⚠︎ summary: 3 unmapped on pull (...)`.
|
|
82
|
+
* Clean still goes to stdout so it survives backgrounded shell-rc invocations
|
|
83
|
+
* like `nomad pull 2>/dev/null &`. The fourth positional parameter defaults to
|
|
84
|
+
* 0 so legacy three-arg call sites continue to work unchanged (D-03 additive
|
|
85
|
+
* contract). `cmdDiff` still calls this for its standalone summary line.
|
|
86
|
+
*/
|
|
87
|
+
export function emitSummary(
|
|
88
|
+
verb: 'pull' | 'push' | 'diff',
|
|
89
|
+
unmapped: number,
|
|
90
|
+
collisions = 0,
|
|
91
|
+
extrasSkipped = 0,
|
|
92
|
+
): void {
|
|
93
|
+
const { text, clean } = summaryText(verb, unmapped, collisions, extrasSkipped);
|
|
94
|
+
if (clean) {
|
|
95
|
+
ok(text);
|
|
47
96
|
return;
|
|
48
97
|
}
|
|
49
|
-
|
|
50
|
-
warn(`summary: ${unmapped} unmapped on ${verb}${extras} (run nomad doctor to list)`);
|
|
98
|
+
warn(text);
|
|
51
99
|
}
|