@orbytes/astrolab 0.4.0-next.1 → 0.4.0-next.2

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 (50) hide show
  1. package/README.md +184 -84
  2. package/bin/pin-gallery.mjs +53 -19
  3. package/defaults.mjs +7 -20
  4. package/docs/PIN-CONTRACT.md +76 -10
  5. package/docs/PIN.md +93 -23
  6. package/index.d.ts +1 -7
  7. package/index.mjs +14 -81
  8. package/package.json +2 -2
  9. package/src/Home.astro +7 -8
  10. package/src/LabHead.astro +1 -1
  11. package/src/chrome/ActionsMenu.astro +97 -0
  12. package/src/chrome/ComponentCard.astro +9 -2
  13. package/src/chrome/Nav.astro +36 -10
  14. package/src/chrome/Panel.astro +17 -4
  15. package/src/chrome/Properties.astro +104 -0
  16. package/src/chrome/SectionsTree.astro +128 -0
  17. package/src/chrome/Shell.astro +20 -6
  18. package/src/chrome/StoryView.astro +103 -162
  19. package/src/chrome/Tree.astro +56 -53
  20. package/src/chrome/ViewportControls.astro +136 -61
  21. package/src/chrome/ViewportStage.astro +26 -3
  22. package/src/chrome/icons.ts +9 -0
  23. package/src/chrome/marks-client.ts +26 -53
  24. package/src/chrome/model.ts +14 -0
  25. package/src/chrome/navbar-client.ts +324 -0
  26. package/src/chrome/params-client.ts +434 -0
  27. package/src/chrome/pins-data.ts +42 -9
  28. package/src/chrome/shell-client.ts +99 -3
  29. package/src/chrome/trees.ts +112 -7
  30. package/src/chrome/viewport-client.ts +68 -242
  31. package/src/chrome/views/Assets.astro +21 -6
  32. package/src/chrome/views/Pages.astro +90 -54
  33. package/src/chrome/views/Placeholder.astro +3 -3
  34. package/src/chrome/views/Tasks.astro +12 -40
  35. package/src/core/LICENSE-astrobook +5 -0
  36. package/src/core/utils/kebab-case.ts +2 -2
  37. package/src/pin/board.mjs +25 -15
  38. package/src/pin/index.mjs +34 -20
  39. package/src/pin/tickets.mjs +6 -5
  40. package/src/pin/toolbar.js +81 -3
  41. package/src/shell/Browse.astro +35 -10
  42. package/src/shell/lab-index.ts +5 -4
  43. package/src/shell/lab-params.ts +113 -6
  44. package/src/shell/live-files.mjs +212 -10
  45. package/src/shell/marks.mjs +17 -41
  46. package/src/ui/components/preview-layout.astro +17 -0
  47. package/src/ui/components/theme-script.astro +4 -3
  48. package/src/ui/lab.css +2167 -566
  49. package/virtual.d.ts +0 -4
  50. package/bin/lab-cull.mjs +0 -401
package/virtual.d.ts CHANGED
@@ -6,7 +6,6 @@ declare module "virtual:orbytes-lab/config.mjs" {
6
6
  id: string;
7
7
  label: string;
8
8
  responsive: boolean;
9
- cullable: boolean;
10
9
  }
