etymd 0.14.0 → 0.16.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 +52 -16
- package/README.md +114 -47
- package/dist/{approve-QDOH2E7V.js → approve-K5FFTPA4.js} +2 -2
- package/dist/{audit-42OHF7ON.js → audit-HBNXNGD6.js} +5 -4
- package/dist/{brief-ZUQK3CFL.js → brief-DOQ2DQEG.js} +2 -2
- package/dist/{fleet-F3XDEDCV.js → chunk-ANXGT7V3.js} +431 -466
- package/dist/{chunk-HWC6PKJM.js → chunk-EJM47PIF.js} +1 -1
- package/dist/{chunk-PAMWS665.js → chunk-FKDT4RBP.js} +2 -1
- package/dist/{chunk-UFNETE6P.js → chunk-OKJ3RGJB.js} +1 -1
- package/dist/{chunk-LT7JMVV3.js → chunk-UV3YIBDW.js} +43 -730
- package/dist/{chunk-623W6KVC.js → chunk-UYCGP5BS.js} +1 -1
- package/dist/chunk-Y4E6ERCM.js +783 -0
- package/dist/cli.js +78 -22
- package/dist/{doctor-CE5SD4JW.js → doctor-X6XAGKV7.js} +5 -4
- package/dist/fleet-UEFLKIRI.js +489 -0
- package/dist/{gates-F4YPKWJR.js → gates-NVWWKMT2.js} +3 -3
- package/dist/{generate-KEX75XNG.js → generate-YEARCSHW.js} +1 -1
- package/dist/index.d.ts +301 -20
- package/dist/index.js +958 -233
- package/dist/{init-AHA7VRRI.js → init-Z5O2WVRJ.js} +3 -3
- package/dist/premise-4QRU7LRI.js +368 -0
- package/dist/propose-IKE4CUIM.js +252 -0
- package/dist/scan-64Z6SNYF.js +5 -0
- package/dist/{scan-TVX3FKK4.js → scan-H3NF5IV5.js} +2 -2
- package/package.json +5 -5
- package/dist/scan-QRTH6HAP.js +0 -5
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { dismiss, accept } from './chunk-HOR4M6EC.js';
|
|
3
|
+
import { sweepFleet, FLEET_JSON_SCHEMA, checkManifest, FLEET_TRUST_VALUES, isFleetTrust, MILESTONES_FILE, parseMilestones, parseInitiatives, BOARD_JSON_SCHEMA, renderBoard, loadFleetManifest, fleetLedgerTarget, ensureFindingRecorded } from './chunk-ANXGT7V3.js';
|
|
4
|
+
import './chunk-UV3YIBDW.js';
|
|
5
|
+
import { parseFailOnTier, meetsFailOn } from './chunk-Y4E6ERCM.js';
|
|
6
|
+
import './chunk-3E2IPCRY.js';
|
|
7
|
+
import { print, theme, section, renderFleetRows, renderFleetNotes, renderFindings, TIER_BADGE, glyph } from './chunk-HI7NWPRA.js';
|
|
8
|
+
import { scanProject } from './chunk-EJM47PIF.js';
|
|
9
|
+
import './chunk-UYCGP5BS.js';
|
|
10
|
+
import './chunk-P6ATKV2R.js';
|
|
11
|
+
import './chunk-YQZDYDAK.js';
|
|
12
|
+
import { readText, pathExists, git } from './chunk-4VPBP6K6.js';
|
|
13
|
+
import { promises } from 'fs';
|
|
14
|
+
import os from 'os';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { select, isCancel, cancel, text, confirm } from '@clack/prompts';
|
|
17
|
+
|
|
18
|
+
var LAST_SWEEP_FILE = "last.fleet.json";
|
|
19
|
+
async function loadManifest(cwd, manifestOpt) {
|
|
20
|
+
if (manifestOpt) {
|
|
21
|
+
return { manifest: await loadFleetManifest(path.resolve(cwd, manifestOpt)) };
|
|
22
|
+
}
|
|
23
|
+
const candidate = path.join(cwd, "registry.json");
|
|
24
|
+
if (await pathExists(candidate)) {
|
|
25
|
+
return {
|
|
26
|
+
manifest: await loadFleetManifest(candidate),
|
|
27
|
+
autoNote: `manifest auto-selected from the cwd: ${candidate} (no --manifest given)`
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
throw new Error(
|
|
31
|
+
"no --manifest given and no registry.json in the current directory \u2014 pass --manifest <file> (there is deliberately no env var and no global pointer)"
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
async function readLastSweep(lastPath) {
|
|
35
|
+
const raw = await readText(lastPath);
|
|
36
|
+
if (raw === null) return { record: null };
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(raw);
|
|
40
|
+
} catch {
|
|
41
|
+
return {
|
|
42
|
+
record: null,
|
|
43
|
+
note: `${LAST_SWEEP_FILE} exists but is not valid JSON \u2014 delta baseline reset (every finding reads as new this sweep).`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const rec = parsed;
|
|
47
|
+
if (typeof rec !== "object" || rec === null || rec.schema !== FLEET_JSON_SCHEMA || typeof rec.projects !== "object" || rec.projects === null || Array.isArray(rec.projects) || !Array.isArray(rec.wall)) {
|
|
48
|
+
return {
|
|
49
|
+
record: null,
|
|
50
|
+
note: `${LAST_SWEEP_FILE} carries a foreign schema \u2014 delta baseline reset (every finding reads as new this sweep).`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { record: rec };
|
|
54
|
+
}
|
|
55
|
+
function failOnExit(failOn, findings) {
|
|
56
|
+
if (failOn === void 0) return;
|
|
57
|
+
if (meetsFailOn(findings, parseFailOnTier(failOn))) process.exitCode = 1;
|
|
58
|
+
}
|
|
59
|
+
async function sweep(opts) {
|
|
60
|
+
if (opts.profile && opts.profile !== "personal" && opts.profile !== "guarded") {
|
|
61
|
+
throw new Error(`--profile must be personal or guarded, got \`${opts.profile}\``);
|
|
62
|
+
}
|
|
63
|
+
if (opts.failOn !== void 0) parseFailOnTier(opts.failOn);
|
|
64
|
+
const { manifest, autoNote } = await loadManifest(opts.cwd, opts.manifest);
|
|
65
|
+
const result = await sweepFleet(manifest, {
|
|
66
|
+
only: opts.only,
|
|
67
|
+
profile: opts.profile,
|
|
68
|
+
kind: opts.truth ? "truth" : void 0,
|
|
69
|
+
persistLedgers: opts.persistLedgers
|
|
70
|
+
});
|
|
71
|
+
const lastPath = path.join(manifest.dir, LAST_SWEEP_FILE);
|
|
72
|
+
const { record: previous, note: baselineNote } = await readLastSweep(lastPath);
|
|
73
|
+
const previousIds = (name) => new Set((previous?.projects[name] ?? []).map((e) => e.id));
|
|
74
|
+
const previousWallIds = new Set((previous?.wall ?? []).map((e) => e.id));
|
|
75
|
+
const allFindings = [...result.projects.flatMap((p) => p.findings), ...result.wall];
|
|
76
|
+
failOnExit(opts.failOn, allFindings);
|
|
77
|
+
const fullSweep = !opts.only && !opts.profile && !opts.truth;
|
|
78
|
+
const delta = {};
|
|
79
|
+
for (const p of result.projects) {
|
|
80
|
+
const prev = previousIds(p.name);
|
|
81
|
+
const current = new Set(p.findings.map((f) => f.id));
|
|
82
|
+
delta[p.name] = {
|
|
83
|
+
new: [...current].filter((id) => !prev.has(id)),
|
|
84
|
+
resolved: [...prev].filter((id) => !current.has(id))
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const wallNew = result.wall.filter((f) => !previousWallIds.has(f.id)).map((f) => f.id);
|
|
88
|
+
if (opts.json) {
|
|
89
|
+
print(
|
|
90
|
+
JSON.stringify(
|
|
91
|
+
{
|
|
92
|
+
...result,
|
|
93
|
+
delta: previous ? delta : null,
|
|
94
|
+
...baselineNote ? { deltaBaselineNote: baselineNote } : {}
|
|
95
|
+
},
|
|
96
|
+
null,
|
|
97
|
+
2
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
if (autoNote) print(` ${theme.dim(`\u25E6 ${autoNote}`)}`);
|
|
102
|
+
if (baselineNote) print(` ${theme.dim(`\u25E6 ${baselineNote}`)}`);
|
|
103
|
+
renderSweep(result, previous, delta, wallNew);
|
|
104
|
+
}
|
|
105
|
+
if (fullSweep) {
|
|
106
|
+
const record = {
|
|
107
|
+
schema: FLEET_JSON_SCHEMA,
|
|
108
|
+
sweptAt: result.sweptAt,
|
|
109
|
+
projects: Object.fromEntries(
|
|
110
|
+
result.projects.map((p) => [p.name, p.findings.map((f) => ({ id: f.id, tier: f.tier }))])
|
|
111
|
+
),
|
|
112
|
+
wall: result.wall.map((f) => ({ id: f.id, tier: f.tier }))
|
|
113
|
+
};
|
|
114
|
+
const tmpPath = `${lastPath}.tmp`;
|
|
115
|
+
await promises.writeFile(tmpPath, JSON.stringify(record, null, 2) + "\n", "utf8");
|
|
116
|
+
await promises.rename(tmpPath, lastPath);
|
|
117
|
+
} else if (!opts.json) {
|
|
118
|
+
print();
|
|
119
|
+
print(` ${theme.dim("partial sweep \u2014 the delta baseline was not updated")}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function renderSweep(result, previous, delta, wallNew) {
|
|
123
|
+
section(
|
|
124
|
+
`Fleet sweep ${theme.dim(`\xB7 ${path.basename(result.manifest)} \xB7 ${result.projects.length} project(s) \xB7 schema ${result.schema}`)}`
|
|
125
|
+
);
|
|
126
|
+
const rows = result.projects.map((p) => {
|
|
127
|
+
const d = delta[p.name] ?? { new: [], resolved: [] };
|
|
128
|
+
const deltaStr = !previous ? "first sweep" : d.new.length || d.resolved.length ? `\u0394 ${[d.new.length ? `+${d.new.length}` : "", d.resolved.length ? `\u2212${d.resolved.length}` : ""].filter(Boolean).join(" ")}` : "\u0394 \xB10";
|
|
129
|
+
return {
|
|
130
|
+
name: p.name,
|
|
131
|
+
age: p.stateAgeDays === null ? "\u2014" : `${p.stateAgeDays}/${p.staleAfterDays}d`,
|
|
132
|
+
counts: p.counts,
|
|
133
|
+
delta: deltaStr,
|
|
134
|
+
note: p.unresolved
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
renderFleetRows(rows);
|
|
138
|
+
renderFleetNotes(
|
|
139
|
+
result.problems,
|
|
140
|
+
result.projects.flatMap((p) => p.disclosures.map((d) => `${p.name}: ${d}`))
|
|
141
|
+
);
|
|
142
|
+
const detail = [
|
|
143
|
+
...result.projects.flatMap(
|
|
144
|
+
(p) => p.findings.filter((f) => f.tier === "risk" || (delta[p.name]?.new ?? []).includes(f.id))
|
|
145
|
+
),
|
|
146
|
+
...result.wall.filter((f) => f.tier === "risk" || wallNew.includes(f.id))
|
|
147
|
+
];
|
|
148
|
+
if (detail.length) {
|
|
149
|
+
section(`New or risk findings ${theme.dim(`(${detail.length})`)}`);
|
|
150
|
+
renderFindings(detail);
|
|
151
|
+
}
|
|
152
|
+
if (result.recurringClasses.length) {
|
|
153
|
+
section(`Recurring classes ${theme.dim("(open in \u22652 projects \u2014 class-fix candidates)")}`);
|
|
154
|
+
for (const rc of result.recurringClasses) {
|
|
155
|
+
print(
|
|
156
|
+
` ${TIER_BADGE[rc.tier]} ${rc.classId} ${theme.dim(`\u2014 ${rc.projects.length}\xD7: ${rc.projects.join(", ")}`)}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
section("Fleet wall");
|
|
161
|
+
if (!result.wall.length) print(` ${glyph.ok} ${theme.dim("clean")}`);
|
|
162
|
+
else
|
|
163
|
+
print(
|
|
164
|
+
` ${theme.warn(`${result.wall.length} finding(s)`)} ${theme.dim("(detailed above if new or risk)")}`
|
|
165
|
+
);
|
|
166
|
+
renderFleetNotes([], result.wallDisclosures);
|
|
167
|
+
if (result.outOfScope.length) {
|
|
168
|
+
print(
|
|
169
|
+
` ${theme.dim(`out of scope (not audited, named above): ${result.outOfScope.join(", ")}`)}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function check(opts) {
|
|
174
|
+
const { manifest, autoNote } = await loadManifest(opts.cwd, opts.manifest);
|
|
175
|
+
const { findings, disclosures } = await checkManifest(manifest);
|
|
176
|
+
if (findings.length) process.exitCode = 1;
|
|
177
|
+
if (opts.json) {
|
|
178
|
+
print(
|
|
179
|
+
JSON.stringify(
|
|
180
|
+
{ schema: FLEET_JSON_SCHEMA, manifest: manifest.manifestPath, findings, disclosures },
|
|
181
|
+
null,
|
|
182
|
+
2
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (autoNote) print(` ${theme.dim(`\u25E6 ${autoNote}`)}`);
|
|
188
|
+
section(
|
|
189
|
+
`Fleet check ${theme.dim(`\xB7 ${path.basename(manifest.manifestPath)} \xB7 ${manifest.entries.length} entr${manifest.entries.length === 1 ? "y" : "ies"} \xB7 manifest truth only, no lenses`)}`
|
|
190
|
+
);
|
|
191
|
+
renderFindings(findings);
|
|
192
|
+
renderFleetNotes([], disclosures);
|
|
193
|
+
}
|
|
194
|
+
async function recordGuardedMapping(manifest, name, absTarget) {
|
|
195
|
+
const home = os.homedir();
|
|
196
|
+
const value = absTarget === home || absTarget.startsWith(home + path.sep) ? `~${absTarget.slice(home.length)}` : absTarget;
|
|
197
|
+
const insideRepo = await git(manifest.dir, ["rev-parse", "--is-inside-work-tree"]) === "true";
|
|
198
|
+
if (insideRepo) {
|
|
199
|
+
const ignored = await git(manifest.dir, ["check-ignore", "-q", manifest.localPath]) !== null;
|
|
200
|
+
if (!ignored) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`${path.basename(manifest.localPath)} is not gitignored \u2014 refusing to write guarded-side directory names into a file git would track. Add it to .gitignore, then re-run.`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const existing = await readText(manifest.localPath);
|
|
207
|
+
let doc = {};
|
|
208
|
+
if (existing) {
|
|
209
|
+
try {
|
|
210
|
+
doc = JSON.parse(existing);
|
|
211
|
+
} catch {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`${path.basename(manifest.localPath)} is not valid JSON \u2014 fix it before registering, so the mapping is not written over hand-maintained entries that cannot be re-derived.`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const dirs = doc.dirs ?? {};
|
|
218
|
+
dirs[name] = value;
|
|
219
|
+
doc.dirs = dirs;
|
|
220
|
+
await promises.writeFile(manifest.localPath, JSON.stringify(doc, null, 2) + "\n", "utf8");
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
async function add(opts) {
|
|
224
|
+
const { manifest, autoNote } = await loadManifest(opts.cwd, opts.manifest);
|
|
225
|
+
if (autoNote) print(` ${theme.dim(`\u25E6 ${autoNote}`)}`);
|
|
226
|
+
if (manifest.shape !== "registry") {
|
|
227
|
+
throw new Error("`fleet add` targets the registry shape \u2014 this manifest is a legacy corpus");
|
|
228
|
+
}
|
|
229
|
+
const raw = await readText(manifest.manifestPath);
|
|
230
|
+
if (raw === null) throw new Error(`cannot read ${manifest.manifestPath}`);
|
|
231
|
+
const absTarget = path.resolve(opts.cwd, opts.target);
|
|
232
|
+
if (!await pathExists(absTarget)) throw new Error(`no such directory: ${absTarget}`);
|
|
233
|
+
const name = opts.name ?? path.basename(absTarget);
|
|
234
|
+
if (manifest.entries.some((e) => e.name === name)) {
|
|
235
|
+
throw new Error(`\`${name}\` is already registered \u2014 resolution is keyed by name`);
|
|
236
|
+
}
|
|
237
|
+
const facts = await scanProject(absTarget);
|
|
238
|
+
const remote = facts.git.isRepo ? await git(absTarget, ["remote", "get-url", "origin"]) : null;
|
|
239
|
+
section(`Fleet add ${theme.dim(`\xB7 ${name} \xB7 ${absTarget}`)}`);
|
|
240
|
+
let profile = opts.profile;
|
|
241
|
+
if (!profile && !opts.yes) {
|
|
242
|
+
const picked = await select({
|
|
243
|
+
message: "Which side of the wall does this belong to?",
|
|
244
|
+
initialValue: "personal",
|
|
245
|
+
options: [
|
|
246
|
+
{ value: "personal", label: "personal \u2014 your own work" },
|
|
247
|
+
{ value: "guarded", label: "guarded \u2014 kept apart: alias-only, machine-pinned" }
|
|
248
|
+
]
|
|
249
|
+
});
|
|
250
|
+
if (isCancel(picked)) {
|
|
251
|
+
cancel("No entry written.");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
profile = picked;
|
|
255
|
+
}
|
|
256
|
+
profile = profile ?? "personal";
|
|
257
|
+
if (profile !== "personal" && profile !== "guarded") {
|
|
258
|
+
throw new Error(`--profile must be personal or guarded, got \`${profile}\``);
|
|
259
|
+
}
|
|
260
|
+
const derivedKind = facts.commands.build || facts.commands.test ? "tool" : "repo";
|
|
261
|
+
let kind = opts.kind;
|
|
262
|
+
if (!kind && !opts.yes) {
|
|
263
|
+
const known = [...new Set(manifest.entries.map((e) => e.kind).filter(Boolean))];
|
|
264
|
+
const choices = [.../* @__PURE__ */ new Set([derivedKind, ...known])];
|
|
265
|
+
const picked = await select({
|
|
266
|
+
message: "Kind?",
|
|
267
|
+
initialValue: derivedKind,
|
|
268
|
+
options: [
|
|
269
|
+
...choices.map((k) => ({
|
|
270
|
+
value: k,
|
|
271
|
+
label: k === derivedKind ? `${k} \u2014 matches this repo's shape` : k
|
|
272
|
+
})),
|
|
273
|
+
{ value: "", label: "something else\u2026" }
|
|
274
|
+
]
|
|
275
|
+
});
|
|
276
|
+
if (isCancel(picked)) {
|
|
277
|
+
cancel("No entry written.");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
kind = picked;
|
|
281
|
+
if (!kind) {
|
|
282
|
+
const typed = await text({
|
|
283
|
+
message: "Kind (free text):",
|
|
284
|
+
validate: (v) => v.trim() ? void 0 : "a kind is required"
|
|
285
|
+
});
|
|
286
|
+
if (isCancel(typed)) {
|
|
287
|
+
cancel("No entry written.");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
kind = typed.trim();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
kind = kind ?? derivedKind;
|
|
294
|
+
let trust;
|
|
295
|
+
if (profile === "personal") {
|
|
296
|
+
trust = opts.trust;
|
|
297
|
+
if (!trust && !opts.yes) {
|
|
298
|
+
const picked = await select({
|
|
299
|
+
message: "How exposed is this repo's history? (gates content screening)",
|
|
300
|
+
options: [
|
|
301
|
+
{ value: "private", label: "private \u2014 not destined to be published" },
|
|
302
|
+
{ value: "public-bound", label: "public-bound \u2014 private now, plausibly public later" },
|
|
303
|
+
{ value: "public-repo", label: "public-repo \u2014 already public" }
|
|
304
|
+
]
|
|
305
|
+
});
|
|
306
|
+
if (isCancel(picked)) {
|
|
307
|
+
cancel("No entry written.");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
trust = picked;
|
|
311
|
+
}
|
|
312
|
+
if (!trust) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`\`${name}\` needs a trust level: pass --trust <${FLEET_TRUST_VALUES.join("|")}> (non-interactive runs cannot be prompted, and there is deliberately no default \u2014 trust gates content screening)`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (!isFleetTrust(trust)) {
|
|
318
|
+
throw new Error(`--trust must be one of ${FLEET_TRUST_VALUES.join(", ")}, got \`${trust}\``);
|
|
319
|
+
}
|
|
320
|
+
} else if (opts.trust) {
|
|
321
|
+
throw new Error("guarded entries take no `trust` \u2014 `profile: guarded` already implies it");
|
|
322
|
+
}
|
|
323
|
+
const guardedHost = remote ? manifest.guardedHosts.find((h) => remote.includes(h)) : void 0;
|
|
324
|
+
if (guardedHost && profile !== "guarded") {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`\`${name}\` has a remote on the guarded host \`${guardedHost}\`, but --profile is \`${profile}\`. A personal entry records its path and raw remote in the TRACKED manifest \u2014 register it with \`--profile guarded\` (alias-only, machine-pinned), or move it out of the fleet root.`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const entry = profile === "guarded" ? { name, kind, profile, private: true } : {
|
|
330
|
+
name,
|
|
331
|
+
kind,
|
|
332
|
+
profile,
|
|
333
|
+
path: path.relative(manifest.root ?? manifest.dir, absTarget),
|
|
334
|
+
trust,
|
|
335
|
+
// A milestones file present at registration is registered as the project's plan
|
|
336
|
+
// surface; absent, the board shows the project as "not declared" until one lands —
|
|
337
|
+
// never a silent default to `none`, which is a decision only the owner takes.
|
|
338
|
+
contract: await pathExists(path.join(absTarget, MILESTONES_FILE)) ? { milestones: MILESTONES_FILE } : {},
|
|
339
|
+
links: {}
|
|
340
|
+
// The remote is deliberately NOT recorded. Nothing reads it — it was persisted only
|
|
341
|
+
// because it happened to be derivable — and it is the field that turns a mis-profiled
|
|
342
|
+
// entry into a disclosure: a raw URL carries the host AND the internal group path,
|
|
343
|
+
// while `path` carries a bare directory name. Removing a field no consumer reads
|
|
344
|
+
// retires that whole class without any host-matching guess, and keeps working on a
|
|
345
|
+
// machine that has no local manifest for the guarded-host guard above to read.
|
|
346
|
+
// It stays derivable at any time from the checkout itself, which is where it came from.
|
|
347
|
+
};
|
|
348
|
+
print(` ${glyph.bullet} ${theme.dim("entry")}`);
|
|
349
|
+
print(
|
|
350
|
+
JSON.stringify(entry, null, 2).split("\n").map((l) => ` ${theme.dim(l)}`).join("\n")
|
|
351
|
+
);
|
|
352
|
+
if (!opts.yes) {
|
|
353
|
+
const ok = await confirm({
|
|
354
|
+
message: `Write this entry to ${path.basename(manifest.manifestPath)}?`
|
|
355
|
+
});
|
|
356
|
+
if (isCancel(ok) || !ok) {
|
|
357
|
+
cancel("No entry written.");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
let mappedTo;
|
|
362
|
+
if (profile === "guarded") {
|
|
363
|
+
mappedTo = await recordGuardedMapping(manifest, name, absTarget);
|
|
364
|
+
}
|
|
365
|
+
const doc = JSON.parse(raw);
|
|
366
|
+
if (!Array.isArray(doc.projects)) throw new Error("manifest has no `projects` array to append to");
|
|
367
|
+
doc.projects.push(entry);
|
|
368
|
+
await promises.writeFile(manifest.manifestPath, JSON.stringify(doc, null, 2) + "\n", "utf8");
|
|
369
|
+
print(` ${glyph.ok} ${theme.dim("registered")} ${theme.info(name)}`);
|
|
370
|
+
if (mappedTo) {
|
|
371
|
+
print(
|
|
372
|
+
` ${glyph.ok} ${theme.dim("mapped")} ${theme.info(`${name} \u2192 ${mappedTo}`)} ${theme.dim(`in ${path.basename(manifest.localPath)} (gitignored)`)}`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
print(` ${theme.dim(`verify with \`etymd fleet check --manifest ${manifest.manifestPath}\``)}`);
|
|
376
|
+
}
|
|
377
|
+
async function resolveFleetFinding(opts, status) {
|
|
378
|
+
if (status === "dismissed" && !opts.reason?.trim()) {
|
|
379
|
+
throw new Error('dismiss needs a reason: etymd fleet dismiss <name> <id> --reason "\u2026"');
|
|
380
|
+
}
|
|
381
|
+
const { manifest, autoNote } = await loadManifest(opts.cwd, opts.manifest);
|
|
382
|
+
if (autoNote) print(` ${theme.dim(`\u25E6 ${autoNote}`)}`);
|
|
383
|
+
const target = await fleetLedgerTarget(manifest, opts.name);
|
|
384
|
+
const hadLedger = await pathExists(path.join(target.ledgerRoot, ".etymd"));
|
|
385
|
+
const { ledgerRoot, recorded } = await ensureFindingRecorded(manifest, opts.name, opts.id);
|
|
386
|
+
if (!recorded) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`no finding \`${opts.id}\` recorded for \`${opts.name}\` even after a fresh audit \u2014 check the id against \`etymd fleet\` output (fleet-manifest wall findings are not ledger-quietable)`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (!hadLedger) {
|
|
392
|
+
print(
|
|
393
|
+
` ${theme.dim(`first ledger for \`${opts.name}\`: created ${path.join(ledgerRoot, ".etymd")} (a persisting audit ran to record the finding)`)}`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
const args = { cwd: ledgerRoot, id: opts.id, reason: opts.reason };
|
|
397
|
+
await (status === "dismissed" ? dismiss(args) : accept(args));
|
|
398
|
+
print(` ${theme.dim(`ledger: ${path.join(ledgerRoot, ".etymd", "ledger.json")}`)}`);
|
|
399
|
+
}
|
|
400
|
+
async function dismiss2(opts) {
|
|
401
|
+
await resolveFleetFinding(opts, "dismissed");
|
|
402
|
+
}
|
|
403
|
+
async function accept2(opts) {
|
|
404
|
+
await resolveFleetFinding(opts, "accepted");
|
|
405
|
+
}
|
|
406
|
+
async function board(opts) {
|
|
407
|
+
const { manifest, autoNote } = await loadManifest(opts.cwd, opts.manifest);
|
|
408
|
+
const projects = [];
|
|
409
|
+
for (const entry of manifest.entries) {
|
|
410
|
+
if (entry.profile === "guarded") continue;
|
|
411
|
+
const file = entry.contract.milestones;
|
|
412
|
+
const base = { name: entry.name, kind: entry.kind, rows: [], problems: [] };
|
|
413
|
+
if (!file) {
|
|
414
|
+
projects.push({ ...base, state: "not-declared" });
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (file === "none") {
|
|
418
|
+
projects.push({ ...base, file, state: "none" });
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (!entry.resolvedRoot) {
|
|
422
|
+
projects.push({
|
|
423
|
+
...base,
|
|
424
|
+
file,
|
|
425
|
+
state: "unresolved",
|
|
426
|
+
problems: [entry.unresolved ?? "unresolved"]
|
|
427
|
+
});
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const text2 = await readText(path.join(entry.resolvedRoot, file));
|
|
431
|
+
if (text2 === null) {
|
|
432
|
+
projects.push({ ...base, file, state: "missing" });
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const doc = parseMilestones(text2);
|
|
436
|
+
projects.push({
|
|
437
|
+
...base,
|
|
438
|
+
file,
|
|
439
|
+
rows: doc.rows,
|
|
440
|
+
problems: doc.problems,
|
|
441
|
+
state: doc.problems.length ? "invalid" : doc.rows.length ? "ok" : "declared-empty"
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
let initiatives = null;
|
|
445
|
+
let initiativesProblems = [];
|
|
446
|
+
if (opts.initiatives) {
|
|
447
|
+
const abs = path.resolve(opts.cwd, opts.initiatives);
|
|
448
|
+
const text2 = await readText(abs);
|
|
449
|
+
if (text2 === null) throw new Error(`initiatives file not found: ${abs}`);
|
|
450
|
+
const parsed = parseInitiatives(text2);
|
|
451
|
+
initiatives = parsed.rows;
|
|
452
|
+
initiativesProblems = parsed.problems;
|
|
453
|
+
}
|
|
454
|
+
const generatedOn = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
455
|
+
const input = {
|
|
456
|
+
generatedOn,
|
|
457
|
+
manifest: path.basename(manifest.manifestPath),
|
|
458
|
+
initiatives,
|
|
459
|
+
initiativesProblems,
|
|
460
|
+
projects
|
|
461
|
+
};
|
|
462
|
+
const trouble = projects.filter((p) => p.state === "missing" || p.state === "invalid");
|
|
463
|
+
if (trouble.length || initiativesProblems.length) process.exitCode = 1;
|
|
464
|
+
if (opts.json) {
|
|
465
|
+
print(JSON.stringify({ schema: BOARD_JSON_SCHEMA, ...input }, null, 2));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const markdown = renderBoard(input);
|
|
469
|
+
if (opts.out) {
|
|
470
|
+
const abs = path.resolve(opts.cwd, opts.out);
|
|
471
|
+
await promises.mkdir(path.dirname(abs), { recursive: true });
|
|
472
|
+
await promises.writeFile(abs, markdown, "utf8");
|
|
473
|
+
const total = projects.reduce((n, p) => n + p.rows.length, 0);
|
|
474
|
+
if (autoNote) print(` ${theme.dim(`\u25E6 ${autoNote}`)}`);
|
|
475
|
+
print(
|
|
476
|
+
` ${glyph.bullet} wrote ${opts.out} \u2014 ${projects.length} project${projects.length === 1 ? "" : "s"}, ${total} milestone${total === 1 ? "" : "s"}` + (initiatives ? `, ${initiatives.length} initiative${initiatives.length === 1 ? "" : "s"}` : "")
|
|
477
|
+
);
|
|
478
|
+
for (const p of trouble) {
|
|
479
|
+
print(
|
|
480
|
+
` ${TIER_BADGE.gap} ${p.name}: ${p.state === "missing" ? `\`${p.file}\` declared and absent` : p.problems.join("; ")}`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
for (const problem of initiativesProblems) print(` ${TIER_BADGE.gap} initiatives: ${problem}`);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
print(markdown);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export { accept2 as accept, add, board, check, dismiss2 as dismiss, sweep };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { applyFiles } from './chunk-GWGKEPRX.js';
|
|
3
|
-
import { deriveFailOn, derivedCommands, planWorkflow, runPrefix, isSafeGateCommand } from './chunk-
|
|
3
|
+
import { deriveFailOn, derivedCommands, planWorkflow, runPrefix, isSafeGateCommand } from './chunk-OKJ3RGJB.js';
|
|
4
4
|
import { section, print, glyph, theme, renderPlan } from './chunk-HI7NWPRA.js';
|
|
5
|
-
import { scanProject } from './chunk-
|
|
6
|
-
import './chunk-
|
|
5
|
+
import { scanProject } from './chunk-EJM47PIF.js';
|
|
6
|
+
import './chunk-UYCGP5BS.js';
|
|
7
7
|
import { readConfig, CONFIG_FILE, configPath } from './chunk-P6ATKV2R.js';
|
|
8
8
|
import './chunk-YQZDYDAK.js';
|
|
9
9
|
import { readText, git } from './chunk-4VPBP6K6.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export { deriveFailOn, derivedCommands, planWorkflow, riskReachability } from './chunk-
|
|
2
|
+
export { deriveFailOn, derivedCommands, planWorkflow, riskReachability } from './chunk-OKJ3RGJB.js';
|
|
3
3
|
import './chunk-P6ATKV2R.js';
|
|
4
4
|
import './chunk-YQZDYDAK.js';
|
|
5
5
|
import './chunk-4VPBP6K6.js';
|