@nitra/cursor 4.1.1 → 5.0.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 (71) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/docs/n-cursor.md +1 -9
  3. package/bin/n-cursor.js +3 -25
  4. package/docs/stryker.config.md +37 -0
  5. package/docs/vitest.config.md +23 -0
  6. package/package.json +2 -1
  7. package/rules/docker/lib/docs/docker-mirror.md +1 -1
  8. package/rules/docker/lib/docs/docker-native-addon.md +1 -1
  9. package/rules/test/coverage/coverage.mjs +9 -19
  10. package/rules/test/test.mdc +1 -1
  11. package/scripts/dispatcher/trace.mjs +4 -16
  12. package/scripts/docs/build-agents-commands.md +1 -1
  13. package/scripts/docs/worktree-cli.md +1 -1
  14. package/scripts/lib/changed-files.mjs +19 -3
  15. package/scripts/lib/sync-gitignore-worktree.mjs +4 -5
  16. package/scripts/worktree-cli.mjs +1 -2
  17. package/skills/docgen/js/docgen-gen.mjs +7 -7
  18. package/scripts/dispatcher/docs/graph.md +0 -346
  19. package/scripts/dispatcher/docs/index.md +0 -236
  20. package/scripts/dispatcher/docs/trace.md +0 -296
  21. package/scripts/dispatcher/graph/lib/cmd-init.mjs +0 -112
  22. package/scripts/dispatcher/graph/lib/cmd-invalidate.mjs +0 -96
  23. package/scripts/dispatcher/graph/lib/cmd-kill.mjs +0 -141
  24. package/scripts/dispatcher/graph/lib/cmd-plan.mjs +0 -142
  25. package/scripts/dispatcher/graph/lib/cmd-run.mjs +0 -328
  26. package/scripts/dispatcher/graph/lib/cmd-scan.mjs +0 -115
  27. package/scripts/dispatcher/graph/lib/cmd-setup.mjs +0 -111
  28. package/scripts/dispatcher/graph/lib/cmd-signals.mjs +0 -328
  29. package/scripts/dispatcher/graph/lib/cmd-status.mjs +0 -131
  30. package/scripts/dispatcher/graph/lib/cmd-verify.mjs +0 -100
  31. package/scripts/dispatcher/graph/lib/cmd-watch.mjs +0 -128
  32. package/scripts/dispatcher/graph/lib/config.mjs +0 -103
  33. package/scripts/dispatcher/graph/lib/frontmatter.mjs +0 -224
  34. package/scripts/dispatcher/graph/lib/nnn.mjs +0 -127
  35. package/scripts/dispatcher/graph/lib/node-state.mjs +0 -157
  36. package/scripts/dispatcher/graph/lib/scanner.mjs +0 -235
  37. package/scripts/dispatcher/graph/lib/worktree-ops.mjs +0 -193
  38. package/scripts/dispatcher/graph-tasks.mjs +0 -92
  39. package/scripts/dispatcher/graph.mjs +0 -212
  40. package/scripts/dispatcher/index.mjs +0 -45
  41. package/scripts/dispatcher/lib/docs/active.md +0 -348
  42. package/scripts/dispatcher/lib/docs/artifact.md +0 -232
  43. package/scripts/dispatcher/lib/docs/budget.md +0 -167
  44. package/scripts/dispatcher/lib/docs/capability.md +0 -196
  45. package/scripts/dispatcher/lib/docs/commands.md +0 -210
  46. package/scripts/dispatcher/lib/docs/events.md +0 -183
  47. package/scripts/dispatcher/lib/docs/executor.md +0 -190
  48. package/scripts/dispatcher/lib/docs/gate.md +0 -231
  49. package/scripts/dispatcher/lib/docs/level.md +0 -335
  50. package/scripts/dispatcher/lib/docs/plan-panel.md +0 -181
  51. package/scripts/dispatcher/lib/docs/plan.md +0 -200
  52. package/scripts/dispatcher/lib/docs/planner.md +0 -269
  53. package/scripts/dispatcher/lib/docs/review.md +0 -255
  54. package/scripts/dispatcher/lib/docs/reviewer.md +0 -240
  55. package/scripts/dispatcher/lib/docs/snapshot.md +0 -247
  56. package/scripts/dispatcher/lib/docs/spec.md +0 -203
  57. package/scripts/dispatcher/lib/docs/state-store.md +0 -303
  58. package/scripts/dispatcher/lib/docs/subagent-runner.md +0 -173
  59. package/scripts/dispatcher/lib/events.mjs +0 -67
  60. package/scripts/dispatcher/lib/executor.mjs +0 -107
  61. package/scripts/dispatcher/lib/plan-panel.mjs +0 -76
  62. package/scripts/dispatcher/lib/state-store.mjs +0 -173
  63. package/scripts/dispatcher/lib/subagent-runner.mjs +0 -53
  64. package/scripts/graph/index.mjs +0 -115
  65. package/scripts/graph/lib/config.mjs +0 -62
  66. package/scripts/graph/lib/dag.mjs +0 -161
  67. package/scripts/graph/lib/frontmatter.mjs +0 -70
  68. package/scripts/graph/lib/nnn.mjs +0 -77
  69. package/scripts/graph/lib/state.mjs +0 -110
  70. package/scripts/graph/scan.mjs +0 -64
  71. package/scripts/graph/status.mjs +0 -86
