@marver-design/marver 0.20.0 → 0.22.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 (45) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +17 -4
  3. package/dist/{bake-tAb0D6Rc.mjs → bake-BSX4XR_U.mjs} +1 -1
  4. package/dist/board-status-CWGHIdo_.mjs +1307 -0
  5. package/dist/boards-BTGNPVMx.mjs +187 -0
  6. package/dist/{build-DwpNPl6Z.mjs → build-Dd2Qm3OG.mjs} +87 -16
  7. package/dist/cli.mjs +67 -8
  8. package/dist/config-DJxMRVD8.mjs +373 -0
  9. package/dist/context-DUAENlJ6.mjs +600 -0
  10. package/dist/{daemon-DbHvLQUL.mjs → daemon-BmkwErpC.mjs} +1 -1
  11. package/dist/{dev-BjdDP69b.mjs → dev-BW7a5kDF.mjs} +8 -6
  12. package/dist/{init-DKxuRxBr.mjs → init-NJ3xjLVu.mjs} +39 -44
  13. package/dist/managed-write-Bo-oPc-i.mjs +71 -0
  14. package/dist/{manifest-B01PSyDc.mjs → manifest-BqBcMJcd.mjs} +26 -380
  15. package/dist/{plugin-y4Ch7o_A.mjs → plugin-8XntSCx_.mjs} +234 -97
  16. package/dist/{poster-FCv_nXyP.mjs → poster-DRZDszTI.mjs} +1 -1
  17. package/dist/{publish-bakes-XH6Bac58.mjs → publish-bakes-CCWRV9iX.mjs} +2 -2
  18. package/dist/{shot-iw3SEcpn.mjs → shot-C22Ues04.mjs} +2 -2
  19. package/docs/boards-and-folders.md +161 -0
  20. package/docs/context.md +117 -0
  21. package/docs/sharing.md +12 -2
  22. package/package.json +1 -1
  23. package/src/client/shell/BoardList.tsx +143 -54
  24. package/src/client/shell/ContextMenu.tsx +16 -5
  25. package/src/client/shell/StatusPicker.tsx +91 -0
  26. package/src/client/shell/board-icons.tsx +75 -0
  27. package/src/client/shell/store.ts +93 -14
  28. package/src/client/shell/styles.css +41 -9
  29. package/src/shared/board-tree.ts +291 -143
  30. package/src/shared/board-types.ts +103 -0
  31. package/src/shared/context.ts +265 -0
  32. package/src/shared/status.ts +157 -0
  33. package/templates/AGENTS-embedded.md +4 -3
  34. package/templates/AGENTS-studio.md +4 -3
  35. package/templates/context/INDEX.md +50 -0
  36. package/templates/context/map.json +6 -0
  37. package/templates/context/shipped-knowledge.md +19 -0
  38. package/templates/context/shipped.md +28 -0
  39. package/templates/instructions/boards.md +78 -22
  40. package/templates/instructions/context.md +114 -0
  41. package/templates/playbooks/publish-canvas/PLAYBOOK.md +72 -0
  42. package/templates/playbooks/reorganize-context/PLAYBOOK.md +232 -0
  43. package/templates/playbooks/reorganize-context/eval.md +93 -0
  44. package/dist/boards-BmxcT3Lc.mjs +0 -290
  45. package/dist/boards-PuVzw5Wp.mjs +0 -62
