@blurname/debrief 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +188 -0
  3. package/dist/debrief.js +1207 -0
  4. package/package.json +39 -0
@@ -0,0 +1,1207 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // src/backends/mock.ts
5
+ import { join as join2 } from "node:path";
6
+
7
+ // src/version.ts
8
+ function parseVersion(raw) {
9
+ const v = raw.trim();
10
+ let epoch = 0;
11
+ let rest = v;
12
+ const colon = v.indexOf(":");
13
+ if (colon !== -1) {
14
+ const e = v.slice(0, colon);
15
+ if (/^\d+$/.test(e)) {
16
+ epoch = parseInt(e, 10);
17
+ rest = v.slice(colon + 1);
18
+ }
19
+ }
20
+ let upstream = rest;
21
+ let revision = "";
22
+ const dash = rest.lastIndexOf("-");
23
+ if (dash !== -1) {
24
+ upstream = rest.slice(0, dash);
25
+ revision = rest.slice(dash + 1);
26
+ }
27
+ return { epoch, upstream, revision };
28
+ }
29
+ function order(c) {
30
+ if (c === undefined || c === "")
31
+ return 0;
32
+ if (c === "~")
33
+ return -1;
34
+ if (/[a-zA-Z]/.test(c))
35
+ return c.charCodeAt(0);
36
+ return c.charCodeAt(0) + 256;
37
+ }
38
+ var isDigit = (c) => c !== undefined && c >= "0" && c <= "9";
39
+ function verrevcmp(a, b) {
40
+ let i = 0;
41
+ let j = 0;
42
+ while (i < a.length || j < b.length) {
43
+ while (i < a.length && !isDigit(a[i]) || j < b.length && !isDigit(b[j])) {
44
+ const ac = order(a[i]);
45
+ const bc = order(b[j]);
46
+ if (ac !== bc)
47
+ return ac - bc;
48
+ i++;
49
+ j++;
50
+ }
51
+ while (a[i] === "0")
52
+ i++;
53
+ while (b[j] === "0")
54
+ j++;
55
+ let firstDiff = 0;
56
+ while (isDigit(a[i]) && isDigit(b[j])) {
57
+ if (firstDiff === 0)
58
+ firstDiff = a.charCodeAt(i) - b.charCodeAt(j);
59
+ i++;
60
+ j++;
61
+ }
62
+ if (isDigit(a[i]))
63
+ return 1;
64
+ if (isDigit(b[j]))
65
+ return -1;
66
+ if (firstDiff !== 0)
67
+ return firstDiff;
68
+ }
69
+ return 0;
70
+ }
71
+ function compareVersions(a, b) {
72
+ const va = parseVersion(a);
73
+ const vb = parseVersion(b);
74
+ if (va.epoch !== vb.epoch)
75
+ return va.epoch - vb.epoch;
76
+ const up = verrevcmp(va.upstream, vb.upstream);
77
+ if (up !== 0)
78
+ return up;
79
+ return verrevcmp(va.revision, vb.revision);
80
+ }
81
+ function maxVersion(versions) {
82
+ if (versions.length === 0)
83
+ return;
84
+ return versions.reduce((best, v) => compareVersions(v, best) > 0 ? v : best);
85
+ }
86
+ function numericComponents(upstream) {
87
+ const out = [];
88
+ for (const part of upstream.split(".")) {
89
+ const m = /^(\d+)/.exec(part);
90
+ if (!m)
91
+ break;
92
+ out.push(parseInt(m[1], 10));
93
+ if (m[1].length !== part.length)
94
+ break;
95
+ }
96
+ return out;
97
+ }
98
+ var gte = (v, bound) => compareVersions(v, bound) >= 0;
99
+ var gt = (v, bound) => compareVersions(v, bound) > 0;
100
+ var lt = (v, bound) => compareVersions(v, bound) < 0;
101
+ var lte = (v, bound) => compareVersions(v, bound) <= 0;
102
+ var eq = (v, bound) => compareVersions(v, bound) === 0;
103
+ function caretUpper(version) {
104
+ const { epoch, upstream } = parseVersion(version);
105
+ const comps = numericComponents(upstream);
106
+ const major = comps[0] ?? 0;
107
+ const prefix = epoch > 0 ? `${epoch}:` : "";
108
+ return `${prefix}${major + 1}`;
109
+ }
110
+ function tildeUpper(version) {
111
+ const { epoch, upstream } = parseVersion(version);
112
+ const comps = numericComponents(upstream);
113
+ const prefix = epoch > 0 ? `${epoch}:` : "";
114
+ if (comps.length >= 2)
115
+ return `${prefix}${comps[0]}.${comps[1] + 1}`;
116
+ return `${prefix}${(comps[0] ?? 0) + 1}`;
117
+ }
118
+ function parseComparator(token) {
119
+ const t = token.trim();
120
+ if (t === "" || t === "*" || t === "x" || t === "latest" || t === "any") {
121
+ return () => true;
122
+ }
123
+ let m;
124
+ if (m = /^\^\s*(.+)$/.exec(t)) {
125
+ const base = m[1].trim();
126
+ const upper = caretUpper(base);
127
+ return (v) => gte(v, base) && lt(v, upper);
128
+ }
129
+ if (m = /^~\s*(.+)$/.exec(t)) {
130
+ const base = m[1].trim();
131
+ const upper = tildeUpper(base);
132
+ return (v) => gte(v, base) && lt(v, upper);
133
+ }
134
+ if (m = /^>=\s*(.+)$/.exec(t))
135
+ return (v) => gte(v, m[1].trim());
136
+ if (m = /^<=\s*(.+)$/.exec(t))
137
+ return (v) => lte(v, m[1].trim());
138
+ if (m = /^>\s*(.+)$/.exec(t))
139
+ return (v) => gt(v, m[1].trim());
140
+ if (m = /^<\s*(.+)$/.exec(t))
141
+ return (v) => lt(v, m[1].trim());
142
+ if (m = /^!=\s*(.+)$/.exec(t))
143
+ return (v) => !eq(v, m[1].trim());
144
+ if (m = /^=\s*(.+)$/.exec(t))
145
+ return (v) => eq(v, m[1].trim());
146
+ return (v) => eq(v, t);
147
+ }
148
+ function parseConstraint(raw) {
149
+ const tokens = raw.trim().split(/\s+/).filter(Boolean);
150
+ const preds = tokens.length === 0 ? [() => true] : tokens.map(parseComparator);
151
+ return {
152
+ raw: raw.trim() || "*",
153
+ test: (version) => preds.every((p) => p(version))
154
+ };
155
+ }
156
+ function bestMatch(constraint, candidates) {
157
+ const c = parseConstraint(constraint);
158
+ return maxVersion(candidates.filter((v) => c.test(v)));
159
+ }
160
+
161
+ // src/sys.ts
162
+ import { accessSync, constants } from "node:fs";
163
+ import { access, readFile, writeFile } from "node:fs/promises";
164
+ import { execFile } from "node:child_process";
165
+ import { join } from "node:path";
166
+ async function exists(path) {
167
+ try {
168
+ await access(path);
169
+ return true;
170
+ } catch {
171
+ return false;
172
+ }
173
+ }
174
+ async function readText(path) {
175
+ return readFile(path, "utf8");
176
+ }
177
+ async function readJson(path) {
178
+ return JSON.parse(await readText(path));
179
+ }
180
+ async function readBytes(path) {
181
+ return readFile(path);
182
+ }
183
+ async function writeText(path, data) {
184
+ await writeFile(path, data);
185
+ }
186
+ function run(cmd, opts = {}) {
187
+ const [bin, ...args] = cmd;
188
+ return new Promise((resolve) => {
189
+ execFile(bin, args, {
190
+ env: opts.env,
191
+ timeout: opts.timeoutMs,
192
+ encoding: "utf8",
193
+ maxBuffer: 64 * 1024 * 1024
194
+ }, (err, stdout, stderr) => {
195
+ let code = 0;
196
+ if (err)
197
+ code = typeof err.code === "number" ? err.code : 1;
198
+ resolve({ code, stdout: stdout ?? "", stderr: stderr ?? "" });
199
+ });
200
+ });
201
+ }
202
+ function whichSync(cmd) {
203
+ const paths = (process.env.PATH ?? "").split(":");
204
+ for (const dir of paths) {
205
+ if (!dir)
206
+ continue;
207
+ const full = join(dir, cmd);
208
+ try {
209
+ accessSync(full, constants.X_OK);
210
+ return full;
211
+ } catch {}
212
+ }
213
+ return null;
214
+ }
215
+
216
+ // src/backends/mock.ts
217
+ var DEFAULT_SYSTEM = {
218
+ nginx: { version: "1.24.0-2", auto: false },
219
+ git: { version: "1:2.39.2-1.1", auto: false },
220
+ htop: { version: "3.2.1-1", auto: false },
221
+ curl: { version: "7.88.1-10", auto: false },
222
+ vim: { version: "2:9.0.1378-2", auto: false },
223
+ jq: { version: "1.6-2.1", auto: false },
224
+ "build-essential": { version: "12.9", auto: false },
225
+ libc6: { version: "2.36-9+deb12u4", auto: true },
226
+ libssl3: { version: "3.0.11-1~deb12u2", auto: true },
227
+ zlib1g: { version: "1:1.2.13.dfsg-1", auto: true },
228
+ "libpcre2-8-0": { version: "10.42-1", auto: true },
229
+ "perl-base": { version: "5.36.0-7+deb12u1", auto: true },
230
+ "libstdc++6": { version: "12.2.0-14", auto: true },
231
+ "libgcc-s1": { version: "12.2.0-14", auto: true },
232
+ "ca-certificates": { version: "20230311", auto: true },
233
+ libcurl4: { version: "7.88.1-10", auto: true }
234
+ };
235
+ var DEFAULT_REPO = {
236
+ nginx: ["1.18.0-6.1", "1.22.1-9", "1.24.0-1", "1.24.0-2", "1.26.0-1"],
237
+ git: ["1:2.30.2-1", "1:2.39.2-1.1", "1:2.43.0-1"],
238
+ htop: ["3.0.5-7", "3.2.1-1", "3.3.0-1"],
239
+ curl: ["7.74.0-1.3", "7.88.1-10", "8.5.0-2"],
240
+ vim: ["2:8.2.3995-1", "2:9.0.1378-2", "2:9.1.0016-1"],
241
+ ripgrep: ["13.0.0-4", "14.1.0-1"],
242
+ jq: ["1.6-2.1", "1.7.1-3"],
243
+ postgresql: ["13+225", "15+248", "16+257"],
244
+ redis: ["5:6.0.16-1", "5:7.0.15-1"],
245
+ "build-essential": ["12.9", "12.10"]
246
+ };
247
+ var STATE_FILE = ".debrief-mock-state.json";
248
+ function normalizeInstalled(raw) {
249
+ const out = {};
250
+ const src = raw ?? {};
251
+ for (const [name, val] of Object.entries(src)) {
252
+ if (typeof val === "string")
253
+ out[name] = { version: val, auto: false };
254
+ else if (val && typeof val === "object" && "version" in val) {
255
+ const v = val;
256
+ out[name] = { version: v.version, auto: v.auto ?? false };
257
+ }
258
+ }
259
+ return out;
260
+ }
261
+ var cloneInstalled = (src) => Object.fromEntries(Object.entries(src).map(([k, v]) => [k, { ...v }]));
262
+
263
+ class MockBackend {
264
+ name = "mock";
265
+ repo;
266
+ statePath;
267
+ initialInstalled;
268
+ ownedPaths;
269
+ state = { installed: {} };
270
+ loadPromise = null;
271
+ constructor(opts = {}) {
272
+ this.repo = opts.repo ?? DEFAULT_REPO;
273
+ this.statePath = opts.statePath ?? (opts.cwd ? join2(opts.cwd, STATE_FILE) : null);
274
+ this.initialInstalled = opts.initialInstalled ?? {};
275
+ this.ownedPaths = new Set(opts.ownedPaths ?? []);
276
+ }
277
+ async owns(path) {
278
+ return this.ownedPaths.has(path);
279
+ }
280
+ load() {
281
+ if (!this.loadPromise)
282
+ this.loadPromise = this.doLoad();
283
+ return this.loadPromise;
284
+ }
285
+ async doLoad() {
286
+ if (this.statePath && await exists(this.statePath)) {
287
+ try {
288
+ const raw = await readJson(this.statePath);
289
+ this.state = { installed: normalizeInstalled(raw.installed) };
290
+ return;
291
+ } catch {}
292
+ }
293
+ this.state = { installed: cloneInstalled(this.initialInstalled) };
294
+ }
295
+ async persist() {
296
+ if (!this.statePath)
297
+ return;
298
+ await writeText(this.statePath, JSON.stringify(this.state, null, 2) + `
299
+ `);
300
+ }
301
+ async installed(pkg) {
302
+ await this.load();
303
+ return this.state.installed[pkg]?.version ?? null;
304
+ }
305
+ async listInstalled() {
306
+ await this.load();
307
+ return Object.entries(this.state.installed).map(([name, { version, auto }]) => ({ name, version, arch: "amd64", auto })).sort((a, b) => a.name.localeCompare(b.name));
308
+ }
309
+ async candidates(pkg) {
310
+ return this.repo[pkg] ? [...this.repo[pkg]] : [];
311
+ }
312
+ async install(pkg, version) {
313
+ await this.load();
314
+ const avail = this.repo[pkg];
315
+ if (!avail || avail.length === 0) {
316
+ throw new Error(`package "${pkg}" has no installation candidate (unknown to mock repo)`);
317
+ }
318
+ let target = version ?? maxVersion(avail);
319
+ if (version && !avail.includes(version)) {
320
+ throw new Error(`version "${version}" of "${pkg}" is not available (have: ${avail.join(", ")})`);
321
+ }
322
+ this.state.installed[pkg] = { version: target, auto: false };
323
+ await this.persist();
324
+ return target;
325
+ }
326
+ async remove(pkg) {
327
+ await this.load();
328
+ delete this.state.installed[pkg];
329
+ await this.persist();
330
+ }
331
+ async refresh() {}
332
+ }
333
+
334
+ // src/backends/apt.ts
335
+ function usrmergeVariants(path) {
336
+ const merged = /^\/(bin|sbin|lib|lib32|lib64|libx32)\//;
337
+ if (path.startsWith("/usr/")) {
338
+ const stripped = path.slice(4);
339
+ if (merged.test(stripped))
340
+ return [path, stripped];
341
+ } else if (merged.test(path)) {
342
+ return [path, "/usr" + path];
343
+ }
344
+ return [path];
345
+ }
346
+ function privileged(cmd) {
347
+ const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
348
+ if (isRoot)
349
+ return cmd;
350
+ if (Bun.which("sudo"))
351
+ return ["sudo", ...cmd];
352
+ return cmd;
353
+ }
354
+ var APT_ENV = { ...process.env, DEBIAN_FRONTEND: "noninteractive" };
355
+ var runWithEnv = (cmd) => run(cmd, { env: APT_ENV });
356
+
357
+ class AptBackend {
358
+ name = "apt";
359
+ async installed(pkg) {
360
+ const res = await run([
361
+ "dpkg-query",
362
+ "-W",
363
+ "-f=${db:Status-Status} ${Version}",
364
+ pkg
365
+ ]);
366
+ if (res.code !== 0)
367
+ return null;
368
+ const [status, version] = res.stdout.trim().split(" ");
369
+ if (status !== "installed" || !version)
370
+ return null;
371
+ return version;
372
+ }
373
+ async listInstalled() {
374
+ const dq = await run([
375
+ "dpkg-query",
376
+ "-W",
377
+ "-f=${db:Status-Status}\t${Package}\t${Version}\t${Architecture}\n"
378
+ ]);
379
+ if (dq.code !== 0) {
380
+ throw new Error(`dpkg-query failed:
381
+ ${dq.stderr.trim() || dq.stdout.trim()}`);
382
+ }
383
+ const manual = new Set;
384
+ const mk = await run(["apt-mark", "showmanual"]);
385
+ if (mk.code === 0) {
386
+ for (const line of mk.stdout.split(`
387
+ `)) {
388
+ const name = line.trim();
389
+ if (name)
390
+ manual.add(name);
391
+ }
392
+ }
393
+ const out = [];
394
+ for (const line of dq.stdout.split(`
395
+ `)) {
396
+ if (!line)
397
+ continue;
398
+ const [status, name, version, arch] = line.split("\t");
399
+ if (status !== "installed" || !name || !version)
400
+ continue;
401
+ const isManual = manual.has(name) || (arch ? manual.has(`${name}:${arch}`) : false);
402
+ out.push({ name, version, arch: arch || undefined, auto: !isManual });
403
+ }
404
+ out.sort((a, b) => a.name.localeCompare(b.name));
405
+ return out;
406
+ }
407
+ async owns(path) {
408
+ for (const p of usrmergeVariants(path)) {
409
+ if ((await run(["dpkg", "-S", p])).code === 0)
410
+ return true;
411
+ }
412
+ return false;
413
+ }
414
+ async candidates(pkg) {
415
+ const res = await run(["apt-cache", "madison", pkg]);
416
+ if (res.code !== 0)
417
+ return [];
418
+ const versions = [];
419
+ for (const line of res.stdout.split(`
420
+ `)) {
421
+ const cols = line.split("|").map((c) => c.trim());
422
+ if (cols.length >= 2 && cols[1])
423
+ versions.push(cols[1]);
424
+ }
425
+ return [...new Set(versions)];
426
+ }
427
+ async install(pkg, version) {
428
+ const spec = version ? `${pkg}=${version}` : pkg;
429
+ const cmd = privileged(["apt-get", "install", "-y", "--allow-downgrades", spec]);
430
+ const res = await runWithEnv(cmd);
431
+ if (res.code !== 0) {
432
+ throw new Error(`apt-get install ${spec} failed:
433
+ ${res.stderr.trim() || res.stdout.trim()}`);
434
+ }
435
+ const now = await this.installed(pkg);
436
+ if (!now)
437
+ throw new Error(`installed ${spec} but dpkg does not report it as installed`);
438
+ return now;
439
+ }
440
+ async remove(pkg) {
441
+ const cmd = privileged(["apt-get", "remove", "-y", pkg]);
442
+ const res = await runWithEnv(cmd);
443
+ if (res.code !== 0) {
444
+ throw new Error(`apt-get remove ${pkg} failed:
445
+ ${res.stderr.trim() || res.stdout.trim()}`);
446
+ }
447
+ }
448
+ async refresh() {
449
+ const res = await runWithEnv(privileged(["apt-get", "update"]));
450
+ if (res.code !== 0) {
451
+ throw new Error(`apt-get update failed:
452
+ ${res.stderr.trim() || res.stdout.trim()}`);
453
+ }
454
+ }
455
+ }
456
+
457
+ // src/backend.ts
458
+ function selectBackend(opts) {
459
+ const chosen = opts.explicit ?? process.env.DEBRIEF_BACKEND ?? opts.fromLock ?? (whichSync("apt-get") ? "apt" : "mock");
460
+ switch (chosen) {
461
+ case "apt":
462
+ return new AptBackend;
463
+ case "mock":
464
+ return new MockBackend({ cwd: opts.cwd, initialInstalled: DEFAULT_SYSTEM });
465
+ default:
466
+ throw new Error(`unknown backend "${chosen}" (expected "apt" or "mock")`);
467
+ }
468
+ }
469
+
470
+ // src/store.ts
471
+ import { join as join3 } from "node:path";
472
+ var MANIFEST_FILE = "debrief.json";
473
+ var LOCK_FILE = "debrief.lock";
474
+ var LOCKFILE_VERSION = 1;
475
+ function manifestPath(dir) {
476
+ return join3(dir, MANIFEST_FILE);
477
+ }
478
+ function lockPath(dir) {
479
+ return join3(dir, LOCK_FILE);
480
+ }
481
+ function emptyManifest() {
482
+ return { packages: {} };
483
+ }
484
+ async function readManifest(dir) {
485
+ if (!await exists(manifestPath(dir)))
486
+ return null;
487
+ const data = await readJson(manifestPath(dir));
488
+ return {
489
+ name: data.name,
490
+ description: data.description,
491
+ packages: data.packages ?? {}
492
+ };
493
+ }
494
+ async function requireManifest(dir) {
495
+ const m = await readManifest(dir);
496
+ if (!m) {
497
+ throw new Error(`no ${MANIFEST_FILE} found in ${dir} — run \`debrief init\` first`);
498
+ }
499
+ return m;
500
+ }
501
+ async function writeManifest(dir, manifest) {
502
+ const sorted = {};
503
+ for (const key of Object.keys(manifest.packages).sort()) {
504
+ sorted[key] = manifest.packages[key];
505
+ }
506
+ const out = {
507
+ ...manifest.name !== undefined ? { name: manifest.name } : {},
508
+ ...manifest.description !== undefined ? { description: manifest.description } : {},
509
+ packages: sorted
510
+ };
511
+ await writeText(manifestPath(dir), JSON.stringify(out, null, 2) + `
512
+ `);
513
+ }
514
+ async function readLock(dir) {
515
+ if (!await exists(lockPath(dir)))
516
+ return null;
517
+ return readJson(lockPath(dir));
518
+ }
519
+ async function writeLock(dir, lock) {
520
+ const sorted = {};
521
+ for (const key of Object.keys(lock.packages).sort()) {
522
+ sorted[key] = lock.packages[key];
523
+ }
524
+ const out = { ...lock, packages: sorted };
525
+ if (lock.binaries !== undefined) {
526
+ const sortedBins = {};
527
+ for (const key of Object.keys(lock.binaries).sort()) {
528
+ sortedBins[key] = lock.binaries[key];
529
+ }
530
+ out.binaries = sortedBins;
531
+ } else {
532
+ delete out.binaries;
533
+ }
534
+ await writeText(lockPath(dir), JSON.stringify(out, null, 2) + `
535
+ `);
536
+ }
537
+
538
+ // src/commands.ts
539
+ import { basename } from "node:path";
540
+
541
+ // src/resolve.ts
542
+ async function inspect(pkg, constraint, backend, lock) {
543
+ const [installed, candidates] = await Promise.all([
544
+ backend.installed(pkg),
545
+ backend.candidates(pkg)
546
+ ]);
547
+ const parsed = parseConstraint(constraint);
548
+ const target = bestMatch(constraint, candidates) ?? null;
549
+ const latest = maxVersion(candidates) ?? null;
550
+ const satisfied = installed !== null && parsed.test(installed);
551
+ const locked = lock?.packages[pkg]?.version ?? null;
552
+ const lockedUsable = locked !== null && candidates.includes(locked) && parsed.test(locked);
553
+ const resolvedTarget = lockedUsable ? locked : target;
554
+ return { pkg, constraint, installed, locked, target, latest, satisfied, resolvedTarget };
555
+ }
556
+ async function inspectAll(manifest, backend, lock) {
557
+ const entries = Object.entries(manifest.packages);
558
+ const rows = await Promise.all(entries.map(([pkg, c]) => inspect(pkg, c, backend, lock)));
559
+ return rows.sort((a, b) => a.pkg.localeCompare(b.pkg));
560
+ }
561
+ function classify(row) {
562
+ if (row.installed === null)
563
+ return row.target ? "missing" : "unavailable";
564
+ if (row.satisfied)
565
+ return "satisfied";
566
+ return "drifted";
567
+ }
568
+
569
+ // src/collector.ts
570
+ import { readdir, realpath, stat } from "node:fs/promises";
571
+ import { join as join4 } from "node:path";
572
+ import { createHash } from "node:crypto";
573
+ function defaultBinDirs() {
574
+ const home = process.env.HOME ?? "";
575
+ return [
576
+ "/usr/local/bin",
577
+ "/usr/local/sbin",
578
+ "/opt/bin",
579
+ home && `${home}/.local/bin`,
580
+ home && `${home}/bin`
581
+ ].filter((d) => Boolean(d));
582
+ }
583
+ var HASH_MAX_BYTES = 256 * 1024 * 1024;
584
+ async function sha256File(path, size) {
585
+ if (size > HASH_MAX_BYTES)
586
+ return;
587
+ try {
588
+ const bytes = await readBytes(path);
589
+ return createHash("sha256").update(bytes).digest("hex");
590
+ } catch {
591
+ return;
592
+ }
593
+ }
594
+ async function probeVersion(path) {
595
+ const res = await run([path, "--version"], { timeoutMs: 3000 });
596
+ const line = (res.stdout + res.stderr).split(`
597
+ `).find((l) => l.trim())?.trim() ?? "";
598
+ if (!line)
599
+ return;
600
+ const m = /\d+\.\d+(?:\.\d+)?(?:[-+.\w~]*)?/.exec(line);
601
+ return m?.[0] ?? line.slice(0, 60);
602
+ }
603
+ async function collectBinaries(opts) {
604
+ const wantHash = opts.hash !== false;
605
+ const out = {};
606
+ for (const dir of opts.dirs) {
607
+ let names;
608
+ try {
609
+ names = await readdir(dir);
610
+ } catch {
611
+ continue;
612
+ }
613
+ for (const name of names) {
614
+ if (name in out)
615
+ continue;
616
+ const full = join4(dir, name);
617
+ let st;
618
+ try {
619
+ st = await stat(full);
620
+ } catch {
621
+ continue;
622
+ }
623
+ if (!st.isFile())
624
+ continue;
625
+ if ((st.mode & 73) === 0)
626
+ continue;
627
+ let real = full;
628
+ try {
629
+ real = await realpath(full);
630
+ } catch {}
631
+ if (await opts.backend.owns(real))
632
+ continue;
633
+ const entry = { path: full, size: st.size };
634
+ if (wantHash)
635
+ entry.sha256 = await sha256File(real, st.size);
636
+ if (opts.probeVersions)
637
+ entry.version = await probeVersion(full);
638
+ out[name] = entry;
639
+ }
640
+ }
641
+ return out;
642
+ }
643
+
644
+ // src/table.ts
645
+ var stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
646
+ function table(headers, rows) {
647
+ const all = [headers, ...rows];
648
+ const widths = headers.map((_, col) => Math.max(...all.map((r) => stripAnsi(r[col] ?? "").length)));
649
+ const fmtRow = (r) => r.map((cell, col) => {
650
+ const pad = widths[col] - stripAnsi(cell ?? "").length;
651
+ return (cell ?? "") + " ".repeat(Math.max(0, pad));
652
+ }).join(" ").trimEnd();
653
+ return [fmtRow(headers), ...rows.map(fmtRow)].join(`
654
+ `);
655
+ }
656
+
657
+ // src/log.ts
658
+ var enabled = process.stdout.isTTY && !process.env.NO_COLOR;
659
+ var wrap = (code) => (s) => enabled ? `\x1B[${code}m${s}\x1B[0m` : String(s);
660
+ var c = {
661
+ bold: wrap(1),
662
+ dim: wrap(2),
663
+ red: wrap(31),
664
+ green: wrap(32),
665
+ yellow: wrap(33),
666
+ blue: wrap(34),
667
+ cyan: wrap(36),
668
+ gray: wrap(90)
669
+ };
670
+ var sym = {
671
+ add: c.green("+"),
672
+ ok: c.dim("="),
673
+ warn: c.yellow("!"),
674
+ err: c.red("✗"),
675
+ arrow: c.gray("→")
676
+ };
677
+ function info(msg) {
678
+ console.log(msg);
679
+ }
680
+ function warn(msg) {
681
+ console.warn(`${c.yellow("warning")} ${msg}`);
682
+ }
683
+ function error(msg) {
684
+ console.error(`${c.red("error")} ${msg}`);
685
+ }
686
+
687
+ // src/commands.ts
688
+ function parseSpec(spec) {
689
+ const eq2 = spec.indexOf("=");
690
+ if (eq2 !== -1) {
691
+ const pkg = spec.slice(0, eq2);
692
+ const version = spec.slice(eq2 + 1);
693
+ return { pkg, constraint: version, exactVersion: version };
694
+ }
695
+ const at = spec.indexOf("@");
696
+ if (at !== -1) {
697
+ return { pkg: spec.slice(0, at), constraint: spec.slice(at + 1) };
698
+ }
699
+ return { pkg: spec, constraint: null };
700
+ }
701
+ function defaultConstraint(version) {
702
+ const { epoch, upstream } = parseVersion(version);
703
+ const prefix = epoch > 0 ? `${epoch}:` : "";
704
+ return `^${prefix}${upstream}`;
705
+ }
706
+ async function loadLockOrNew(ctx) {
707
+ const existing = await readLock(ctx.cwd);
708
+ if (existing)
709
+ return existing;
710
+ return { lockfileVersion: LOCKFILE_VERSION, backend: ctx.backend.name, packages: {} };
711
+ }
712
+ async function cmdInit(ctx, opts) {
713
+ const existing = await readManifest(ctx.cwd);
714
+ if (existing) {
715
+ warn(`${MANIFEST_FILE} already exists — leaving it untouched`);
716
+ return 0;
717
+ }
718
+ const manifest = emptyManifest();
719
+ manifest.name = opts.name ?? basename(ctx.cwd);
720
+ if (ctx.dryRun) {
721
+ info(`${sym.arrow} would create ${MANIFEST_FILE} (name: ${manifest.name})`);
722
+ return 0;
723
+ }
724
+ await writeManifest(ctx.cwd, manifest);
725
+ info(`${sym.add} created ${c.bold(MANIFEST_FILE)} (name: ${manifest.name})`);
726
+ info(`${c.dim("add packages with:")} debrief add <package>`);
727
+ return 0;
728
+ }
729
+ async function cmdAdd(ctx, specs) {
730
+ if (specs.length === 0) {
731
+ error("nothing to add — usage: debrief add <package>[@constraint|=version] ...");
732
+ return 1;
733
+ }
734
+ const manifest = await readManifest(ctx.cwd) ?? emptyManifest();
735
+ const lock = await loadLockOrNew(ctx);
736
+ lock.backend = ctx.backend.name;
737
+ let failures = 0;
738
+ for (const spec of specs.map(parseSpec)) {
739
+ const constraint = spec.constraint ?? "*";
740
+ const row = await inspect(spec.pkg, constraint, ctx.backend, lock);
741
+ if (row.latest === null && row.installed === null) {
742
+ error(`${spec.pkg}: no installation candidate (unknown package)`);
743
+ failures++;
744
+ continue;
745
+ }
746
+ const wanted = spec.exactVersion ?? row.target ?? row.latest;
747
+ if (spec.constraint && row.target === null && !spec.exactVersion) {
748
+ error(`${spec.pkg}: no available version satisfies "${constraint}" (latest is ${row.latest})`);
749
+ failures++;
750
+ continue;
751
+ }
752
+ const alreadyGood = row.installed !== null && row.satisfied && !spec.exactVersion;
753
+ let installedVersion = row.installed;
754
+ if (alreadyGood) {
755
+ info(`${sym.ok} ${spec.pkg} ${row.installed} ${c.dim(`(already satisfies ${constraint})`)}`);
756
+ } else if (ctx.dryRun) {
757
+ info(`${sym.arrow} would install ${spec.pkg} ${c.bold(String(wanted))}`);
758
+ installedVersion = wanted ?? row.installed;
759
+ } else {
760
+ try {
761
+ installedVersion = await ctx.backend.install(spec.pkg, spec.exactVersion ?? wanted ?? undefined);
762
+ info(`${sym.add} ${spec.pkg} ${c.bold(installedVersion)}`);
763
+ } catch (e) {
764
+ error(`${spec.pkg}: ${e.message}`);
765
+ failures++;
766
+ continue;
767
+ }
768
+ }
769
+ const finalConstraint = spec.constraint ?? (installedVersion ? defaultConstraint(installedVersion) : "*");
770
+ manifest.packages[spec.pkg] = finalConstraint;
771
+ if (installedVersion) {
772
+ lock.packages[spec.pkg] = { version: installedVersion, constraint: finalConstraint };
773
+ }
774
+ }
775
+ if (!ctx.dryRun) {
776
+ await writeManifest(ctx.cwd, manifest);
777
+ await writeLock(ctx.cwd, lock);
778
+ }
779
+ return failures > 0 ? 1 : 0;
780
+ }
781
+ async function cmdRemove(ctx, pkgs, opts) {
782
+ if (pkgs.length === 0) {
783
+ error("nothing to remove — usage: debrief remove <package> ...");
784
+ return 1;
785
+ }
786
+ const manifest = await requireManifest(ctx.cwd);
787
+ const lock = await loadLockOrNew(ctx);
788
+ let failures = 0;
789
+ for (const pkg of pkgs) {
790
+ const inManifest = pkg in manifest.packages;
791
+ const installed = opts.saveOnly ? null : await ctx.backend.installed(pkg);
792
+ if (!inManifest && !installed) {
793
+ warn(`${pkg}: not in ${MANIFEST_FILE} and not installed — skipping`);
794
+ continue;
795
+ }
796
+ if (!opts.saveOnly && installed) {
797
+ if (ctx.dryRun) {
798
+ info(`${sym.arrow} would uninstall ${pkg} ${c.dim(installed)}`);
799
+ } else {
800
+ try {
801
+ await ctx.backend.remove(pkg);
802
+ info(`${c.red("-")} ${pkg} ${c.dim(`(was ${installed})`)}`);
803
+ } catch (e) {
804
+ error(`${pkg}: ${e.message}`);
805
+ failures++;
806
+ continue;
807
+ }
808
+ }
809
+ }
810
+ delete manifest.packages[pkg];
811
+ delete lock.packages[pkg];
812
+ if (opts.saveOnly)
813
+ info(`${c.red("-")} ${pkg} ${c.dim("(removed from manifest only)")}`);
814
+ }
815
+ if (!ctx.dryRun) {
816
+ await writeManifest(ctx.cwd, manifest);
817
+ await writeLock(ctx.cwd, lock);
818
+ }
819
+ return failures > 0 ? 1 : 0;
820
+ }
821
+ async function cmdInstall(ctx) {
822
+ const manifest = await requireManifest(ctx.cwd);
823
+ const lock = await loadLockOrNew(ctx);
824
+ lock.backend = ctx.backend.name;
825
+ const rows = await inspectAll(manifest, ctx.backend, lock);
826
+ if (rows.length === 0) {
827
+ info(`${c.dim("no packages declared in")} ${MANIFEST_FILE}`);
828
+ return 0;
829
+ }
830
+ let installed = 0;
831
+ let failures = 0;
832
+ let drifted = 0;
833
+ for (const row of rows) {
834
+ const state = classify(row);
835
+ switch (state) {
836
+ case "missing": {
837
+ const version = row.resolvedTarget;
838
+ const pinned = row.locked !== null && version === row.locked;
839
+ const mark = pinned ? c.dim(" ↺") : "";
840
+ if (ctx.dryRun) {
841
+ info(`${sym.arrow} would install ${row.pkg} ${c.bold(String(version))}${mark}`);
842
+ if (version)
843
+ lock.packages[row.pkg] = { version, constraint: row.constraint };
844
+ installed++;
845
+ break;
846
+ }
847
+ try {
848
+ const v = await ctx.backend.install(row.pkg, version ?? undefined);
849
+ info(`${sym.add} ${row.pkg} ${c.bold(v)}${mark}`);
850
+ lock.packages[row.pkg] = { version: v, constraint: row.constraint };
851
+ installed++;
852
+ } catch (e) {
853
+ error(`${row.pkg}: ${e.message}`);
854
+ failures++;
855
+ }
856
+ break;
857
+ }
858
+ case "satisfied": {
859
+ info(`${sym.ok} ${row.pkg} ${c.dim(row.installed)}`);
860
+ lock.packages[row.pkg] = { version: row.installed, constraint: row.constraint };
861
+ break;
862
+ }
863
+ case "drifted": {
864
+ warn(`${row.pkg} ${row.installed} does not satisfy ${c.bold(row.constraint)} ` + `${c.dim(`(run \`debrief add ${row.pkg}@${row.constraint}\` to change it)`)}`);
865
+ drifted++;
866
+ break;
867
+ }
868
+ case "unavailable": {
869
+ error(`${row.pkg}: no candidate satisfies ${c.bold(row.constraint)}`);
870
+ failures++;
871
+ break;
872
+ }
873
+ }
874
+ }
875
+ if (!ctx.dryRun)
876
+ await writeLock(ctx.cwd, lock);
877
+ const parts = [];
878
+ parts.push(`${installed} installed`);
879
+ if (drifted)
880
+ parts.push(c.yellow(`${drifted} drifted`));
881
+ if (failures)
882
+ parts.push(c.red(`${failures} failed`));
883
+ info(c.dim(`— ${parts.join(", ")}`));
884
+ return failures > 0 ? 1 : 0;
885
+ }
886
+ function statusLabel(row) {
887
+ switch (classify(row)) {
888
+ case "missing":
889
+ return c.blue("missing");
890
+ case "satisfied":
891
+ return c.green("ok");
892
+ case "drifted":
893
+ return c.yellow("drift");
894
+ case "unavailable":
895
+ return c.red("no-candidate");
896
+ }
897
+ }
898
+ async function cmdList(ctx) {
899
+ const manifest = await requireManifest(ctx.cwd);
900
+ const lock = await readLock(ctx.cwd);
901
+ const rows = await inspectAll(manifest, ctx.backend, lock);
902
+ if (rows.length === 0) {
903
+ info(`${c.dim("no packages declared. add one with:")} debrief add <package>`);
904
+ return 0;
905
+ }
906
+ const body = rows.map((r) => {
907
+ const newer = r.latest && r.installed && r.latest !== r.installed ? c.dim(`(latest ${r.latest})`) : "";
908
+ return [
909
+ c.bold(r.pkg),
910
+ r.constraint,
911
+ r.installed ?? c.dim("—"),
912
+ statusLabel(r),
913
+ newer
914
+ ];
915
+ });
916
+ info(table(["PACKAGE", "CONSTRAINT", "INSTALLED", "STATUS", ""], body));
917
+ info(c.dim(`
918
+ backend: ${ctx.backend.name} • ${rows.length} package(s) declared`));
919
+ return 0;
920
+ }
921
+ var byName = (a, b) => a.name.localeCompare(b.name);
922
+ async function cmdCollect(ctx, opts) {
923
+ const inventory = await ctx.backend.listInstalled();
924
+ if (inventory.length === 0) {
925
+ warn("the backend reports no installed packages — nothing to collect");
926
+ return 0;
927
+ }
928
+ const manual = inventory.filter((p) => !p.auto);
929
+ const manifest = opts.replace ? emptyManifest() : await readManifest(ctx.cwd) ?? emptyManifest();
930
+ manifest.name ??= basename(ctx.cwd);
931
+ let added = 0;
932
+ for (const p of manual) {
933
+ if (p.name in manifest.packages && !opts.replace)
934
+ continue;
935
+ manifest.packages[p.name] = opts.pin ? p.version : "*";
936
+ added++;
937
+ }
938
+ const lock = { lockfileVersion: LOCKFILE_VERSION, backend: ctx.backend.name, packages: {} };
939
+ for (const p of inventory) {
940
+ lock.packages[p.name] = {
941
+ version: p.version,
942
+ constraint: manifest.packages[p.name] ?? "*",
943
+ ...p.arch ? { arch: p.arch } : {},
944
+ ...p.auto ? { auto: true } : {}
945
+ };
946
+ }
947
+ let bins = {};
948
+ if (opts.binaries !== false) {
949
+ bins = await collectBinaries({
950
+ dirs: opts.dirs ?? defaultBinDirs(),
951
+ backend: ctx.backend,
952
+ probeVersions: opts.probeVersions
953
+ });
954
+ lock.binaries = bins;
955
+ }
956
+ const binCount = Object.keys(bins).length;
957
+ if (ctx.dryRun) {
958
+ info(`${sym.arrow} would record ${c.bold(String(manual.length))} manual package(s) in ${MANIFEST_FILE} ` + `(${added} new), ${c.bold(String(inventory.length))} total + ${c.bold(String(binCount))} loose binaries in ${LOCK_FILE}`);
959
+ return 0;
960
+ }
961
+ await writeManifest(ctx.cwd, manifest);
962
+ await writeLock(ctx.cwd, lock);
963
+ info(`${sym.add} collected ${c.bold(String(inventory.length))} installed package(s)`);
964
+ info(` ${c.dim("→ manifest:")} ${manual.length} manual ${c.dim(`(+${added} new)`)} ` + `${c.dim("→ lock:")} ${inventory.length} total ${c.dim(`(incl. ${inventory.length - manual.length} deps)`)}`);
965
+ if (opts.binaries !== false) {
966
+ info(` ${c.dim("→ loose binaries (non-dpkg):")} ${binCount}`);
967
+ }
968
+ return 0;
969
+ }
970
+ async function cmdStatus(ctx, opts = {}) {
971
+ const lock = await readLock(ctx.cwd);
972
+ if (!lock) {
973
+ error(`no snapshot found — run \`debrief collect\` first`);
974
+ return 1;
975
+ }
976
+ const live = await ctx.backend.listInstalled();
977
+ const liveNames = new Set(live.map((p) => p.name));
978
+ const snap = lock.packages;
979
+ const added = [];
980
+ const changed = [];
981
+ for (const p of live) {
982
+ const s = snap[p.name];
983
+ if (!s)
984
+ added.push(p);
985
+ else if (s.version !== p.version) {
986
+ changed.push({ name: p.name, from: s.version, to: p.version, auto: p.auto });
987
+ }
988
+ }
989
+ const removed = Object.entries(snap).filter(([name]) => !liveNames.has(name)).map(([name, e]) => ({ name, version: e.version, auto: e.auto ?? false }));
990
+ const binAdded = [];
991
+ const binChanged = [];
992
+ const binRemoved = [];
993
+ if (lock.binaries) {
994
+ const liveBins = await collectBinaries({ dirs: opts.dirs ?? defaultBinDirs(), backend: ctx.backend });
995
+ const snapBins = lock.binaries;
996
+ for (const [name, e] of Object.entries(liveBins)) {
997
+ const s = snapBins[name];
998
+ if (!s)
999
+ binAdded.push(`${name} ${e.path}`);
1000
+ else if (s.sha256 && e.sha256 && s.sha256 !== e.sha256)
1001
+ binChanged.push(`${name} ${e.path}`);
1002
+ }
1003
+ for (const name of Object.keys(snapBins)) {
1004
+ if (!(name in liveBins))
1005
+ binRemoved.push(`${name} ${snapBins[name].path}`);
1006
+ }
1007
+ }
1008
+ const nothing = added.length + changed.length + removed.length + binAdded.length + binChanged.length + binRemoved.length === 0;
1009
+ if (nothing) {
1010
+ info(c.green("in sync — the live system matches the snapshot"));
1011
+ return 0;
1012
+ }
1013
+ const tag = (auto) => auto ? c.dim(" (dep)") : "";
1014
+ for (const p of added.sort(byName))
1015
+ info(`${c.green("+")} ${p.name} ${p.version}${tag(p.auto)}`);
1016
+ for (const ch of changed.sort(byName)) {
1017
+ info(`${c.yellow("~")} ${ch.name} ${c.dim(ch.from)} ${sym.arrow} ${ch.to}${tag(ch.auto)}`);
1018
+ }
1019
+ for (const p of removed.sort(byName))
1020
+ info(`${c.red("-")} ${p.name} ${c.dim(p.version)}${tag(p.auto)}`);
1021
+ if (binAdded.length + binChanged.length + binRemoved.length > 0) {
1022
+ info(c.dim(`
1023
+ non-dpkg binaries (curl|sh / manual / brew / ...):`));
1024
+ for (const b of binAdded.sort())
1025
+ info(`${c.green("+")} ${b}`);
1026
+ for (const b of binChanged.sort())
1027
+ info(`${c.yellow("~")} ${b} ${c.dim("(contents changed)")}`);
1028
+ for (const b of binRemoved.sort())
1029
+ info(`${c.red("-")} ${b}`);
1030
+ }
1031
+ const pkgLine = `${added.length} added ~${changed.length} changed -${removed.length} removed`;
1032
+ const binLine = lock.binaries !== undefined ? ` binaries: +${binAdded.length} ~${binChanged.length} -${binRemoved.length}` : "";
1033
+ info(c.dim(`
1034
+ packages: +${pkgLine}${binLine} (run \`debrief collect\` to update the snapshot)`));
1035
+ return 0;
1036
+ }
1037
+ async function cmdOutdated(ctx) {
1038
+ const manifest = await requireManifest(ctx.cwd);
1039
+ const lock = await readLock(ctx.cwd);
1040
+ const rows = await inspectAll(manifest, ctx.backend, lock);
1041
+ const outdated = rows.filter((r) => r.installed !== null && r.latest !== null && r.installed !== r.latest);
1042
+ if (outdated.length === 0) {
1043
+ info(c.green("everything is up to date"));
1044
+ return 0;
1045
+ }
1046
+ const body = outdated.map((r) => [
1047
+ c.bold(r.pkg),
1048
+ r.installed ?? c.dim("—"),
1049
+ r.target && r.target !== r.installed ? c.green(r.target) : c.dim(r.target ?? "—"),
1050
+ c.cyan(r.latest)
1051
+ ]);
1052
+ info(table(["PACKAGE", "INSTALLED", "WANTED", "LATEST"], body));
1053
+ info(c.dim(`
1054
+ WANTED = newest within your constraint • LATEST = newest in the repo
1055
+ ` + `bump a constraint with: debrief add <pkg>@<constraint>`));
1056
+ return 0;
1057
+ }
1058
+
1059
+ // src/cli.ts
1060
+ var VERSION = "0.1.0";
1061
+ var HELP = `${c.bold("debrief")} — a package.json for your Debian packages
1062
+
1063
+ ${c.bold("USAGE")}
1064
+ debrief <command> [args] [options]
1065
+
1066
+ ${c.bold("COMMANDS")}
1067
+ ${c.bold("collect")} snapshot dpkg packages + loose PATH binaries into the lock
1068
+ ${c.bold("status")} diff the live system against the snapshot
1069
+ list show declared packages and their status (alias: ls)
1070
+ outdated show packages with newer versions available
1071
+ init create an empty debrief.json
1072
+ add <pkg[@ver|=ver]> … install package(s) and record them in the manifest
1073
+ remove <pkg> … uninstall package(s) and drop them from the manifest
1074
+ install install everything in the manifest that is missing
1075
+ help show this help
1076
+
1077
+ ${c.bold("COLLECT / STATUS OPTIONS")}
1078
+ --pin (collect) record exact installed versions, not "*"
1079
+ --replace (collect) overwrite the manifest instead of merging
1080
+ --no-binaries skip scanning PATH for loose (non-dpkg) binaries
1081
+ --probe-versions run \`<bin> --version\` on loose binaries (executes them)
1082
+ --scan-dirs a:b:c override which dirs to scan for loose binaries
1083
+
1084
+ ${c.bold("VERSION SPECS")} (npm-flavored, resolved against real Debian versions)
1085
+ nginx latest candidate; manifest gets ^<installed>
1086
+ nginx@^1.24 >= 1.24 and < 2 (caret: allow non-major)
1087
+ nginx@~1.24 >= 1.24 and < 1.25 (tilde: pin the minor)
1088
+ nginx@'>=1.22 <1.26' an AND-ed range
1089
+ nginx=1.24.0-1 an exact Debian version
1090
+
1091
+ ${c.bold("OPTIONS")}
1092
+ --backend <mock|apt> force a backend (default: from lockfile, else auto)
1093
+ -n, --dry-run show what would happen without changing anything
1094
+ -y, --yes assume yes (non-interactive)
1095
+ --save-only (remove) drop from manifest without uninstalling
1096
+ --name <name> (init) set the project name
1097
+ --cwd <dir> run as if in <dir>
1098
+ -V, --version print version
1099
+ -h, --help show this help
1100
+
1101
+ ${c.dim("On a machine without apt (e.g. NixOS) the mock backend is used automatically.")}`;
1102
+ var VALUE_FLAGS = new Set(["backend", "cwd", "name", "scan-dirs"]);
1103
+ var ALIASES = {
1104
+ n: "dry-run",
1105
+ y: "yes",
1106
+ h: "help",
1107
+ V: "version"
1108
+ };
1109
+ function parse(argv) {
1110
+ const positionals = [];
1111
+ const flags = {};
1112
+ for (let i = 0;i < argv.length; i++) {
1113
+ const tok = argv[i];
1114
+ if (tok.startsWith("--")) {
1115
+ const body = tok.slice(2);
1116
+ const eq2 = body.indexOf("=");
1117
+ if (eq2 !== -1) {
1118
+ flags[body.slice(0, eq2)] = body.slice(eq2 + 1);
1119
+ } else if (VALUE_FLAGS.has(body)) {
1120
+ flags[body] = argv[++i] ?? "";
1121
+ } else {
1122
+ flags[body] = true;
1123
+ }
1124
+ } else if (tok.startsWith("-") && tok.length > 1) {
1125
+ const name = ALIASES[tok.slice(1)] ?? tok.slice(1);
1126
+ if (VALUE_FLAGS.has(name))
1127
+ flags[name] = argv[++i] ?? "";
1128
+ else
1129
+ flags[name] = true;
1130
+ } else {
1131
+ positionals.push(tok);
1132
+ }
1133
+ }
1134
+ const [command = null, ...rest] = positionals;
1135
+ return { command, positionals: rest, flags };
1136
+ }
1137
+ function scanDirs(flags) {
1138
+ const v = flags["scan-dirs"];
1139
+ return typeof v === "string" ? v.split(":").filter(Boolean) : undefined;
1140
+ }
1141
+ async function main(argv) {
1142
+ const { command, positionals, flags } = parse(argv);
1143
+ if (flags.version) {
1144
+ console.log(VERSION);
1145
+ return 0;
1146
+ }
1147
+ if (flags.help || command === null || command === "help") {
1148
+ console.log(HELP);
1149
+ return 0;
1150
+ }
1151
+ const cwd = typeof flags.cwd === "string" ? flags.cwd : process.cwd();
1152
+ const dryRun = !!flags["dry-run"];
1153
+ const yes = !!flags.yes;
1154
+ const lock = await readLock(cwd).catch(() => null);
1155
+ const backend = selectBackend({
1156
+ explicit: typeof flags.backend === "string" ? flags.backend : undefined,
1157
+ fromLock: lock?.backend,
1158
+ cwd
1159
+ });
1160
+ const ctx = { cwd, backend, dryRun, yes };
1161
+ try {
1162
+ switch (command) {
1163
+ case "collect":
1164
+ case "import":
1165
+ case "freeze":
1166
+ return await cmdCollect(ctx, {
1167
+ pin: !!flags.pin,
1168
+ replace: !!flags.replace,
1169
+ binaries: !flags["no-binaries"],
1170
+ probeVersions: !!flags["probe-versions"],
1171
+ dirs: scanDirs(flags)
1172
+ });
1173
+ case "status":
1174
+ case "diff":
1175
+ return await cmdStatus(ctx, { dirs: scanDirs(flags) });
1176
+ case "init":
1177
+ return await cmdInit(ctx, { name: typeof flags.name === "string" ? flags.name : undefined });
1178
+ case "add":
1179
+ return await cmdAdd(ctx, positionals);
1180
+ case "remove":
1181
+ case "rm":
1182
+ case "uninstall":
1183
+ return await cmdRemove(ctx, positionals, { saveOnly: !!flags["save-only"] });
1184
+ case "install":
1185
+ case "i":
1186
+ if (positionals.length > 0) {
1187
+ error(`\`install\` takes no package arguments — use \`debrief add ${positionals.join(" ")}\``);
1188
+ return 1;
1189
+ }
1190
+ return await cmdInstall(ctx);
1191
+ case "list":
1192
+ case "ls":
1193
+ return await cmdList(ctx);
1194
+ case "outdated":
1195
+ return await cmdOutdated(ctx);
1196
+ default:
1197
+ error(`unknown command "${command}" — run \`debrief help\``);
1198
+ return 1;
1199
+ }
1200
+ } catch (e) {
1201
+ error(e.message);
1202
+ return 1;
1203
+ }
1204
+ }
1205
+
1206
+ // bin/debrief.ts
1207
+ process.exit(await main(process.argv.slice(2)));