@@ -1,296 +0,0 @@
1
- # `trace.mjs` — наскрізна простежуваність артефактів документації
2
-
3
- ## Огляд
4
-
5
- Модуль реалізує CLI-команду `n-cursor trace` (специфікація §5.4 / §7) — інструмент **наскрізної простежуваності** (traceability) між артефактами в `docs/`. Він читає YAML-front-matter з усіх Markdown-файлів у каталогах `docs/tasks`, `docs/specs`, `docs/plans`, `docs/adr`, будує ланцюг зв'язків між ними за полями-лінками (`adr`, `spec`, `plan`, `flow`, `change`, `task`) і **флагує розриви** — тобто посилання на неіснуючі файли.
6
-
7
- Модуль повністю **read-only**: жодних мутацій файлової системи. Підтримує два режими виводу:
8
-
9
- - **текстовий** (за замовчуванням) — людино-читабельний звіт з символами `→`/`✗`/`~`;
10
- - **JSON** (`--json`) — machine-readable структура для CI / інших інструментів.
11
-
12
- Поведінка щодо FS повністю інжектабельна (`readdir`, `readFile`, `exists`, `cwd`, `log`), завдяки чому модуль тестується без реального диска та без зміни робочої директорії.
13
-
14
- Окремий нюанс — поле `flow` трактується як **інформаційне**, а не chain-поле: воно вказує на runtime-стан у `.worktrees/<branch>.flow.json`, який gitignored і за межами `docs/`, тому його відсутність ніколи не вважається розривом ланцюга (інакше у чистому checkout або CI-сесії був би хибний сигнал).
15
-
16
- ## Експорти / API
17
-
18
- | Експорт | Тип | Призначення |
19
- | ----------------------------- | -------------- | ------------------------------------------------------------ |
20
- | `parseFrontMatter(content)` | named function | Парсить плаский YAML-front-matter Markdown-файла. |
21
- | `analyze(artifacts, resolve)` | named function | Будує аналіз лінків артефактів зі статусами `ok`/`breaking`. |
22
- | `render(analysis)` | named function | Текстовий рендер результату `analyze`. |
23
- | `runTraceCli(args, deps?)` | named function | Точка входу CLI `n-cursor trace [--json]`. |
24
-
25
- Внутрішні (не експортуються): `isSimpleKey`, `resolveLink`, `renderLink`, константи `LINK_FIELDS`, `INFO_LINK_FIELDS`, `DIRS`.
26
-
27
- ### Константи модуля
28
-
29
- - `LINK_FIELDS = ['adr', 'spec', 'plan', 'flow', 'change', 'task']` — впорядкований список полів front-matter, які розглядаються як лінки. Порядок впливає на порядок виводу.
30
- - `INFO_LINK_FIELDS = new Set(['flow'])` — підмножина полів, відсутність яких **не** рве ланцюг (інформаційні, не breaking).
31
- - `DIRS = ['docs/tasks', 'docs/specs', 'docs/plans', 'docs/adr']` — каталоги, у яких шукаються traceable-артефакти.
32
-
33
- ## Функції
34
-
35
- ### `parseFrontMatter(content)`
36
-
37
- **Сигнатура:** `parseFrontMatter(content: string): Record<string, string | null> | null`
38
-
39
- **Параметри:**
40
-
41
- - `content` — повний текст Markdown-файла.
42
-
43
- **Повертає:**
44
-
45
- - Об'єкт `{ key: value }` зі значеннями типу `string` (звичайне поле) або `null` (порожнє чи літерал `null`);
46
- - `null`, якщо файл не починається з `---` або немає закриваючого `\n---`.
47
-
48
- **Логіка:**
49
-
50
- 1. Перевіряє, що файл починається з `---`. Інакше — `null`.
51
- 2. Шукає закриваючий маркер `\n---` починаючи з 4-ї позиції. Якщо нема — `null`.
52
- 3. Розбиває блок front-matter на рядки.
53
- 4. Для кожного рядка: знаходить перше `:`, ділить на `key` / `val`.
54
- 5. Відсікає `key`, який не є простим ідентифікатором (`isSimpleKey`) — тобто рядки з вкладеними структурами, дефісами, цифрами тощо ігноруються.
55
- 6. Відрізає інлайн-коментар у форматі ` #…` (пробіл-решітка).
56
- 7. Тримує значення, прибирає одиничні зовнішні лапки `"`/`'`.
57
- 8. Якщо значення порожнє або дорівнює рядку `'null'` — нормалізує у `null`.
58
-
59
- **Side effects:** немає (чиста функція).
60
-
61
- ### `isSimpleKey(key)` (внутрішня)
62
-
63
- **Сигнатура:** `isSimpleKey(key: string): boolean`
64
-
65
- **Параметри:** `key` — потенційний ключ front-matter.
66
-
67
- **Повертає:** `true`, якщо ключ непорожній і складається тільки з літер `a–z`/`A–Z` та підкреслень. Регулярний вираз: `/[a-z_]/iu` для кожного символу.
68
-
69
- **Side effects:** немає.
70
-
71
- **Призначення:** захист від спроби парсити вкладені структури або не-ідентифікатори (рядки з `-`, цифрами, `.`, тощо).
72
-
73
- ### `analyze(artifacts, resolve)`
74
-
75
- **Сигнатура:**
76
-
77
- ```
78
- analyze(
79
- artifacts: { file: string, fm: Record<string, string | null> }[],
80
- resolve: (target: string, artifactFile: string) => boolean
81
- ): {
82
- file: string,
83
- kind: string | null,
84
- id: string | null,
85
- status: string | null,
86
- links: { field: string, target: string, ok: boolean, breaking: boolean }[]
87
- }[]
88
- ```
89
-
90
- **Параметри:**
91
-
92
- - `artifacts` — масив пар `{ file, fm }`: відносний шлях артефакту та розпарсений front-matter.
93
- - `resolve` — предикат, що повертає `true`, якщо лінк-цільовий файл існує (зазвичай — bound-`resolveLink`).
94
-
95
- **Повертає:** масив аналізованих артефактів. Для кожного:
96
-
97
- - `kind`, `id`, `status` — з front-matter (або `null`);
98
- - `links` — масив об'єктів про кожен наявний лінк:
99
- - `field` — назва поля (один з `LINK_FIELDS`);
100
- - `target` — значення лінка;
101
- - `ok` — чи резолвиться цільовий файл;
102
- - `breaking` — `false`, якщо поле в `INFO_LINK_FIELDS` (зараз — `flow`); `true` для всіх інших.
103
-
104
- **Логіка:** проходить `LINK_FIELDS` у фіксованому порядку, бере тільки поля, наявні у `fm` з truthy-значенням.
105
-
106
- **Side effects:** немає (логіка чиста, `resolve` інкапсулює I/O).
107
-
108
- ### `resolveLink(root, artifactFile, target, exists)` (внутрішня)
109
-
110
- **Сигнатура:** `resolveLink(root: string, artifactFile: string, target: string, exists: (absPath: string) => boolean): boolean`
111
-
112
- **Параметри:**
113
-
114
- - `root` — абсолютний шлях кореня репо;
115
- - `artifactFile` — rel-шлях артефакту (напр. `docs/plans/x.md`);
116
- - `target` — значення лінка з front-matter;
117
- - `exists` — інжекторована перевірка існування файла.
118
-
119
- **Повертає:** `true`, якщо `target` резолвиться **або** відносно теки артефакту (`<root>/<dirname(artifactFile)>/<target>`), **або** root-relative (`<root>/<target>`). Обидві форми вважаються валідними — це конвенція документації (`../specs/…` чи `docs/specs/…`).
120
-
121
- **Side effects:** виклик `exists` (зовнішнє I/O), але інкапсульовано — модуль сам диск не чіпає.
122
-
123
- ### `render(analysis)`
124
-
125
- **Сигнатура:** `render(analysis: ReturnType<typeof analyze>): string`
126
-
127
- **Параметри:** `analysis` — результат `analyze`.
128
-
129
- **Повертає:** багаторядковий текст:
130
-
131
- - Якщо `analysis` порожній — рядок `'trace: артефактів із front-matter не знайдено'`.
132
- - Інакше для кожного артефакту:
133
- - заголовок виду `${kind} · ${id ?? file} [${status ?? '—'}]`;
134
- - вкладені рядки лінків (через `renderLink`), з відступом 3 пробіли.
135
-
136
- **Side effects:** немає.
137
-
138
- ### `renderLink(l)` (внутрішня)
139
-
140
- **Сигнатура:** `renderLink(l: { field: string, target: string, ok: boolean, breaking: boolean }): string`
141
-
142
- **Повертає:** один з трьох форматів:
143
-
144
- - `→ <field>: <target>` — резолвлено успішно (`l.ok === true`);
145
- - `✗ <field>: <target> (РОЗРИВ — файл відсутній)` — нерезолвлене chain-поле (`breaking && !ok`);
146
- - `~ <field>: <target> (runtime-стан — не рве ланцюг)` — нерезолвлене info-поле (`!breaking && !ok`, наприклад `flow`).
147
-
148
- **Side effects:** немає.
149
-
150
- ### `runTraceCli(args, deps?)`
151
-
152
- **Сигнатура:**
153
-
154
- ```
155
- runTraceCli(
156
- args: string[],
157
- deps?: {
158
- cwd?: string,
159
- readdir?: (dir: string) => string[],
160
- readFile?: (file: string) => string,
161
- exists?: (file: string) => boolean,
162
- log?: (m: string) => void
163
- }
164
- ): number
165
- ```
166
-
167
- **Параметри:**
168
-
169
- - `args` — CLI-аргументи (підтримується тільки `--json`);
170
- - `deps` — інжектовані залежності для тестування. Дефолти:
171
- - `cwd` → `process.cwd()`;
172
- - `readdir` → `readdirSync` з охороною `existsSync` (повертає `[]`, якщо каталога нема);
173
- - `readFile` → `readFileSync(file, 'utf8')`;
174
- - `exists` → `existsSync`;
175
- - `log` → `console.log`.
176
-
177
- **Повертає:** exit code:
178
-
179
- - `0` — ланцюг цілісний (немає breaking-розривів);
180
- - `1` — є хоча б один breaking-лінк, що не резолвиться.
181
-
182
- **Side effects:**
183
-
184
- - Читання FS через `readdir` / `readFile` / `exists` (інжектабельне).
185
- - Виклик `log` — за замовчуванням друк у `stdout`.
186
- - Жодних мутацій FS, мережі або стану процесу.
187
-
188
- **Логіка покроково:**
189
-
190
- 1. Резолвить `root`, `readdir`, `readFile`, `exists`, `log` з `deps` або дефолтів.
191
- 2. Проходить кожен каталог з `DIRS`.
192
- 3. У кожному каталозі бере файли з розширенням `.md`.
193
- 4. Для кожного `.md`-файла:
194
- - читає вміст через `readFile`;
195
- - парсить front-matter через `parseFrontMatter`;
196
- - якщо парсинг успішний і у front-matter є `id` або `kind` — додає в `artifacts`.
197
- 5. Викликає `analyze(artifacts, resolve)`, де `resolve` — частково застосований `resolveLink(root, file, target, exists)`.
198
- 6. Друкує результат: `JSON.stringify(analysis, null, 2)` для `--json`, інакше `render(analysis)`.
199
- 7. Повертає `1`, якщо існує лінк з `breaking && !ok`, інакше `0`. Нерезолвлений `flow` (info-поле) ігнорується для exit code.
200
-
201
- ## Залежності
202
-
203
- ### Системні (Node.js)
204
-
205
- - `node:fs` — `existsSync`, `readdirSync`, `readFileSync` (використовуються тільки як дефолтні значення для `deps`).
206
- - `node:path` — `dirname`, `join` для побудови шляхів у `resolveLink` та CLI.
207
- - `node:process` — `cwd` (alias-імпорт `processCwd`) для дефолтного кореня.
208
-
209
- ### Внутрішньопроєктні
210
-
211
- - Жодних. Модуль автономний.
212
-
213
- ### Зовнішні / npm
214
-
215
- - Жодних.
216
-
217
- ### Споживачі
218
-
219
- Модуль викликається з диспатчера `n-cursor` як CLI-команда `n-cursor trace`. Експортовані `parseFrontMatter`, `analyze`, `render` доступні для повторного використання іншими інструментами (напр. для дашбордів простежуваності або юніт-тестів).
220
-
221
- ## Потік виконання / Використання
222
-
223
- ### CLI
224
-
225
- ```
226
- n-cursor trace
227
- n-cursor trace --json
228
- ```
229
-
230
- **Текстовий приклад виводу:**
231
-
232
- ```
233
- spec · NSPEC-42 [draft]
234
- → adr: ../adr/NADR-7.md
235
- ✗ plan: ../plans/NPLAN-99.md (РОЗРИВ — файл відсутній)
236
- ~ flow: ../../.worktrees/feat-x.flow.json (runtime-стан — не рве ланцюг)
237
- plan · NPLAN-12 [active]
238
- → spec: ../specs/NSPEC-42.md
239
- ```
240
-
241
- **JSON-приклад:**
242
-
243
- ```
244
- [
245
- {
246
- "file": "docs/specs/NSPEC-42.md",
247
- "kind": "spec",
248
- "id": "NSPEC-42",
249
- "status": "draft",
250
- "links": [
251
- { "field": "adr", "target": "../adr/NADR-7.md", "ok": true, "breaking": true },
252
- { "field": "plan", "target": "../plans/NPLAN-99.md", "ok": false, "breaking": true },
253
- { "field": "flow", "target": "../../.worktrees/x.flow.json","ok": false, "breaking": false }
254
- ]
255
- }
256
- ]
257
- ```
258
-
259
- ### Алгоритм (псевдо-flowchart)
260
-
261
- 1. **DIR walk** — `for dir in DIRS: for name in readdir(root/dir)`.
262
- 2. **Filter** — лише `*.md`.
263
- 3. **Parse** — `parseFrontMatter(readFile(...))`.
264
- 4. **Filter artifacts** — лише ті, що мають `fm.id` або `fm.kind`.
265
- 5. **Analyze** — для кожного артефакту перетворити front-matter-лінки на `{ field, target, ok, breaking }`.
266
- 6. **Render / JSON** — серіалізація.
267
- 7. **Exit code** — `1` якщо `any(link.breaking && !link.ok)`, інакше `0`.
268
-
269
- ### Програмне використання
270
-
271
- ```
272
- import { parseFrontMatter, analyze, render, runTraceCli } from './trace.mjs'
273
-
274
- // Як CLI з мок-FS
275
- const code = runTraceCli(['--json'], {
276
- cwd: '/repo',
277
- readdir: (dir) => fakeFs[dir] ?? [],
278
- readFile: (file) => fakeFs[file],
279
- exists: (file) => file in fakeFs,
280
- log: (msg) => collected.push(msg)
281
- })
282
-
283
- // Як бібліотека (без I/O)
284
- const fm = parseFrontMatter('---\nid: NSPEC-1\nkind: spec\nplan: ../plans/NPLAN-1.md\n---\n# …')
285
- const result = analyze([{ file: 'docs/specs/x.md', fm }], () => true)
286
- console.log(render(result))
287
- ```
288
-
289
- ### Семантика exit code
290
-
291
- - `0` — всі **chain**-лінки (`adr`, `spec`, `plan`, `change`, `task`) резолвляться.
292
- - `1` — є хоча б один chain-лінк, що **не** резолвиться. Поле `flow` ніколи не впливає на код виходу.
293
-
294
- ### Тестованість
295
-
296
- Через повну ін'єкцію `cwd`/`readdir`/`readFile`/`exists`/`log` модуль покривається юніт-тестами без файлової системи. Чисті функції `parseFrontMatter`, `analyze`, `render` тестуються прямо на in-memory даних.
@@ -1,112 +0,0 @@
1
- /**
2
- * `n-cursor graph init <name>` — створює task.md шаблон для нового вузла.
3
- *
4
- * Не потребує LLM. Просто пише task.md з front-matter і порожнім тілом.
5
- * Ім'я може містити `/` для вкладених вузлів (напр. "research/collect-data").
6
- *
7
- * FS ін'єктується для тестованості.
8
- */
9
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
10
- import { join } from 'node:path'
11
- import { cwd as processCwd } from 'node:process'
12
-
13
- import { buildMarkdown } from './frontmatter.mjs'
14
- import { loadConfig, resolveTasksDir } from './config.mjs'
15
-
16
- /**
17
- * Будує front-matter для task.md шаблону.
18
- * @param {{ now: string, name: string }} params параметри
19
- * @returns {Record<string, unknown>} front-matter об'єкт
20
- */
21
- export function buildTaskFrontMatter(params) {
22
- return {
23
- created_at: params.now,
24
- budget_sec: 600,
25
- mode: 'human',
26
- interactive: true,
27
- executor: {
28
- type: 'agent',
29
- model_tier: 'AVG',
30
- skills: ['bash', 'write-files']
31
- },
32
- hint: 'atomic',
33
- deps: []
34
- }
35
- }
36
-
37
- /**
38
- * `graph init <name>` command handler.
39
- * @param {string[]} args аргументи: [name]
40
- * @param {{
41
- * cwd?: string,
42
- * log?: (m: string) => void,
43
- * writeFile?: (p: string, c: string, enc: string) => void,
44
- * exists?: (p: string) => boolean,
45
- * mkdir?: (p: string, opts?: object) => void,
46
- * now?: () => string,
47
- * readFile?: (p: string, enc: string) => string
48
- * }} [deps] ін'єкції
49
- * @returns {Promise<number>} exit code
50
- */
51
- export async function cmdInit(args, deps = {}) {
52
- const root = deps.cwd ?? processCwd()
53
- const log = deps.log ?? console.log
54
- const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
55
- const exists = deps.exists ?? existsSync
56
- const mkdir = deps.mkdir ?? ((p, opts) => mkdirSync(p, opts))
57
- const nowFn = deps.now ?? (() => new Date().toISOString())
58
-
59
- const [name] = args
60
- if (!name) {
61
- log('Usage: n-cursor graph init <name>')
62
- log(' name може містити / для вкладених вузлів (напр. "research/collect-data")')
63
- return 1
64
- }
65
-
66
- const config = loadConfig({ root, readFile: deps.readFile, exists })
67
- const tasksDir = resolveTasksDir(config, root)
68
-
69
- const nodeDir = join(tasksDir, name)
70
- const taskPath = join(nodeDir, 'task.md')
71
-
72
- if (exists(taskPath)) {
73
- log(`init: ${taskPath} вже існує — пропускаємо`)
74
- return 0
75
- }
76
-
77
- // Створюємо директорію рекурсивно
78
- try {
79
- mkdir(nodeDir, { recursive: true })
80
- } catch (err) {
81
- log(`init: не вдалося створити директорію ${nodeDir} — ${err.message ?? String(err)}`)
82
- return 1
83
- }
84
-
85
- const fm = buildTaskFrontMatter({ now: nowFn(), name })
86
- const body = [
87
- `## Mission`,
88
- ``,
89
- `<!-- Опишіть завдання вузла тут -->`,
90
- ``,
91
- `## Done when`,
92
- ``,
93
- `<!-- Критерії успіху -->`,
94
- ``,
95
- `## Context`,
96
- ``,
97
- `<!-- Додатковий контекст для виконавця -->`,
98
- ``
99
- ].join('\n')
100
-
101
- const content = buildMarkdown(fm, body)
102
-
103
- try {
104
- writeFile(taskPath, content, 'utf8')
105
- log(`init: створено ${taskPath}`)
106
- } catch (err) {
107
- log(`init: не вдалося записати ${taskPath} — ${err.message ?? String(err)}`)
108
- return 1
109
- }
110
-
111
- return 0
112
- }
@@ -1,96 +0,0 @@
1
- /**
2
- * `n-cursor graph invalidate <path> [--no-cascade]` — позначає вузол як invalidated.
3
- *
4
- * Записує порожній файл `invalidated` у директорію вузла.
5
- * За замовчуванням каскадно інвалідує всі залежні вузли.
6
- * --no-cascade — лише поточний вузол.
7
- *
8
- * FS ін'єктується для тестованості.
9
- */
10
- import { execSync } from 'node:child_process'
11
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
12
- import { join } from 'node:path'
13
- import { cwd as processCwd } from 'node:process'
14
-
15
- import { loadConfig, resolveTasksDir } from './config.mjs'
16
- import { scanNodes } from './scanner.mjs'
17
- import { listActiveWorktrees } from './worktree-ops.mjs'
18
-
19
- /**
20
- * `graph invalidate <path> [--no-cascade]` command handler.
21
- * @param {string[]} args аргументи
22
- * @param {{
23
- * cwd?: string,
24
- * log?: (m: string) => void,
25
- * readFile?: (p: string, enc: string) => string,
26
- * writeFile?: (p: string, c: string, enc: string) => void,
27
- * readdir?: (d: string) => string[],
28
- * exists?: (p: string) => boolean,
29
- * execSync?: (cmd: string, opts?: object) => string
30
- * }} [deps] ін'єкції
31
- * @returns {Promise<number>} exit code
32
- */
33
- export async function cmdInvalidate(args, deps = {}) {
34
- const root = deps.cwd ?? processCwd()
35
- const log = deps.log ?? console.log
36
- const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
37
- const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
38
- const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
39
- const exists = deps.exists ?? existsSync
40
- const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
41
-
42
- let nodePath = null
43
- let noCascade = false
44
-
45
- for (const arg of args) {
46
- if (arg === '--no-cascade') noCascade = true
47
- else if (!arg.startsWith('-')) nodePath = arg
48
- }
49
-
50
- if (!nodePath) {
51
- log('Usage: n-cursor graph invalidate <path> [--no-cascade]')
52
- return 1
53
- }
54
-
55
- const config = loadConfig({ root, readFile, exists })
56
- const tasksDir = resolveTasksDir(config, root)
57
- const nodeDir = join(tasksDir, nodePath)
58
-
59
- if (!exists(join(nodeDir, 'task.md'))) {
60
- log(`invalidate: вузол "${nodePath}" не знайдено`)
61
- return 1
62
- }
63
-
64
- // Записуємо invalidated sentinel
65
- try {
66
- writeFile(join(nodeDir, 'invalidated'), '', 'utf8')
67
- log(`invalidate: вузол "${nodePath}" інвалідовано`)
68
- } catch (err) {
69
- log(`invalidate: не вдалося записати invalidated — ${err.message ?? String(err)}`)
70
- return 1
71
- }
72
-
73
- if (noCascade) return 0
74
-
75
- // Каскадна інвалідація
76
- const activeWorktrees = listActiveWorktrees(root, { execSync: execSyncFn })
77
- const allNodes = scanNodes(tasksDir, activeWorktrees, {
78
- readdirSync: readdir,
79
- existsSync: exists,
80
- readFileSync: readFile
81
- })
82
-
83
- const dependents = allNodes.filter(n => n.deps.includes(nodePath))
84
- for (const dep of dependents) {
85
- if (!exists(join(dep.dir, 'invalidated'))) {
86
- try {
87
- writeFile(join(dep.dir, 'invalidated'), '', 'utf8')
88
- log(`invalidate: каскадна інвалідація "${dep.path}"`)
89
- } catch {
90
- // пропускаємо
91
- }
92
- }
93
- }
94
-
95
- return 0
96
- }
@@ -1,141 +0,0 @@
1
- /**
2
- * `n-cursor graph kill <path>` — вбиває worktree вузла і каскадно інвалідує нащадків.
3
- *
4
- * 1. Знаходить worktree вузла
5
- * 2. Видаляє worktree (force)
6
- * 3. Видаляє plan_*.md (скидає планування)
7
- * 4. Записує invalidated sentinel
8
- * 5. Каскадно інвалідує всі залежні вузли
9
- *
10
- * FS і child_process ін'єктуються для тестованості.
11
- */
12
- import { execSync } from 'node:child_process'
13
- import { existsSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs'
14
- import { join } from 'node:path'
15
- import { cwd as processCwd } from 'node:process'
16
-
17
- import { loadConfig, resolveTasksDir, resolveWorktreesDir } from './config.mjs'
18
- import { scanNodes } from './scanner.mjs'
19
- import { findNodeWorktree, listActiveWorktrees, removeWorktree } from './worktree-ops.mjs'
20
-
21
- /**
22
- * Записує invalidated sentinel для вузла.
23
- * @param {string} nodeDir директорія вузла
24
- * @param {(p: string, c: string, enc: string) => void} writeFile функція запису
25
- */
26
- function writeInvalidated(nodeDir, writeFile) {
27
- writeFile(join(nodeDir, 'invalidated'), '', 'utf8')
28
- }
29
-
30
- /**
31
- * Видаляє plan_*.md файли з директорії вузла.
32
- * @param {string} nodeDir директорія вузла
33
- * @param {string[]} files список файлів
34
- * @param {(p: string) => void} unlink функція видалення
35
- */
36
- function deletePlanFiles(nodeDir, files, unlink) {
37
- for (const f of files) {
38
- if (/^plan_\d+\.md$/.test(f)) {
39
- try {
40
- unlink(join(nodeDir, f))
41
- } catch {
42
- // пропускаємо
43
- }
44
- }
45
- }
46
- }
47
-
48
- /**
49
- * `graph kill <path>` command handler.
50
- * @param {string[]} args аргументи: [path]
51
- * @param {{
52
- * cwd?: string,
53
- * log?: (m: string) => void,
54
- * readFile?: (p: string, enc: string) => string,
55
- * writeFile?: (p: string, c: string, enc: string) => void,
56
- * readdir?: (d: string) => string[],
57
- * exists?: (p: string) => boolean,
58
- * unlink?: (p: string) => void,
59
- * execSync?: (cmd: string, opts?: object) => string
60
- * }} [deps] ін'єкції
61
- * @returns {Promise<number>} exit code
62
- */
63
- export async function cmdKill(args, deps = {}) {
64
- const root = deps.cwd ?? processCwd()
65
- const log = deps.log ?? console.log
66
- const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
67
- const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
68
- const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
69
- const exists = deps.exists ?? existsSync
70
- const unlink = deps.unlink ?? unlinkSync
71
- const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
72
-
73
- const [nodePath] = args
74
- if (!nodePath) {
75
- log('Usage: n-cursor graph kill <path>')
76
- return 1
77
- }
78
-
79
- const config = loadConfig({ root, readFile, exists })
80
- const tasksDir = resolveTasksDir(config, root)
81
- const worktreesDir = resolveWorktreesDir(config, root)
82
-
83
- const nodeDir = join(tasksDir, nodePath)
84
- if (!exists(join(nodeDir, 'task.md'))) {
85
- log(`kill: вузол "${nodePath}" не знайдено`)
86
- return 1
87
- }
88
-
89
- // 1. Знаходимо і видаляємо worktree
90
- const worktreePath = findNodeWorktree(nodePath, worktreesDir, {
91
- readdirSync: readdir,
92
- execSync: execSyncFn
93
- })
94
-
95
- if (worktreePath) {
96
- log(`kill: видаляємо worktree ${worktreePath}`)
97
- removeWorktree(worktreePath, root, { execSync: execSyncFn })
98
- } else {
99
- log(`kill: worktree не знайдено для "${nodePath}"`)
100
- }
101
-
102
- // 2. Видаляємо plan_*.md
103
- const files = readdir(nodeDir)
104
- deletePlanFiles(nodeDir, files, unlink)
105
- const planCount = files.filter(f => /^plan_\d+\.md$/.test(f)).length
106
- if (planCount > 0) {
107
- log(`kill: видалено ${planCount} plan_*.md файл(ів)`)
108
- }
109
-
110
- // 3. Записуємо invalidated sentinel
111
- try {
112
- writeInvalidated(nodeDir, writeFile)
113
- log(`kill: вузол "${nodePath}" інвалідовано`)
114
- } catch (err) {
115
- log(`kill: не вдалося записати invalidated — ${err.message ?? String(err)}`)
116
- return 1
117
- }
118
-
119
- // 4. Каскадна інвалідація залежних вузлів
120
- const activeWorktrees = listActiveWorktrees(root, { execSync: execSyncFn })
121
- const allNodes = scanNodes(tasksDir, activeWorktrees, {
122
- readdirSync: readdir,
123
- existsSync: exists,
124
- readFileSync: readFile
125
- })
126
-
127
- // Знаходимо вузли що залежать від нашого вузла
128
- const dependents = allNodes.filter(n => n.deps.includes(nodePath))
129
- for (const dep of dependents) {
130
- if (!exists(join(dep.dir, 'invalidated'))) {
131
- try {
132
- writeInvalidated(dep.dir, writeFile)
133
- log(`kill: каскадна інвалідація "${dep.path}"`)
134
- } catch {
135
- // пропускаємо
136
- }
137
- }
138
- }
139
-
140
- return 0
141
- }