11
10
  export interface LabConfig {
12
11
  directory: string;
@@ -18,11 +17,8 @@ declare module "virtual:orbytes-lab/config.mjs" {
18
17
  feedbucketKey: string | null;
19
18
  tiers: LabTier[];
20
19
  sectionsTier: string | null;
21
- cullTier: string | null;
22
20
  responsiveDir: string | null;
23
- cullDir: string | null;
24
21
  responsiveFile: string;
25
- cullFile: string;
26
22
  /**
27
23
  * The pin board, when it is running: dev only, and only when the board is on and found a git
28
24
  * repository. Null in every build. Set by the pin half through the integration's shared state
package/bin/lab-cull.mjs DELETED
@@ -1,401 +0,0 @@
1
- #!/usr/bin/env node
2
- // Turns the lab's cull marks into deletions — and never from the browser.
3
- //
4
- // You mark lab items for deletion in the sidebar; the dev-only API saves the marks to
5
- // <directory>/cull.json. This script is the only thing that deletes: it reads the marks, works
6
- // out what can go, prints the plan, and removes files only when told to.
7
- //
8
- // orbytes-lab-cull dry run — print the plan, remove nothing
9
- // orbytes-lab-cull --json the same plan as JSON
10
- // orbytes-lab-cull --yes perform it: `git rm` (staged, NOT committed), clear the
11
- // processed marks, print the build-log line to paste
12
- // orbytes-lab-cull --dir=src/lab where the lab lives, when it is not the default
13
- //
14
- // exit 0 plan clean · 2 blockers · 1 error
15
- //
16
- // IT RUNS IN THE CONSUMER'S REPO, NOT ITS OWN. The root is `process.cwd()` — never this file's
17
- // location, which since the lab became a package is somewhere under node_modules. Everything it
18
- // reads is derived from that root plus the shared defaults in ../defaults.mjs, so a path this
19
- // script protects and a path the integration writes cannot drift apart.
20
- //
21
- // What goes: each marked stories file, the .astro it exports as `component` (same directory or
22
- // below), then any file under the lab directory that ONLY the removal set imports — closed
23
- // repeatedly, so a vendored library goes when its last importer goes. Never, whatever the marks
24
- // say: decorators/, shell/, vendor/*/LICENSE, the lab's own head and home components, the mark
25
- // files, anything outside the lab directory — reported as "kept (protected)".
26
- //
27
- // Blockers refuse an entry outright: the entry is outside the cullable tier (section versions are
28
- // the version history and components are shared chrome, so neither is ever culled), the component
29
- // is LIVE (any page under src/pages/ mounts it, or mounts something that imports it — derived from
30
- // the pages themselves, nothing declared), the path is not a stories file on disk, or an unmarked
31
- // file still imports the component.
32
- // --yes refuses to run while any blocker stands.
33
- import { execFileSync } from "node:child_process";
34
- import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
35
- import { dirname, join, relative, resolve, sep } from "node:path";
36
- import { resolveLabOptions } from "../defaults.mjs";
37
- import { pageComponentFiles, sitePages } from "../src/shell/live-files.mjs";
38
- import { readCull } from "../src/shell/marks.mjs";
39
-
40
- const SOURCE_EXT = new Set([".astro", ".ts", ".mts", ".tsx", ".js", ".mjs", ".jsx", ".css"]);
41
-
42
- const args = process.argv.slice(2);
43
- const YES = args.includes("--yes");
44
- const JSON_OUT = args.includes("--json");
45
- const DIR = args.find((a) => a.startsWith("--dir="))?.slice("--dir=".length);
46
- for (const a of args)
47
- if (!["--yes", "--json"].includes(a) && !a.startsWith("--dir="))
48
- die(`unknown flag ${a} — use --yes, --json and/or --dir=<lab directory>`);
49
-
50
- // The repo the command was run in, never this file's own location: as a package binary it lives
51
- // under node_modules, and deriving the root from there would delete nothing or the wrong thing.
52
- const ROOT = process.cwd();
53
- const CONFIG = resolveLabOptions(DIR ? { directory: DIR } : {});
54
- const CULL = CONFIG.cullFile;
55
- const LAB = `${CONFIG.directory}/`;
56
-
57
- function die(message) {
58
- console.error(" FAIL " + message);
59
- process.exit(1);
60
- }
61
-
62
- const abs = (rel) => join(ROOT, rel);
63
- const rel = (absolute) => relative(ROOT, absolute).split(sep).join("/");
64
- const read = (relFile) => readFileSync(abs(relFile), "utf8");
65
- const git = (...argv) => execFileSync("git", argv, { cwd: ROOT, stdio: ["ignore", "pipe", "pipe"] }).toString();
66
-
67
- // ---- protection ------------------------------------------------------------------------------
68
- // Everything outside the lab directory is protected by the first clause, so the site's own source
69
- // can never be swept up. Inside it, the shell the lab needs and the marks themselves are named.
70
- const PROTECTED_DIRS = [`${LAB}decorators/`, `${LAB}shell/`];
71
- const PROTECTED_FILES = new Set([
72
- `${LAB}Home.astro`,
73
- `${LAB}LabHead.astro`,
74
- `${LAB}integration.mjs`,
75
- CONFIG.responsiveFile,
76
- CULL,
77
- ]);
78
- const vendorLicense = new RegExp(
79
- `^${LAB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}vendor/.*/LICENSE(\\.\\w+)?$`,
80
- );
81
- const isProtected = (f) =>
82
- !f.startsWith(LAB) ||
83
- PROTECTED_DIRS.some((d) => f.startsWith(d)) ||
84
- PROTECTED_FILES.has(f) ||
85
- vendorLicense.test(f);
86
-
87
- // ---- the import graph over all of src/ ------------------------------------------------------
88
- const walk = (dir, out = []) => {
89
- for (const entry of readdirSync(dir)) {
90
- const full = join(dir, entry);
91
- if (statSync(full).isDirectory()) walk(full, out);
92
- else if (SOURCE_EXT.has(full.slice(full.lastIndexOf(".")))) out.push(full);
93
- }
94
- return out;
95
- };
96
-
97
- // Strips comments that could carry a stale `import … from "…"` line, then collects every
98
- // specifier: static imports and re-exports (multi-line, `type`, side-effect), dynamic import(),
99
- // and CSS @import. Bare packages and anything not starting ./ or ../ are ignored.
100
- const stripComments = (src) => src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
101
- const SPECIFIER_PATTERNS = [
102
- /\b(?:import|export)\s+(?:type\s+)?(?:[\w$*{}\s,]*?\s+from\s+)?["']([^"'\n]+)["']/g,
103
- /\bimport\s*\(\s*["']([^"'\n]+)["']\s*\)/g,
104
- /@import\s+(?:url\(\s*)?["']([^"'\n]+)["']/g,
105
- ];
106
- const specifiersOf = (src) => {
107
- const clean = stripComments(src);
108
- const found = [];
109
- for (const pattern of SPECIFIER_PATTERNS) for (const m of clean.matchAll(pattern)) found.push(m[1]);
110
- return found;
111
- };
112
-
113
- // Relative specifier → existing file, or null for "not ours to follow" (bare package, /src/…
114
- // glob, alias), or undefined when a relative path resolves to nothing on disk.
115
- function resolveSpecifier(fromAbs, specifier) {
116
- const clean = specifier.replace(/[?#].*$/, "");
117
- if (!clean.startsWith("./") && !clean.startsWith("../")) return null;
118
- const base = resolve(dirname(fromAbs), clean);
119
- const candidates = [base];
120
- for (const ext of [".ts", ".mts", ".tsx", ".mjs", ".js", ".astro"]) candidates.push(base + ext);
121
- if (base.endsWith(".js")) candidates.push(base.slice(0, -3) + ".ts", base.slice(0, -3) + ".tsx");
122
- if (base.endsWith(".mjs")) candidates.push(base.slice(0, -4) + ".mts");
123
- for (const index of ["index.ts", "index.mts", "index.mjs", "index.js"]) candidates.push(join(base, index));
124
- for (const c of candidates) if (existsSync(c) && statSync(c).isFile()) return c;
125
- return undefined;
126
- }
127
-
128
- const imports = new Map(); // file → Set(files it imports)
129
- const importers = new Map(); // file → Set(files that import it)
130
- const unresolved = []; // { file, specifier } — relative paths that point at nothing
131
- for (const fileAbs of walk(abs("src"))) {
132
- const file = rel(fileAbs);
133
- const deps = new Set();
134
- for (const specifier of specifiersOf(readFileSync(fileAbs, "utf8"))) {
135
- const target = resolveSpecifier(fileAbs, specifier);
136
- if (target === null) continue;
137
- if (target === undefined) {
138
- unresolved.push({ file, specifier });
139
- continue;
140
- }
141
- const dep = rel(target);
142
- deps.add(dep);
143
- if (!importers.has(dep)) importers.set(dep, new Set());
144
- importers.get(dep).add(file);
145
- }
146
- imports.set(file, deps);
147
- }
148
- const importersOf = (f) => importers.get(f) ?? new Set();
149
-
150
- // ---- LIVE: what any page mounts, and what those mount ---------------------------------------
151
- // "Live" has ONE definition, shared with the lab's own pill (../src/shell/live-files.mjs). This
152
- // block used to read src/pages/index.astro alone, which is correct for a one-page site and wrong
153
- // for every client build — the same defect the pill carried until 2026-09-06. A second copy of a
154
- // rule like this is how the two quietly disagree, so it imports the helper rather than repeat it.
155
- const pages = sitePages(ROOT);
156
- if (pages.length === 0) die("no pages found under src/pages — cannot tell what is live");
157
- const live = new Map(); // file → why
158
- for (const page of pages) {
159
- for (const dep of pageComponentFiles(ROOT, page)) {
160
- if (!live.has(dep)) live.set(dep, `imported by ${page}`);
161
- for (const inner of imports.get(dep) ?? [])
162
- if (!live.has(inner)) live.set(inner, `imported by ${dep}, which ${page} imports`);
163
- }
164
- }
165
-
166
- // ---- the marks ---------------------------------------------------------------------------------
167
- if (!existsSync(abs(CULL))) {
168
- finish({ marked: [], entries: [], removal: [], kept: [], protectedKept: [], blockers: [], notes: [`${CULL} does not exist — nothing is marked`] });
169
- }
170
- // Read through the same helper the mark API writes with, so the two cannot disagree about shape.
171
- const cull = readCull(ROOT, CONFIG);
172
- const marked = [...new Set(cull.marked.map((p) => String(p).replace(/^\.\//, "").replace(/\\/g, "/")))];
173
-
174
- // The `component:` identifier at the top level of `export default { … }`, ignoring nested
175
- // braces, then the `import X from "./X.astro"` that names it.
176
- function componentOf(storiesRel) {
177
- const src = stripComments(read(storiesRel));
178
- const start = src.search(/export\s+default\s*\{/);
179
- if (start < 0) return { error: "no `export default { … }`" };
180
- let depth = 0;
181
- let topLevel = "";
182
- for (let i = src.indexOf("{", start); i < src.length; i++) {
183
- const ch = src[i];
184
- if (ch === "{" || ch === "[") depth++;
185
- else if (ch === "}" || ch === "]") {
186
- depth--;
187
- if (depth === 0) break;
188
- } else if (depth === 1) topLevel += ch;
189
- }
190
- const name = topLevel.match(/\bcomponent\s*:\s*([A-Za-z_$][\w$]*)/)?.[1];
191
- if (!name) return { error: "no `component: X` in the default export" };
192
- const specifier = src.match(new RegExp(`\\bimport\\s+${name}\\s+from\\s+["']([^"'\\n]+\\.astro)["']`))?.[1];
193
- if (!specifier) return { error: `no \`import ${name} from "…astro"\` to follow` };
194
- const target = resolveSpecifier(abs(storiesRel), specifier);
195
- if (!target) return { error: `component import "${specifier}" resolves to nothing` };
196
- const inside = relative(dirname(abs(storiesRel)), target);
197
- if (inside.startsWith("..")) return { error: `component "${specifier}" lives outside the stories file's directory` };
198
- return { component: rel(target) };
199
- }
200
-
201
- const entries = []; // { stories, component?, blocker? }
202
- for (const stories of marked) {
203
- const entry = { stories };
204
- entries.push(entry);
205
- if (!stories.startsWith(LAB) || !stories.endsWith(".stories.ts")) {
206
- entry.blocker = `not a ${LAB}**/*.stories.ts path`;
207
- continue;
208
- }
209
- // Ruled 2026-09-05: deletion is restricted to the explorations tier — section versions are the
210
- // version history and components are shared chrome; neither is ever culled, whatever the marks say.
211
- // Which tier that is comes from the consumer's `tiers` option, through the shared defaults.
212
- if (!CONFIG.cullDir || !stories.startsWith(CONFIG.cullDir)) {
213
- entry.blocker = `only the ${CONFIG.cullTier ?? "(none)"} tier can be culled — section versions and components stay`;
214
- continue;
215
- }
216
- if (!existsSync(abs(stories)) || !statSync(abs(stories)).isFile()) {
217
- entry.blocker = "file does not exist";
218
- continue;
219
- }
220
- if (isProtected(stories)) {
221
- entry.blocker = "protected path";
222
- continue;
223
- }
224
- const parsed = componentOf(stories);
225
- if (parsed.error) {
226
- entry.blocker = parsed.error;
227
- continue;
228
- }
229
- entry.component = parsed.component;
230
- if (isProtected(entry.component)) entry.blocker = `component ${entry.component} is protected`;
231
- else if (live.has(entry.component)) entry.blocker = `component ${entry.component} is LIVE — ${live.get(entry.component)}`;
232
- }
233
-
234
- // An unmarked importer outside the removal set blocks the entry. Blocking one entry shrinks the
235
- // set, which can expose another, so iterate to a fixed point.
236
- let changed = true;
237
- while (changed) {
238
- changed = false;
239
- const seeds = new Set(entries.filter((e) => !e.blocker).flatMap((e) => [e.stories, e.component]));
240
- for (const entry of entries) {
241
- if (entry.blocker) continue;
242
- for (const seed of [entry.component, entry.stories]) {
243
- const outside = [...importersOf(seed)].filter((i) => !seeds.has(i)).sort();
244
- if (outside.length) {
245
- entry.blocker = `${seed === entry.stories ? "stories file" : "component " + seed} is still imported by unmarked ${outside.join(", ")}`;
246
- changed = true;
247
- break;
248
- }
249
- }
250
- }
251
- }
252
-
253
- // ---- closure: what only the removal set imports ------------------------------------------------
254
- // A `.d.ts` beside a `.js` of the same name is that file's type sidecar (the vendored
255
- // html-to-image ships one per module); nothing imports it, so it rides along with its .js.
256
- const companionsOf = (file) => {
257
- const dts = file.replace(/\.m?js$/, ".d.ts");
258
- return dts !== file && existsSync(abs(dts)) ? [dts] : [];
259
- };
260
- const removal = new Set(entries.filter((e) => !e.blocker).flatMap((e) => [e.stories, e.component]));
261
- const kept = new Map(); // file → importers outside the set
262
- const protectedKept = new Map(); // file → files in the set that import it
263
- changed = true;
264
- while (changed) {
265
- changed = false;
266
- for (const file of [...removal]) {
267
- for (const dep of [...(imports.get(file) ?? []), ...companionsOf(file)]) {
268
- if (removal.has(dep)) continue;
269
- if (isProtected(dep)) {
270
- if (!protectedKept.has(dep)) protectedKept.set(dep, new Set());
271
- protectedKept.get(dep).add(file);
272
- continue;
273
- }
274
- const outside = [...importersOf(dep)].filter((i) => !removal.has(i)).sort();
275
- if (outside.length) kept.set(dep, outside);
276
- else {
277
- removal.add(dep);
278
- kept.delete(dep);
279
- changed = true;
280
- }
281
- }
282
- }
283
- }
284
-
285
- // A vendored library whose every file goes leaves its LICENSE behind on purpose — say so.
286
- const notes = [];
287
- const vendorDir = abs(`${LAB}vendor`);
288
- if (existsSync(vendorDir))
289
- for (const lib of readdirSync(vendorDir)) {
290
- const libDir = join(vendorDir, lib);
291
- if (!statSync(libDir).isDirectory()) continue;
292
- const files = walkAll(libDir).map(rel);
293
- const code = files.filter((f) => !isProtected(f));
294
- if (code.length && code.every((f) => removal.has(f)))
295
- notes.push(`${LAB}vendor/${lib}/ empties out — its LICENSE stays (protected); delete it by hand if the library is truly gone`);
296
- }
297
- function walkAll(dir, out = []) {
298
- for (const entry of readdirSync(dir)) {
299
- const full = join(dir, entry);
300
- statSync(full).isDirectory() ? walkAll(full, out) : out.push(full);
301
- }
302
- return out;
303
- }
304
-
305
- const plan = {
306
- marked,
307
- entries,
308
- removal: [...removal].sort(),
309
- kept: [...kept].map(([file, by]) => ({ file, importedBy: by })).sort((a, b) => a.file.localeCompare(b.file)),
310
- protectedKept: [...protectedKept].map(([file, by]) => ({ file, importedBy: [...by].sort() })).sort((a, b) => a.file.localeCompare(b.file)),
311
- blockers: entries.filter((e) => e.blocker).map((e) => ({ entry: e.stories, reason: e.blocker })),
312
- notes,
313
- };
314
-
315
- // ---- perform -----------------------------------------------------------------------------------
316
- if (YES && plan.blockers.length === 0 && plan.removal.length) {
317
- const tracked = new Set(git("ls-files", "-z", "--", ...plan.removal).split("\0").filter(Boolean));
318
- const viaGit = plan.removal.filter((f) => tracked.has(f));
319
- const viaDisk = plan.removal.filter((f) => !tracked.has(f));
320
- if (viaGit.length) git("rm", "-q", "--", ...viaGit);
321
- for (const f of viaDisk) rmSync(abs(f));
322
-
323
- const processed = new Set(entries.filter((e) => !e.blocker).map((e) => e.stories));
324
- const remaining = cull.marked.filter((p) => !processed.has(String(p).replace(/^\.\//, "").replace(/\\/g, "/")));
325
- writeFileSync(abs(CULL), JSON.stringify({ marked: remaining, updated: new Date().toISOString() }, null, 2) + "\n");
326
- const cullTracked = git("ls-files", "--", CULL).trim() !== "";
327
- if (cullTracked) git("add", "--", CULL);
328
-
329
- const now = new Date();
330
- const date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
331
- const names = entries.filter((e) => !e.blocker).map((e) => e.component.replace(/^.*\//, "").replace(/\.astro$/, ""));
332
- plan.performed = {
333
- gitRm: viaGit,
334
- deletedUntracked: viaDisk,
335
- marksCleared: processed.size,
336
- marksRemaining: remaining.length,
337
- cullJsonStaged: cullTracked,
338
- buildLogLine: `#### ${date} · [WILL] Lab cull — ${names.length} item${names.length === 1 ? "" : "s"} removed: ${names.join(", ")}`,
339
- };
340
- }
341
-
342
- finish(plan);
343
-
344
- // ---- report ------------------------------------------------------------------------------------
345
- function finish(p) {
346
- const exitCode = p.blockers.length ? 2 : 0;
347
- if (JSON_OUT) {
348
- console.log(JSON.stringify({ ok: exitCode === 0, dryRun: !p.performed, ...p, unresolved }, null, 2));
349
- process.exit(exitCode);
350
- }
351
-
352
- const list = (items, render = (x) => x) => items.forEach((i) => console.log(" " + render(i)));
353
- const header = p.performed
354
- ? "lab cull — performed"
355
- : YES && p.blockers.length
356
- ? "lab cull — REFUSED (blockers below; nothing removed)"
357
- : YES
358
- ? "lab cull — nothing to perform"
359
- : "lab cull — dry run (pass --yes to perform)";
360
- console.log(header);
361
- console.log(`marked: ${p.marked.length} entr${p.marked.length === 1 ? "y" : "ies"} in ${CULL}`);
362
- if (p.marked.length === 0) {
363
- p.notes.forEach((n) => console.log(" " + n));
364
- console.log(" ok nothing marked — nothing to do");
365
- process.exit(0);
366
- }
367
-
368
- console.log(`\nremove (${p.removal.length}):`);
369
- list(p.removal);
370
- console.log(`\nkept — still imported by unmarked files (${p.kept.length}):`);
371
- list(p.kept, (k) => `${k.file} ← ${k.importedBy.join(", ")}`);
372
- console.log(`\nkept (protected) (${p.protectedKept.length}):`);
373
- list(p.protectedKept, (k) => `${k.file} ← ${k.importedBy.join(", ")}`);
374
- console.log(`\nblockers (${p.blockers.length}):`);
375
- p.blockers.forEach((b) => console.error(` FAIL ${b.entry} — ${b.reason}`));
376
- if (p.notes.length) {
377
- console.log("\nnotes:");
378
- list(p.notes);
379
- }
380
- if (unresolved.length) {
381
- console.log(`\nunresolved relative imports (${unresolved.length}) — treated as nothing, check them:`);
382
- list(unresolved, (u) => `${u.file} → "${u.specifier}"`);
383
- }
384
-
385
- console.log("");
386
- if (p.performed) {
387
- const r = p.performed;
388
- console.log(` ok git rm (staged, not committed): ${r.gitRm.length} file${r.gitRm.length === 1 ? "" : "s"}`);
389
- if (r.deletedUntracked.length) console.log(` ok deleted from disk (were untracked): ${r.deletedUntracked.join(", ")}`);
390
- console.log(` ok ${CULL}: ${r.marksCleared} mark${r.marksCleared === 1 ? "" : "s"} cleared, ${r.marksRemaining} left${r.cullJsonStaged ? ", staged" : " (untracked, not staged)"}`);
391
- console.log("\nBuild-log line — paste it, this script edits no log and commits nothing:\n");
392
- console.log(r.buildLogLine);
393
- } else if (exitCode === 2) {
394
- console.log(YES ? " FAIL refusing --yes while blockers stand — clear them (unmark, or unmark the importer too) and rerun" : " FAIL plan has blockers — --yes would be refused");
395
- } else if (p.removal.length) {
396
- console.log(` ok plan is clean — ${p.removal.length} file${p.removal.length === 1 ? "" : "s"} would be removed`);
397
- } else {
398
- console.log(" ok nothing to remove");
399
- }
400
- process.exit(exitCode);
401
- }