@@ -0,0 +1,93 @@
1
+ # Context eval - can a fresh agent answer from the repo alone?
2
+
3
+ The reorganization is judged by one thing: a fresh agent, with no memory and no chat history,
4
+ answering real questions about the product correctly and cheaply. Run it **before** and **after**,
5
+ the same way both times.
6
+
7
+ This file is the protocol and the questions. The **answer key is a separate file**
8
+ (`answer-key-<project>.md`) and never enters the worktree the agent reads.
9
+
10
+ ## Isolation - check before each pass
11
+
12
+ - The agent runs in the worktree (`../<repo>-context`), a fresh process per question.
13
+ - No agent memory for that path: Claude Code keys memory by project path, so
14
+ `~/.claude/projects/<worktree-slug>/memory/` must not exist. Codex keeps none by default.
15
+ - No hook injects project context for that path (a vault feed keyed by path, a knowledge inbox).
16
+ - The answer key, the reorg notes kept outside the worktree, and earlier pass results are not in
17
+ the worktree. Before the first pass, `context/` does not exist yet.
18
+ - **A clean profile.** Global instructions also count as injected context: Claude Code loads
19
+ `~/.claude/CLAUDE.md`, Codex loads its global `AGENTS.md`. Launch with an empty config home and
20
+ only credentials in it - Claude Code: `CLAUDE_CONFIG_DIR=<empty dir>` with an API key; Codex:
21
+ `CODEX_HOME=<dir holding only auth>`. Confirm from the transcript's first event that no user or
22
+ global instruction file was loaded.
23
+ - *Learned on the first run:* without an API key, Claude Code cannot run clean - `--bare` needs one,
24
+ and a plain run loads the auto-memory that worktrees of one repository share. Codex with a
25
+ `CODEX_HOME` holding a copy of `auth.json` works; check `codex features list` shows `memories`
26
+ off, and keep each pass's session rollouts out of that home before the next pass. Copy the auth
27
+ only when its `last_refresh` is recent, and delete the copy after.
28
+ - **Read-only.** Claude Code with read tools only (`--allowedTools Read,Grep,Glob`); Codex with
29
+ `--sandbox read-only`.
30
+ - Same CLI, same model, same prompt, both passes.
31
+
32
+ ## The prompt
33
+
34
+ > Answer this question using only files in this repository: <question>. Give the answer, the
35
+ > files you used, and your confidence. If the repository cannot settle it, say "cannot tell" and
36
+ > why. Do not run the app, query a database, call an API, or read outside this directory.
37
+
38
+ ## The measurement - one metric, per question
39
+
40
+ Run each question as its own invocation with a machine-readable transcript (Claude Code:
41
+ `claude -p "<prompt>" --output-format stream-json --verbose`; Codex: `codex exec --json`). From the
42
+ transcript record:
43
+
44
+ - **result** - correct / partial / wrong / cannot-tell, judged against the answer key;
45
+ - **confident wrong** - a wrong answer given with high confidence, counted separately;
46
+ - **bytes read** - the summed size of every tool result returned to the agent (file reads, searches,
47
+ listings). Measure what the model saw, not what the command printed: Codex truncates long output
48
+ (each result carries its `original_token_count`), so read the session rollout's tool outputs, not
49
+ `exec --json`'s `aggregated_output`. Record the session's **input tokens** beside it;
50
+ - **tool calls**;
51
+ - **out of bounds** - any read outside the worktree. One voids that answer.
52
+
53
+ "Cannot tell" with the right reason counts as correct where the key says the repository genuinely
54
+ does not hold the answer.
55
+
56
+ ## Pass
57
+
58
+ - no confident wrong answer after;
59
+ - more correct answers after than before - or, if the before pass was already perfect, equal;
60
+ - fewer total bytes read after.
61
+
62
+ A capable agent may already answer everything before - the first run's before pass was 16 / 16 at
63
+ medium effort, the traps notwithstanding. Then the pass is decided on cost, and a weaker or cheaper
64
+ profile is worth a second pair of passes to see whether the reorganization moves correctness.
65
+
66
+ ## The six kinds of question
67
+
68
+ Write 10-16, at least one of each, from Phase 2 evidence, with the key signed off by the human where
69
+ Phase 8 asked:
70
+
71
+ 1. **Shipped** - is X available, where, for whom, since when?
72
+ 2. **Behaviour** - how does surface Y work today?
73
+ 3. **Interfaces** - what runs behind action Z: jobs, mails, data written?
74
+ 4. **Why** - why was decision N taken, and does it still apply?
75
+ 5. **Next** - what is being built next, and what counts as done?
76
+ 6. **Receipts** - what did someone say about T, and where is it recorded?
77
+
78
+ Include **traps**: questions where a stale spec, a design frame, or a wrong instruction gives a
79
+ confident wrong answer today. They are the point.
80
+
81
+ ## Your project's questions
82
+
83
+ Write them before the before pass, from Phase 2's evidence, and keep the key out of the worktree:
84
+
85
+ | # | Kind | Question | Trap |
86
+ |---|---|---|---|
87
+
88
+ ## Report format
89
+
90
+ | # | Before: result | Before: bytes / calls | After: result | After: bytes / calls |
91
+ |---|---|---|---|---|
92
+
93
+ Write the before column before Phase 1 begins - not from memory afterwards.
@@ -1,290 +0,0 @@
1
- import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
2
- import { join, sep } from "node:path";
3
- import { createHash } from "node:crypto";
4
- //#region src/server/hash.ts
5
- /** sha256 hex of a string - the CAS token for board files, the registry, the manifest. */
6
- const hash = (s) => createHash("sha256").update(s).digest("hex");
7
- //#endregion
8
- //#region src/shared/board-tree.ts
9
- /**
10
- * Board folders - the pure tree shared by the sidebar, the dev API, the build and the
11
- * tests. Files are the truth: a board says which folder it sits in (`folder` on the
12
- * board file, ranked among its siblings by `order`), and `design/boards/_folders.json`
13
- * says which folders exist and where they rank at the root. One level only: folders
14
- * hold boards, never folders. `all-scenes` never enters the tree - callers pin it last.
15
- */
16
- /** The on-disk name grammar shared by boards and folders (a board name is a filename). */
17
- const BOARD_NAME = /^[a-z0-9][a-z0-9-]*$/;
18
- const isBoardName = (n) => typeof n === "string" && n.length >= 1 && n.length <= 64 && BOARD_NAME.test(n);
19
- /** The folder registry beside the boards - underscore = infrastructure, never a board. */
20
- const FOLDERS_FILE = "_folders.json";
21
- /** Is this basename in design/boards/ a board file? `_folders.json`, temp files and any
22
- * off-grammar name are not - every lister (dev API, build, watcher) shares this rule. */
23
- const isBoardFile = (f) => f.endsWith(".json") && isBoardName(f.slice(0, -5));
24
- const readDescription = (v) => {
25
- if (typeof v !== "string") return void 0;
26
- return v.trim().replace(/\s+/g, " ").slice(0, 300) || void 0;
27
- };
28
- const readTitle = (v) => {
29
- if (typeof v !== "string") return void 0;
30
- return Array.from(v.replace(/[\u0000-\u001f\u007f]/g, " ").trim().replace(/\s+/g, " ")).slice(0, 120).join("").trim() || void 0;
31
- };
32
- const rank = (o) => typeof o === "number" && Number.isFinite(o) ? o : Infinity;
33
- /** A folder's title and description, present only when set. */
34
- const folderExtras = (it) => ({
35
- ...it.title ? { title: it.title } : {},
36
- ...it.description ? { description: it.description } : {}
37
- });
38
- /** The registry file's shape. Returns the rows, or a string naming what is wrong - a
39
- * malformed registry is an ERROR the human must fix (silently reading it as empty would
40
- * let the next drag overwrite their folders), while a missing file is simply no folders. */
41
- function parseFolders(raw) {
42
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "expected an object";
43
- const { version, folders } = raw;
44
- if (version !== void 0 && version !== 1) return `unsupported version ${String(version)}`;
45
- if (!Array.isArray(folders)) return "expected a \"folders\" array";
46
- const out = [];
47
- const seen = /* @__PURE__ */ new Set();
48
- for (const f of folders) {
49
- const name = f?.name;
50
- if (!isBoardName(name)) return "a folder needs a name - lowercase letters, numbers and dashes";
51
- if (seen.has(name)) return `folder "${name}" is listed twice`;
52
- seen.add(name);
53
- const o = f.order;
54
- const t = readTitle(f.title);
55
- const d = readDescription(f.description);
56
- out.push({
57
- name,
58
- ...typeof o === "number" && Number.isFinite(o) ? { order: o } : {},
59
- ...t ? { title: t } : {},
60
- ...d ? { description: d } : {}
61
- });
62
- }
63
- return out;
64
- }
65
- /** Sidebar order from the files. Root: boards with no folder + every folder (registered
66
- * or implied by a board), ranked by `order` then kind (board before folder) then name.
67
- * Inside a folder: its boards by `order` then name. Unranked sorts after ranked. A folder's
68
- * title and description ride on its item (they live in the registry a tree write rewrites);
69
- * a board's title stays with its row - it lives in the board's own file. */
70
- function buildTree(boards, folders) {
71
- const folderOrder = /* @__PURE__ */ new Map();
72
- const folderMeta = /* @__PURE__ */ new Map();
73
- for (const f of folders) if (isBoardName(f.name) && !folderOrder.has(f.name)) {
74
- folderOrder.set(f.name, f.order);
75
- folderMeta.set(f.name, folderExtras(f));
76
- }
77
- const members = /* @__PURE__ */ new Map();
78
- const rootBoards = [];
79
- for (const b of boards) {
80
- if (!isBoardName(b.name) || b.name === "all-scenes") continue;
81
- const folder = isBoardName(b.folder) ? b.folder : void 0;
82
- if (!folder) {
83
- rootBoards.push(b);
84
- continue;
85
- }
86
- if (!folderOrder.has(folder)) folderOrder.set(folder, void 0);
87
- const list = members.get(folder) ?? [];
88
- list.push(b);
89
- members.set(folder, list);
90
- }
91
- const byRank = (a, b) => rank(a.order) - rank(b.order) || a.name.localeCompare(b.name);
92
- const root = [...rootBoards.map((b) => ({
93
- item: {
94
- kind: "board",
95
- name: b.name
96
- },
97
- order: b.order
98
- })), ...[...folderOrder].map(([name, order]) => ({
99
- item: {
100
- kind: "folder",
101
- name,
102
- boards: (members.get(name) ?? []).sort(byRank).map((b) => b.name),
103
- ...folderMeta.get(name) ?? {}
104
- },
105
- order
106
- }))];
107
- root.sort((a, b) => rank(a.order) - rank(b.order) || (a.item.kind === b.item.kind ? 0 : a.item.kind === "board" ? -1 : 1) || a.item.name.localeCompare(b.item.name));
108
- return root.map((r) => r.item);
109
- }
110
- /** Every board in reading order - the order the switchers and the landing pick use. */
111
- function flatten(tree) {
112
- const out = [];
113
- for (const it of tree) if (it.kind === "board") out.push(it.name);
114
- else out.push(...it.boards);
115
- return out;
116
- }
117
- function validateWire(wire) {
118
- if (!Array.isArray(wire)) return "invalid tree";
119
- const boards = /* @__PURE__ */ new Set(), folders = /* @__PURE__ */ new Set();
120
- const board = (n) => {
121
- if (!isBoardName(n) || n === "all-scenes") return "invalid board name in tree";
122
- if (boards.has(n)) return `board "${n}" appears twice`;
123
- boards.add(n);
124
- return null;
125
- };
126
- for (const w of wire) {
127
- if (typeof w === "string") {
128
- const e = board(w);
129
- if (e) return e;
130
- continue;
131
- }
132
- if (!w || typeof w !== "object" || Array.isArray(w)) return "invalid tree item";
133
- const { folder, boards: kids, title, description } = w;
134
- if (!isBoardName(folder)) return "invalid folder name in tree";
135
- if (title !== void 0 && (typeof title !== "string" || Array.from(title).length > 120)) return "invalid folder title";
136
- if (description !== void 0 && (typeof description !== "string" || description.length > 300)) return "invalid folder description";
137
- if (folders.has(folder)) return `folder "${folder}" appears twice`;
138
- folders.add(folder);
139
- if (!Array.isArray(kids)) return "invalid folder in tree";
140
- for (const k of kids) {
141
- const e = board(k);
142
- if (e) return e;
143
- }
144
- }
145
- if (boards.size > 200 || folders.size > 50) return "tree too large";
146
- return null;
147
- }
148
- //#endregion
149
- //#region src/server/boards.ts
150
- /**
151
- * Reading design/boards/ safely - the one enumerator the dev API, the build and the tests
152
- * share. Only REGULAR files on the board-name grammar count as boards (a symlink, dangling or
153
- * live, could read or publish JSON from outside the project - it is skipped, and reported so
154
- * the build can fail closed). The folder registry is read the same way: absent = no folders,
155
- * malformed = an error the human must fix, never a silently empty registry.
156
- */
157
- /** Does realpath(dir) stay inside realpath(root)? A symlinked design/boards can't escape. */
158
- function underRoot(root, dir) {
159
- try {
160
- const rr = realpathSync(root);
161
- const rd = realpathSync(dir);
162
- return rd === rr || rd.startsWith(rr + sep);
163
- } catch {
164
- return false;
165
- }
166
- }
167
- /** Is design/boards a directory we may read and write? It must not be a symlink at all (a
168
- * link to the repo root would list package.json as a board and let a tree write rewrite
169
- * it; a link outside would publish foreign JSON) and must resolve inside the root. Absent
170
- * is fine (no boards yet). Returns the error, or null. */
171
- function checkBoardsDir(root, boardsDir) {
172
- return checkRealDirs(root, [[join(boardsDir, ".."), "design"], [boardsDir, "design/boards"]]);
173
- }
174
- /** Every EXISTING path in `dirs` (root-down order) must be a real directory inside the root -
175
- * a symlinked `design` with no boards dir yet would otherwise be followed by the mkdir that
176
- * creates it; a symlinked `design/scenes` would let a brief write land outside the project.
177
- * An absent one ends the walk (nothing beneath it exists either). Returns the error, or null. */
178
- function checkRealDirs(root, dirs) {
179
- for (const [p, label] of dirs) {
180
- try {
181
- if (lstatSync(p).isSymbolicLink()) return `${label} must be a real directory, not a symlink`;
182
- } catch {
183
- return null;
184
- }
185
- if (!underRoot(root, p)) return `${label} escapes the project`;
186
- }
187
- return null;
188
- }
189
- /** A regular file (lstat: a symlink is never followed, a dangling one is not "absent"). */
190
- const isRegularFile = (p) => {
191
- try {
192
- return lstatSync(p).isFile();
193
- } catch {
194
- return false;
195
- }
196
- };
197
- /** Is there ANY node at p (a dangling symlink counts)? */
198
- const nodeExists = (p) => {
199
- try {
200
- lstatSync(p);
201
- return true;
202
- } catch {
203
- return false;
204
- }
205
- };
206
- /** Every board file: name, raw content, hash, and its JSON (null when malformed). `skipped`
207
- * names the entries that looked like boards but were not regular files. */
208
- function listBoardFiles(boardsDir) {
209
- const boards = [], skipped = [];
210
- if (!existsSync(boardsDir)) return {
211
- boards,
212
- skipped
213
- };
214
- for (const f of readdirSync(boardsDir)) {
215
- if (!isBoardFile(f)) continue;
216
- const file = join(boardsDir, f);
217
- if (!isRegularFile(file)) {
218
- skipped.push(f);
219
- continue;
220
- }
221
- const content = readFileSync(file, "utf8");
222
- let json = null;
223
- try {
224
- json = JSON.parse(content);
225
- } catch {}
226
- boards.push({
227
- name: f.slice(0, -5),
228
- file,
229
- content,
230
- sha256: hash(content),
231
- json
232
- });
233
- }
234
- return {
235
- boards,
236
- skipped
237
- };
238
- }
239
- /** The author-owned sidebar fields off a board's JSON, leniently: rank, folder, the title
240
- * humans see, the sentence agents read. */
241
- function boardFields(json, validName) {
242
- const o = json;
243
- const title = readTitle(o?.title);
244
- const description = readDescription(o?.description);
245
- return {
246
- ...typeof o?.order === "number" && Number.isFinite(o.order) ? { order: o.order } : {},
247
- ...validName(o?.folder) ? { folder: o.folder } : {},
248
- ...title ? { title } : {},
249
- ...description ? { description } : {}
250
- };
251
- }
252
- /** The folder registry. `sha256` is the CAS token a tree write must echo (null = "there was
253
- * no file"), so a write can never silently replace a registry it never saw. */
254
- function readRegistry(boardsDir) {
255
- const p = join(boardsDir, FOLDERS_FILE);
256
- if (!nodeExists(p)) return {
257
- state: "absent",
258
- folders: [],
259
- sha256: null
260
- };
261
- if (!isRegularFile(p)) return {
262
- state: "malformed",
263
- error: `design/boards/${FOLDERS_FILE} must be a regular file, not a symlink`,
264
- sha256: null
265
- };
266
- const content = readFileSync(p, "utf8");
267
- let raw;
268
- try {
269
- raw = JSON.parse(content);
270
- } catch {
271
- return {
272
- state: "malformed",
273
- error: `design/boards/${FOLDERS_FILE} is not valid JSON - fix the file`,
274
- sha256: hash(content)
275
- };
276
- }
277
- const parsed = parseFolders(raw);
278
- if (typeof parsed === "string") return {
279
- state: "malformed",
280
- error: `design/boards/${FOLDERS_FILE}: ${parsed}`,
281
- sha256: hash(content)
282
- };
283
- return {
284
- state: "ok",
285
- folders: parsed,
286
- sha256: hash(content)
287
- };
288
- }
289
- //#endregion
290
- export { listBoardFiles as a, BOARD_NAME as c, flatten as d, isBoardName as f, hash as g, validateWire as h, isRegularFile as i, FOLDERS_FILE as l, readTitle as m, checkBoardsDir as n, nodeExists as o, readDescription as p, checkRealDirs as r, readRegistry as s, boardFields as t, buildTree as u };
@@ -1,62 +0,0 @@
1
- import { a as listBoardFiles, d as flatten, f as isBoardName, n as checkBoardsDir, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BmxcT3Lc.mjs";
2
- import { join } from "node:path";
3
- //#region src/cli/boards.ts
4
- /**
5
- * `marver boards` - the sidebar as the agent sees it: every folder and board in reading
6
- * order, from the files (no dev server needed). One call answers "what folders exist, what
7
- * is in them, what ranks where" before the agent writes `folder` on a board or edits
8
- * `design/boards/_folders.json`. `--json` gives the tree shape the shell uses.
9
- */
10
- function boardsCommand(root, opts) {
11
- const dir = join(root, "design", "boards");
12
- const de = checkBoardsDir(root, dir);
13
- if (de) throw new Error(de);
14
- const { boards, skipped } = listBoardFiles(dir);
15
- const reg = readRegistry(dir);
16
- if (reg.state === "malformed") throw new Error(reg.error);
17
- const rows = boards.map((b) => ({
18
- name: b.name,
19
- ...boardFields(b.json, isBoardName)
20
- }));
21
- const tree = buildTree(rows, reg.folders);
22
- const hasAll = boards.some((b) => b.name === "all-scenes");
23
- if (opts.json) {
24
- console.log(JSON.stringify({
25
- tree,
26
- boards: rows.filter((r) => r.name !== "all-scenes"),
27
- landing: flatten(tree)[0] ?? (hasAll ? "all-scenes" : null),
28
- registry: reg.state === "ok" ? "design/boards/_folders.json" : null
29
- }, null, 2));
30
- return;
31
- }
32
- if (!tree.length && !hasAll) {
33
- console.log("no boards yet - design/boards/ is empty");
34
- return;
35
- }
36
- const order = (n) => {
37
- const o = rows.find((r) => r.name === n)?.order;
38
- return o === void 0 ? "" : ` order ${o}`;
39
- };
40
- const title = (t) => t ? ` "${t}"` : "";
41
- const desc = (d) => d ? ` - ${d}` : "";
42
- const boardLine = (n) => {
43
- const r = rows.find((x) => x.name === n);
44
- return `${n}${title(r?.title)}${order(n)}${desc(r?.description)}`;
45
- };
46
- for (const it of tree) {
47
- if (it.kind === "board") {
48
- console.log(boardLine(it.name));
49
- continue;
50
- }
51
- console.log(`${it.name}/${title(it.title)} (folder, ${it.boards.length} board${it.boards.length === 1 ? "" : "s"}${reg.folders.some((f) => f.name === it.name) ? "" : ", implied by its boards - not in _folders.json"})${desc(it.description)}`);
52
- for (const b of it.boards) console.log(` ${boardLine(b)}`);
53
- if (!it.boards.length) console.log(" (empty)");
54
- }
55
- if (hasAll) console.log("all-scenes (auto, always last)");
56
- const landing = flatten(tree)[0];
57
- if (landing) console.log(`\nlanding board: ${landing}`);
58
- console.log(`registry: ${reg.state === "ok" ? "design/boards/_folders.json" : "none (no empty or ranked folders yet)"}`);
59
- if (skipped.length) console.log(`skipped (not regular files): ${skipped.join(", ")}`);
60
- }
61
- //#endregion
62
- export { boardsCommand };