@nitra/cursor 4.1.0 → 4.1.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.
- package/CHANGELOG.md +12 -0
- package/bin/n-cursor.js +25 -13
- package/docs/flow.MD +1364 -0
- package/docs/stryker.config.md +37 -0
- package/docs/vitest.config.md +23 -0
- package/lib/models.mjs +1 -2
- package/package.json +2 -1
- package/rules/abie/fix.mjs +1 -1
- package/rules/bun/docs/fix.md +3 -0
- package/rules/bun/fix.mjs +1 -1
- package/rules/capacitor/fix.mjs +1 -1
- package/rules/changelog/docs/fix.md +3 -0
- package/rules/changelog/fix.mjs +1 -1
- package/rules/ci4/fix.mjs +1 -1
- package/rules/ci4/js/docs/marksman_config.md +1 -0
- package/rules/docker/docs/fix.md +1 -1
- package/rules/docker/fix.mjs +1 -1
- package/rules/docker/lint/docs/lint.md +1 -0
- package/rules/efes/docs/fix.md +2 -1
- package/rules/efes/fix.mjs +1 -1
- package/rules/feedback/fix.mjs +1 -1
- package/rules/ga/fix.mjs +1 -1
- package/rules/ga/js/lint.mjs +1 -1
- package/rules/graphql/docs/fix.md +4 -1
- package/rules/graphql/fix.mjs +1 -1
- package/rules/graphql/lib/docs/graphql-gql-scan.md +3 -0
- package/rules/hasura/fix.mjs +1 -1
- package/rules/image-avif/docs/fix.md +4 -1
- package/rules/image-avif/fix.mjs +1 -1
- package/rules/image-avif/js/docs/avif_generation.md +1 -0
- package/rules/image-compress/fix.mjs +1 -1
- package/rules/js-bun-db/fix.mjs +1 -1
- package/rules/js-bun-db/lib/docs/bun-sql-scan.md +6 -0
- package/rules/js-bun-redis/fix.mjs +1 -1
- package/rules/js-lint/fix.mjs +1 -1
- package/rules/js-lint/js/docs/utils_imports.md +1 -0
- package/rules/js-lint-ci/docs/fix.md +4 -1
- package/rules/js-lint-ci/fix.mjs +1 -1
- package/rules/js-mssql/docs/fix.md +3 -0
- package/rules/js-mssql/fix.mjs +1 -1
- package/rules/js-mssql/lib/docs/mssql-pool-scan.md +9 -0
- package/rules/js-run/docs/fix.md +3 -0
- package/rules/js-run/fix.mjs +1 -1
- package/rules/js-run/lib/docs/check-env-scan.md +2 -1
- package/rules/js-run/lib/docs/promise-settimeout-scan.md +4 -0
- package/rules/k8s/docs/fix.md +3 -0
- package/rules/k8s/fix.mjs +1 -1
- package/rules/nginx-default-tpl/docs/fix.md +3 -0
- package/rules/nginx-default-tpl/fix.mjs +1 -1
- package/rules/npm-module/fix.mjs +1 -1
- package/rules/npm-module/js/header_doc_pointer.mjs +14 -3
- package/rules/php/docs/fix.md +2 -1
- package/rules/php/fix.mjs +1 -1
- package/rules/python/docs/fix.md +4 -1
- package/rules/python/fix.mjs +1 -1
- package/rules/rego/fix.mjs +1 -1
- package/rules/rego/js/lint.mjs +1 -1
- package/rules/release/docs/fix.md +4 -1
- package/rules/release/fix.mjs +1 -1
- package/rules/rust/fix.mjs +1 -1
- package/rules/security/docs/fix.md +1 -1
- package/rules/security/fix.mjs +1 -1
- package/rules/style-lint/docs/fix.md +3 -0
- package/rules/style-lint/fix.mjs +1 -1
- package/rules/tauri/docs/fix.md +4 -1
- package/rules/tauri/fix.mjs +1 -1
- package/rules/test/docs/fix.md +3 -0
- package/rules/test/fix.mjs +1 -1
- package/rules/test/js/no-relative-fs-path.mjs +2 -1
- package/rules/text/docs/fix.md +3 -0
- package/rules/text/fix.mjs +1 -1
- package/rules/text/js/lint.mjs +1 -1
- package/rules/vue/fix.mjs +1 -1
- package/rules/worktree/fix.mjs +1 -1
- package/scripts/auto-rules.mjs +1 -1
- package/scripts/coverage-classify/index.mjs +10 -10
- package/scripts/coverage-fix.mjs +2 -2
- package/scripts/dispatcher/graph/lib/cmd-init.mjs +112 -0
- package/scripts/dispatcher/graph/lib/cmd-invalidate.mjs +96 -0
- package/scripts/dispatcher/graph/lib/cmd-kill.mjs +141 -0
- package/scripts/dispatcher/graph/lib/cmd-plan.mjs +142 -0
- package/scripts/dispatcher/graph/lib/cmd-run.mjs +328 -0
- package/scripts/dispatcher/graph/lib/cmd-scan.mjs +115 -0
- package/scripts/dispatcher/graph/lib/cmd-setup.mjs +111 -0
- package/scripts/dispatcher/graph/lib/cmd-signals.mjs +328 -0
- package/scripts/dispatcher/graph/lib/cmd-status.mjs +131 -0
- package/scripts/dispatcher/graph/lib/cmd-verify.mjs +100 -0
- package/scripts/dispatcher/graph/lib/cmd-watch.mjs +128 -0
- package/scripts/dispatcher/graph/lib/config.mjs +103 -0
- package/scripts/dispatcher/graph/lib/frontmatter.mjs +224 -0
- package/scripts/dispatcher/graph/lib/nnn.mjs +127 -0
- package/scripts/dispatcher/graph/lib/node-state.mjs +157 -0
- package/scripts/dispatcher/graph/lib/scanner.mjs +235 -0
- package/scripts/dispatcher/graph/lib/worktree-ops.mjs +193 -0
- package/scripts/dispatcher/graph-tasks.mjs +92 -0
- package/scripts/dispatcher/index.mjs +3 -3
- package/scripts/dispatcher/lib/docs/events.md +1 -0
- package/scripts/dispatcher/lib/executor.mjs +1 -1
- package/scripts/dispatcher/lib/subagent-runner.mjs +9 -9
- package/scripts/dispatcher/trace.mjs +6 -2
- package/scripts/docs/build-agents-commands.md +1 -0
- package/scripts/docs/cli-entry.md +6 -0
- package/scripts/graph/index.mjs +115 -0
- package/scripts/graph/lib/config.mjs +62 -0
- package/scripts/graph/lib/dag.mjs +161 -0
- package/scripts/graph/lib/frontmatter.mjs +70 -0
- package/scripts/graph/lib/nnn.mjs +77 -0
- package/scripts/graph/lib/state.mjs +110 -0
- package/scripts/graph/scan.mjs +64 -0
- package/scripts/graph/status.mjs +86 -0
- package/scripts/lib/docs/load-cursor-config.md +3 -0
- package/scripts/lib/root-notice.mjs +4 -2
- package/scripts/lib/rule-predicates.mjs +1 -1
- package/scripts/lib/worktree-notice.mjs +14 -7
- package/scripts/lib/worktree.mjs +3 -2
- package/scripts/utils/resolve-js-root.mjs +2 -1
- package/scripts/utils/with-lock.mjs +1 -1
- package/skills/docgen/js/docgen-batch.mjs +7 -7
- package/skills/docgen/js/docgen-extract.mjs +80 -37
- package/skills/docgen/js/docgen-ignore.mjs +1 -1
- package/skills/docgen/js/docgen-prompts.mjs +21 -5
- package/skills/fix/js/llm-worker.mjs +19 -22
- package/skills/fix/js/orchestrator.mjs +6 -7
- package/skills/fix/js/t0.mjs +14 -13
- package/types/bin/n-cursor.d.ts +1 -1
- package/rules/flow/docs/fix.md +0 -152
- package/rules/flow/fix.mjs +0 -18
- package/rules/flow/flow.mdc +0 -127
- package/rules/flow/meta.json +0 -1
- package/scripts/dispatcher/lib/docs/flow-lock.md +0 -161
- package/scripts/dispatcher/lib/docs/flow-resolve.md +0 -267
- package/scripts/dispatcher/lib/flow-plan.mjs +0 -153
- package/scripts/dispatcher/lib/flow-resolve.mjs +0 -156
- package/scripts/dispatcher/lib/flow-signals.mjs +0 -235
- package/scripts/dispatcher/lib/flow-verify.mjs +0 -127
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `n-cursor graph scan [--json]` — повний скан DAG, exit 1 якщо є failed-вузли.
|
|
3
|
+
*
|
|
4
|
+
* Обходить весь DAG, деривує стани, виводить зведення.
|
|
5
|
+
* exit 0 = все чисто (або лише needs-plan/waiting)
|
|
6
|
+
* exit 1 = є failed або pending-audit без відповіді
|
|
7
|
+
*
|
|
8
|
+
* FS ін'єктується для тестованості.
|
|
9
|
+
*/
|
|
10
|
+
import { execSync } from 'node:child_process'
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } 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, topoSort, areDepsResolved } from './scanner.mjs'
|
|
17
|
+
import { listActiveWorktrees } from './worktree-ops.mjs'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `graph scan [--json]` command handler.
|
|
21
|
+
* @param {string[]} args аргументи
|
|
22
|
+
* @param {{
|
|
23
|
+
* cwd?: string,
|
|
24
|
+
* log?: (m: string) => void,
|
|
25
|
+
* readFile?: (p: string, enc: string) => string,
|
|
26
|
+
* readdir?: (d: string) => string[],
|
|
27
|
+
* exists?: (p: string) => boolean,
|
|
28
|
+
* execSync?: (cmd: string, opts?: object) => string
|
|
29
|
+
* }} [deps] ін'єкції
|
|
30
|
+
* @returns {Promise<number>} exit code (0=clean, 1=attention)
|
|
31
|
+
*/
|
|
32
|
+
export async function cmdScan(args, deps = {}) {
|
|
33
|
+
const root = deps.cwd ?? processCwd()
|
|
34
|
+
const log = deps.log ?? console.log
|
|
35
|
+
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
36
|
+
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
37
|
+
const exists = deps.exists ?? existsSync
|
|
38
|
+
const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, { ...opts, encoding: 'utf8' }))
|
|
39
|
+
|
|
40
|
+
const jsonMode = args.includes('--json')
|
|
41
|
+
|
|
42
|
+
const config = loadConfig({ root, readFile, exists })
|
|
43
|
+
const tasksDir = resolveTasksDir(config, root)
|
|
44
|
+
|
|
45
|
+
const activeWorktrees = listActiveWorktrees(root, { execSync: execSyncFn })
|
|
46
|
+
|
|
47
|
+
const allNodes = scanNodes(tasksDir, activeWorktrees, {
|
|
48
|
+
readdirSync: readdir,
|
|
49
|
+
existsSync: exists,
|
|
50
|
+
readFileSync: readFile
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const sorted = topoSort(allNodes)
|
|
54
|
+
|
|
55
|
+
// Підрахунок по станах
|
|
56
|
+
const stateCounts = {}
|
|
57
|
+
for (const n of sorted) {
|
|
58
|
+
stateCounts[n.state] = (stateCounts[n.state] ?? 0) + 1
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Знаходимо проблемні вузли
|
|
62
|
+
const failed = sorted.filter(n => n.state === 'failed')
|
|
63
|
+
const pendingAudit = sorted.filter(n => n.state === 'pending-audit')
|
|
64
|
+
const needsPlan = sorted.filter(n => n.state === 'needs-plan')
|
|
65
|
+
|
|
66
|
+
// Знаходимо готові до запуску (waiting + deps resolved)
|
|
67
|
+
const nodeMap = new Map(sorted.map(n => [n.id, n]))
|
|
68
|
+
const ready = sorted.filter(n => n.state === 'waiting' && areDepsResolved(n, nodeMap))
|
|
69
|
+
|
|
70
|
+
const hasProblems = failed.length > 0
|
|
71
|
+
|
|
72
|
+
if (jsonMode) {
|
|
73
|
+
console.log(JSON.stringify({
|
|
74
|
+
ok: !hasProblems,
|
|
75
|
+
total: sorted.length,
|
|
76
|
+
counts: stateCounts,
|
|
77
|
+
failed: failed.map(n => n.path),
|
|
78
|
+
pending_audit: pendingAudit.map(n => n.path),
|
|
79
|
+
needs_plan: needsPlan.map(n => n.path),
|
|
80
|
+
ready: ready.map(n => n.path)
|
|
81
|
+
}, null, 2))
|
|
82
|
+
} else {
|
|
83
|
+
const summaryParts = Object.entries(stateCounts)
|
|
84
|
+
.map(([s, c]) => `${s}:${c}`)
|
|
85
|
+
.join(' ')
|
|
86
|
+
|
|
87
|
+
log(`scan: ${sorted.length} вузлів — ${summaryParts}`)
|
|
88
|
+
|
|
89
|
+
if (failed.length > 0) {
|
|
90
|
+
log(`\nFAILED (${failed.length}):`)
|
|
91
|
+
for (const n of failed) log(` - ${n.path}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (pendingAudit.length > 0) {
|
|
95
|
+
log(`\npending-audit (${pendingAudit.length}):`)
|
|
96
|
+
for (const n of pendingAudit) log(` - ${n.path}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (needsPlan.length > 0) {
|
|
100
|
+
log(`\nneeds-plan (${needsPlan.length}):`)
|
|
101
|
+
for (const n of needsPlan) log(` - ${n.path}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (ready.length > 0) {
|
|
105
|
+
log(`\nready to run (${ready.length}):`)
|
|
106
|
+
for (const n of ready) log(` - ${n.path}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!hasProblems && failed.length === 0) {
|
|
110
|
+
log('\nscan: OK')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return hasProblems ? 1 : 0
|
|
115
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `n-cursor graph setup` — ініціалізація проєкту для graph task system.
|
|
3
|
+
*
|
|
4
|
+
* Створює:
|
|
5
|
+
* - .n-cursor.json з дефолтними налаштуваннями (якщо не існує)
|
|
6
|
+
* - tasks/ директорію
|
|
7
|
+
* - git hook (post-commit) для автоматичного оновлення стану (якщо є .git)
|
|
8
|
+
*
|
|
9
|
+
* FS ін'єктується для тестованості.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { cwd as processCwd } from 'node:process'
|
|
14
|
+
|
|
15
|
+
import { CONFIG_DEFAULTS } from './config.mjs'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `graph setup` command handler.
|
|
19
|
+
* @param {string[]} _args аргументи (не використовуються)
|
|
20
|
+
* @param {{
|
|
21
|
+
* cwd?: string,
|
|
22
|
+
* log?: (m: string) => void,
|
|
23
|
+
* writeFile?: (p: string, c: string, enc: string) => void,
|
|
24
|
+
* readFile?: (p: string, enc: string) => string,
|
|
25
|
+
* exists?: (p: string) => boolean,
|
|
26
|
+
* mkdir?: (p: string, opts?: object) => void
|
|
27
|
+
* }} [deps] ін'єкції
|
|
28
|
+
* @returns {Promise<number>} exit code
|
|
29
|
+
*/
|
|
30
|
+
export async function cmdSetup(_args, deps = {}) {
|
|
31
|
+
const root = deps.cwd ?? processCwd()
|
|
32
|
+
const log = deps.log ?? console.log
|
|
33
|
+
const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
|
|
34
|
+
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
35
|
+
const exists = deps.exists ?? existsSync
|
|
36
|
+
const mkdir = deps.mkdir ?? ((p, opts) => mkdirSync(p, opts))
|
|
37
|
+
|
|
38
|
+
// 1. Створюємо .n-cursor.json якщо не існує
|
|
39
|
+
const configPath = join(root, '.n-cursor.json')
|
|
40
|
+
if (!exists(configPath)) {
|
|
41
|
+
try {
|
|
42
|
+
writeFile(configPath, JSON.stringify(CONFIG_DEFAULTS, null, 2) + '\n', 'utf8')
|
|
43
|
+
log(`setup: створено ${configPath}`)
|
|
44
|
+
} catch (err) {
|
|
45
|
+
log(`setup: не вдалося створити ${configPath} — ${err.message ?? String(err)}`)
|
|
46
|
+
return 1
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
log(`setup: ${configPath} вже існує — пропускаємо`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. Створюємо tasks/ директорію
|
|
53
|
+
const tasksDir = join(root, 'tasks')
|
|
54
|
+
if (!exists(tasksDir)) {
|
|
55
|
+
try {
|
|
56
|
+
mkdir(tasksDir, { recursive: true })
|
|
57
|
+
log(`setup: створено ${tasksDir}`)
|
|
58
|
+
} catch (err) {
|
|
59
|
+
log(`setup: не вдалося створити ${tasksDir} — ${err.message ?? String(err)}`)
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
log(`setup: ${tasksDir} вже існує — пропускаємо`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 3. Створюємо .n-cursor/ директорію
|
|
67
|
+
const ncursorDir = join(root, '.n-cursor')
|
|
68
|
+
if (!exists(ncursorDir)) {
|
|
69
|
+
try {
|
|
70
|
+
mkdir(ncursorDir, { recursive: true })
|
|
71
|
+
log(`setup: створено ${ncursorDir}`)
|
|
72
|
+
} catch (err) {
|
|
73
|
+
log(`setup: не вдалося створити ${ncursorDir} — ${err.message ?? String(err)}`)
|
|
74
|
+
return 1
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 4. Перевіряємо чи є .git і додаємо hook
|
|
79
|
+
const gitDir = join(root, '.git')
|
|
80
|
+
if (exists(gitDir)) {
|
|
81
|
+
const hooksDir = join(gitDir, 'hooks')
|
|
82
|
+
try {
|
|
83
|
+
mkdir(hooksDir, { recursive: true })
|
|
84
|
+
} catch {
|
|
85
|
+
// hooks/ може вже існувати
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const hookPath = join(hooksDir, 'post-commit')
|
|
89
|
+
if (!exists(hookPath)) {
|
|
90
|
+
const hookContent = [
|
|
91
|
+
'#!/bin/sh',
|
|
92
|
+
'# n-cursor graph: automatic state refresh after commit',
|
|
93
|
+
'npx @nitra/cursor graph scan --json > /dev/null 2>&1 || true',
|
|
94
|
+
''
|
|
95
|
+
].join('\n')
|
|
96
|
+
try {
|
|
97
|
+
writeFile(hookPath, hookContent, 'utf8')
|
|
98
|
+
// chmod +x через окрему команду не робимо — залежить від FS dep
|
|
99
|
+
log(`setup: створено git hook ${hookPath}`)
|
|
100
|
+
} catch (err) {
|
|
101
|
+
log(`setup: не вдалося створити git hook — ${err.message ?? String(err)}`)
|
|
102
|
+
// Не критично — продовжуємо
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
log(`setup: git hook ${hookPath} вже існує — пропускаємо`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
log('setup: готово')
|
|
110
|
+
return 0
|
|
111
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Сигнальні команди: `done`, `audit`, `failed`, `spawn`.
|
|
3
|
+
*
|
|
4
|
+
* Ці команди викликаються зсередини worktree (агентом або скриптом),
|
|
5
|
+
* або зовні через `n-cursor graph done|audit|failed|spawn <path>`.
|
|
6
|
+
*
|
|
7
|
+
* done → записує run_NNN.md (result:success), мерджить worktree
|
|
8
|
+
* audit → знаходить latest fact_NNN.md, створює pending-audit_NNN.md,
|
|
9
|
+
* записує run_NNN.md, мерджить worktree
|
|
10
|
+
* failed → записує run_NNN.md (result:failed), залишає worktree
|
|
11
|
+
* spawn → перевіряє що дочірні вузли зареєстровані (мають task.md)
|
|
12
|
+
*
|
|
13
|
+
* FS і child_process ін'єктуються для тестованості.
|
|
14
|
+
*/
|
|
15
|
+
import { execSync } from 'node:child_process'
|
|
16
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { cwd as processCwd } from 'node:process'
|
|
19
|
+
|
|
20
|
+
import { buildMarkdown } from './frontmatter.mjs'
|
|
21
|
+
import { latestFactNNN, nextRunNNN } from './nnn.mjs'
|
|
22
|
+
import { loadConfig, resolveTasksDir, resolveWorktreesDir } from './config.mjs'
|
|
23
|
+
import { findNodeWorktree, listActiveWorktrees, mergeWorktree } from './worktree-ops.mjs'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Пише run_NNN.md артефакт.
|
|
27
|
+
* @param {string} nodeDir директорія вузла
|
|
28
|
+
* @param {string} nnn NNN рядок
|
|
29
|
+
* @param {'success'|'failed'} result результат
|
|
30
|
+
* @param {{ actor: string, now: string }} meta метадані
|
|
31
|
+
* @param {(p: string, c: string, enc: string) => void} writeFile функція запису
|
|
32
|
+
*/
|
|
33
|
+
function writeRunFile(nodeDir, nnn, result, meta, writeFile) {
|
|
34
|
+
const fm = {
|
|
35
|
+
created_at: meta.now,
|
|
36
|
+
actor: meta.actor,
|
|
37
|
+
result
|
|
38
|
+
}
|
|
39
|
+
const content = buildMarkdown(fm, `## Run ${nnn}\n\nactor: ${meta.actor}\nresult: ${result}\n`)
|
|
40
|
+
writeFile(join(nodeDir, `run_${nnn}.md`), content, 'utf8')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Резолвить шлях вузла з аргументів або env/fallback-файлу.
|
|
45
|
+
* @param {string[]} args аргументи командного рядка
|
|
46
|
+
* @param {{ env?: Record<string, string>, cwd?: string, exists?: (p: string) => boolean, readFile?: (p: string, enc: string) => string }} deps ін'єкції
|
|
47
|
+
* @returns {{ nodePath: string | null, error: string | null }} результат
|
|
48
|
+
*/
|
|
49
|
+
function resolveNodePath(args, deps) {
|
|
50
|
+
// 1. Прямий аргумент
|
|
51
|
+
if (args[0] && !args[0].startsWith('-')) {
|
|
52
|
+
return { nodePath: args[0], error: null }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. ENV var
|
|
56
|
+
const env = deps.env ?? process.env
|
|
57
|
+
const fromEnv = env['NCURSOR_NODE_PATH']
|
|
58
|
+
if (fromEnv?.trim()) {
|
|
59
|
+
return { nodePath: fromEnv.trim(), error: null }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. Fallback файл .n-cursor/current-node
|
|
63
|
+
const cwd = deps.cwd ?? processCwd()
|
|
64
|
+
const exists = deps.exists ?? existsSync
|
|
65
|
+
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
66
|
+
const fallbackPath = join(cwd, '.n-cursor', 'current-node')
|
|
67
|
+
if (exists(fallbackPath)) {
|
|
68
|
+
try {
|
|
69
|
+
const content = readFile(fallbackPath, 'utf8').trim()
|
|
70
|
+
if (content.length > 0) return { nodePath: content, error: null }
|
|
71
|
+
} catch {
|
|
72
|
+
// пропускаємо
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { nodePath: null, error: 'NCURSOR_NODE_PATH not set and .n-cursor/current-node not found' }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* `graph done <path>` — успіх → пише run_NNN.md (success), мерджить worktree.
|
|
81
|
+
* @param {string[]} args аргументи
|
|
82
|
+
* @param {object} [deps] ін'єкції
|
|
83
|
+
* @returns {Promise<number>} exit code
|
|
84
|
+
*/
|
|
85
|
+
export async function cmdDone(args, deps = {}) {
|
|
86
|
+
const root = deps.cwd ?? processCwd()
|
|
87
|
+
const log = deps.log ?? console.log
|
|
88
|
+
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
89
|
+
const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
|
|
90
|
+
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
91
|
+
const exists = deps.exists ?? existsSync
|
|
92
|
+
const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
|
|
93
|
+
const nowFn = deps.now ?? (() => new Date().toISOString())
|
|
94
|
+
|
|
95
|
+
const { nodePath, error } = resolveNodePath(args, { env: deps.env, cwd: root, exists, readFile })
|
|
96
|
+
if (!nodePath) {
|
|
97
|
+
log(`done: ${error}`)
|
|
98
|
+
return 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const config = loadConfig({ root, readFile, exists })
|
|
102
|
+
const tasksDir = resolveTasksDir(config, root)
|
|
103
|
+
const worktreesDir = resolveWorktreesDir(config, root)
|
|
104
|
+
const nodeDir = join(tasksDir, nodePath)
|
|
105
|
+
|
|
106
|
+
if (!exists(join(nodeDir, 'task.md'))) {
|
|
107
|
+
log(`done: вузол "${nodePath}" не знайдено`)
|
|
108
|
+
return 1
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Записуємо run_NNN.md
|
|
112
|
+
const nnn = nextRunNNN(nodeDir, readdir)
|
|
113
|
+
try {
|
|
114
|
+
writeRunFile(nodeDir, nnn, 'success', { actor: 'agent', now: nowFn() }, writeFile)
|
|
115
|
+
log(`done: записано run_${nnn}.md (result: success)`)
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log(`done: не вдалося записати run_${nnn}.md — ${err.message ?? String(err)}`)
|
|
118
|
+
return 1
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Знаходимо і мерджимо worktree
|
|
122
|
+
const worktreePath = findNodeWorktree(nodePath, worktreesDir, {
|
|
123
|
+
readdirSync: readdir,
|
|
124
|
+
execSync: execSyncFn
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (worktreePath) {
|
|
128
|
+
const mergeResult = mergeWorktree(worktreePath, root, { execSync: execSyncFn })
|
|
129
|
+
if (!mergeResult.ok) {
|
|
130
|
+
log(`done: merge не вдався — ${mergeResult.error}`)
|
|
131
|
+
return 1
|
|
132
|
+
}
|
|
133
|
+
log(`done: worktree merged і видалено`)
|
|
134
|
+
} else {
|
|
135
|
+
log(`done: worktree не знайдено для "${nodePath}" — пропускаємо merge`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
log(`done: вузол "${nodePath}" успішно завершено`)
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* `graph audit <path>` — аудит → creates pending-audit_NNN.md, merge worktree.
|
|
144
|
+
* @param {string[]} args аргументи
|
|
145
|
+
* @param {object} [deps] ін'єкції
|
|
146
|
+
* @returns {Promise<number>} exit code
|
|
147
|
+
*/
|
|
148
|
+
export async function cmdAudit(args, deps = {}) {
|
|
149
|
+
const root = deps.cwd ?? processCwd()
|
|
150
|
+
const log = deps.log ?? console.log
|
|
151
|
+
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
152
|
+
const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
|
|
153
|
+
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
154
|
+
const exists = deps.exists ?? existsSync
|
|
155
|
+
const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
|
|
156
|
+
const nowFn = deps.now ?? (() => new Date().toISOString())
|
|
157
|
+
|
|
158
|
+
const { nodePath, error } = resolveNodePath(args, { env: deps.env, cwd: root, exists, readFile })
|
|
159
|
+
if (!nodePath) {
|
|
160
|
+
log(`audit: ${error}`)
|
|
161
|
+
return 1
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const config = loadConfig({ root, readFile, exists })
|
|
165
|
+
const tasksDir = resolveTasksDir(config, root)
|
|
166
|
+
const worktreesDir = resolveWorktreesDir(config, root)
|
|
167
|
+
const nodeDir = join(tasksDir, nodePath)
|
|
168
|
+
|
|
169
|
+
if (!exists(join(nodeDir, 'task.md'))) {
|
|
170
|
+
log(`audit: вузол "${nodePath}" не знайдено`)
|
|
171
|
+
return 1
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Знаходимо latest fact_NNN.md NNN
|
|
175
|
+
const factNNN = latestFactNNN(nodeDir, readdir)
|
|
176
|
+
if (!factNNN) {
|
|
177
|
+
log(`audit: fact_NNN.md не знайдено для "${nodePath}" — спершу виконайте задачу`)
|
|
178
|
+
return 1
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Створюємо pending-audit_NNN.md
|
|
182
|
+
const pendingPath = join(nodeDir, `pending-audit_${factNNN}.md`)
|
|
183
|
+
if (exists(pendingPath)) {
|
|
184
|
+
log(`audit: ${pendingPath} вже існує — audit вже запитано`)
|
|
185
|
+
return 1
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const pendingContent = buildMarkdown({
|
|
189
|
+
created_at: nowFn(),
|
|
190
|
+
fact_ref: `fact_${factNNN}.md`,
|
|
191
|
+
actor: 'agent'
|
|
192
|
+
}, '')
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
writeFile(pendingPath, pendingContent, 'utf8')
|
|
196
|
+
log(`audit: створено ${pendingPath}`)
|
|
197
|
+
} catch (err) {
|
|
198
|
+
log(`audit: не вдалося записати ${pendingPath} — ${err.message ?? String(err)}`)
|
|
199
|
+
return 1
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Записуємо run_NNN.md
|
|
203
|
+
const nnn = nextRunNNN(nodeDir, readdir)
|
|
204
|
+
try {
|
|
205
|
+
writeRunFile(nodeDir, nnn, 'success', { actor: 'agent', now: nowFn() }, writeFile)
|
|
206
|
+
log(`audit: записано run_${nnn}.md`)
|
|
207
|
+
} catch (err) {
|
|
208
|
+
log(`audit: не вдалося записати run_${nnn}.md — ${err.message ?? String(err)}`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Мерджимо worktree агента
|
|
212
|
+
const worktreePath = findNodeWorktree(nodePath, worktreesDir, {
|
|
213
|
+
readdirSync: readdir,
|
|
214
|
+
execSync: execSyncFn
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
if (worktreePath) {
|
|
218
|
+
const mergeResult = mergeWorktree(worktreePath, root, { execSync: execSyncFn })
|
|
219
|
+
if (!mergeResult.ok) {
|
|
220
|
+
log(`audit: merge не вдався — ${mergeResult.error}`)
|
|
221
|
+
} else {
|
|
222
|
+
log(`audit: agent worktree merged і видалено`)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
log(`audit: запит аудиту для "${nodePath}" (fact_${factNNN}.md) успішно створено`)
|
|
227
|
+
return 0
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* `graph failed <path>` — провал → пише run_NNN.md (failed), залишає worktree.
|
|
232
|
+
* @param {string[]} args аргументи
|
|
233
|
+
* @param {object} [deps] ін'єкції
|
|
234
|
+
* @returns {Promise<number>} exit code
|
|
235
|
+
*/
|
|
236
|
+
export async function cmdFailed(args, deps = {}) {
|
|
237
|
+
const root = deps.cwd ?? processCwd()
|
|
238
|
+
const log = deps.log ?? console.log
|
|
239
|
+
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
240
|
+
const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
|
|
241
|
+
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
242
|
+
const exists = deps.exists ?? existsSync
|
|
243
|
+
const nowFn = deps.now ?? (() => new Date().toISOString())
|
|
244
|
+
|
|
245
|
+
const { nodePath, error } = resolveNodePath(args, { env: deps.env, cwd: root, exists, readFile })
|
|
246
|
+
if (!nodePath) {
|
|
247
|
+
log(`failed: ${error}`)
|
|
248
|
+
return 1
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const config = loadConfig({ root, readFile, exists })
|
|
252
|
+
const tasksDir = resolveTasksDir(config, root)
|
|
253
|
+
const nodeDir = join(tasksDir, nodePath)
|
|
254
|
+
|
|
255
|
+
if (!exists(join(nodeDir, 'task.md'))) {
|
|
256
|
+
log(`failed: вузол "${nodePath}" не знайдено`)
|
|
257
|
+
return 1
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Записуємо run_NNN.md з result:failed
|
|
261
|
+
const nnn = nextRunNNN(nodeDir, readdir)
|
|
262
|
+
try {
|
|
263
|
+
writeRunFile(nodeDir, nnn, 'failed', { actor: 'agent', now: nowFn() }, writeFile)
|
|
264
|
+
log(`failed: записано run_${nnn}.md (result: failed)`)
|
|
265
|
+
} catch (err) {
|
|
266
|
+
log(`failed: не вдалося записати run_${nnn}.md — ${err.message ?? String(err)}`)
|
|
267
|
+
return 1
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
log(`failed: вузол "${nodePath}" позначено як failed — worktree збережено для діагностики`)
|
|
271
|
+
return 0
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* `graph spawn <path>` — composite → перевіряє що дочірні вузли зареєстровані.
|
|
276
|
+
* @param {string[]} args аргументи
|
|
277
|
+
* @param {object} [deps] ін'єкції
|
|
278
|
+
* @returns {Promise<number>} exit code
|
|
279
|
+
*/
|
|
280
|
+
export async function cmdSpawn(args, deps = {}) {
|
|
281
|
+
const root = deps.cwd ?? processCwd()
|
|
282
|
+
const log = deps.log ?? console.log
|
|
283
|
+
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
284
|
+
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
285
|
+
const exists = deps.exists ?? existsSync
|
|
286
|
+
|
|
287
|
+
const { nodePath, error } = resolveNodePath(args, { env: deps.env, cwd: root, exists, readFile })
|
|
288
|
+
if (!nodePath) {
|
|
289
|
+
log(`spawn: ${error}`)
|
|
290
|
+
return 1
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const config = loadConfig({ root, readFile, exists })
|
|
294
|
+
const tasksDir = resolveTasksDir(config, root)
|
|
295
|
+
const nodeDir = join(tasksDir, nodePath)
|
|
296
|
+
|
|
297
|
+
if (!exists(join(nodeDir, 'task.md'))) {
|
|
298
|
+
log(`spawn: вузол "${nodePath}" не знайдено`)
|
|
299
|
+
return 1
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Перевіряємо дочірні директорії
|
|
303
|
+
let entries
|
|
304
|
+
try {
|
|
305
|
+
entries = readdir(nodeDir)
|
|
306
|
+
} catch {
|
|
307
|
+
log(`spawn: не вдалося прочитати директорію вузла`)
|
|
308
|
+
return 1
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const childDirs = entries.filter(name => {
|
|
312
|
+
if (name.startsWith('.') || name.endsWith('.md') || name.endsWith('.json')) return false
|
|
313
|
+
return exists(join(nodeDir, name, 'task.md'))
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
if (childDirs.length === 0) {
|
|
317
|
+
log(`spawn: вузол "${nodePath}" не має дочірніх вузлів із task.md`)
|
|
318
|
+
log(`spawn: для composite вузла треба створити дочірні директорії з task.md`)
|
|
319
|
+
return 1
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
log(`spawn: вузол "${nodePath}" є composite з ${childDirs.length} дочірніми вузлами:`)
|
|
323
|
+
for (const child of childDirs) {
|
|
324
|
+
log(` - ${nodePath}/${child}`)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return 0
|
|
328
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `n-cursor graph status [<path>] [--json]` — показує стан DAG вузлів.
|
|
3
|
+
*
|
|
4
|
+
* Без path — показує всі вузли. З path — лише вузол і його нащадків.
|
|
5
|
+
* --json — machine-readable JSON вивід.
|
|
6
|
+
*
|
|
7
|
+
* FS ін'єктується для тестованості.
|
|
8
|
+
*/
|
|
9
|
+
import { execSync } from 'node:child_process'
|
|
10
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import { cwd as processCwd } from 'node:process'
|
|
13
|
+
|
|
14
|
+
import { loadConfig, resolveTasksDir } from './config.mjs'
|
|
15
|
+
import { scanNodes, topoSort } from './scanner.mjs'
|
|
16
|
+
import { listActiveWorktrees } from './worktree-ops.mjs'
|
|
17
|
+
|
|
18
|
+
/** Кольори для стану (ANSI). */
|
|
19
|
+
const STATE_COLORS = {
|
|
20
|
+
'needs-plan': '\x1b[33m', // жовтий
|
|
21
|
+
waiting: '\x1b[36m', // блакитний
|
|
22
|
+
running: '\x1b[34m', // синій
|
|
23
|
+
'pending-audit': '\x1b[35m', // фіолетовий
|
|
24
|
+
resolved: '\x1b[32m', // зелений
|
|
25
|
+
failed: '\x1b[31m', // червоний
|
|
26
|
+
invalidated: '\x1b[90m' // сірий
|
|
27
|
+
}
|
|
28
|
+
const RESET = '\x1b[0m'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Повертає colored рядок стану (якщо TTY).
|
|
32
|
+
* @param {string} state стан вузла
|
|
33
|
+
* @param {boolean} color чи потрібен колір
|
|
34
|
+
* @returns {string} рядок
|
|
35
|
+
*/
|
|
36
|
+
function colorState(state, color) {
|
|
37
|
+
if (!color) return state
|
|
38
|
+
const c = STATE_COLORS[state] ?? ''
|
|
39
|
+
return `${c}${state}${RESET}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `graph status [<path>] [--json]` command handler.
|
|
44
|
+
* @param {string[]} args аргументи
|
|
45
|
+
* @param {{
|
|
46
|
+
* cwd?: string,
|
|
47
|
+
* log?: (m: string) => void,
|
|
48
|
+
* readFile?: (p: string, enc: string) => string,
|
|
49
|
+
* readdir?: (d: string) => string[],
|
|
50
|
+
* exists?: (p: string) => boolean,
|
|
51
|
+
* execSync?: (cmd: string, opts?: object) => string
|
|
52
|
+
* }} [deps] ін'єкції
|
|
53
|
+
* @returns {Promise<number>} exit code
|
|
54
|
+
*/
|
|
55
|
+
export async function cmdStatus(args, deps = {}) {
|
|
56
|
+
const root = deps.cwd ?? processCwd()
|
|
57
|
+
const log = deps.log ?? console.log
|
|
58
|
+
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
59
|
+
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
60
|
+
const exists = deps.exists ?? existsSync
|
|
61
|
+
const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, { ...opts, encoding: 'utf8' }))
|
|
62
|
+
|
|
63
|
+
// Парсимо аргументи
|
|
64
|
+
let nodePath = null
|
|
65
|
+
let jsonMode = false
|
|
66
|
+
|
|
67
|
+
for (const arg of args) {
|
|
68
|
+
if (arg === '--json') jsonMode = true
|
|
69
|
+
else if (!arg.startsWith('-')) nodePath = arg
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const config = loadConfig({ root, readFile, exists })
|
|
73
|
+
const tasksDir = resolveTasksDir(config, root)
|
|
74
|
+
const worktreesDir = join(root, config.worktrees_dir.startsWith('/') ? config.worktrees_dir : config.worktrees_dir.slice(2))
|
|
75
|
+
|
|
76
|
+
const activeWorktrees = listActiveWorktrees(root, { execSync: execSyncFn })
|
|
77
|
+
|
|
78
|
+
const allNodes = scanNodes(tasksDir, activeWorktrees, {
|
|
79
|
+
readdirSync: readdir,
|
|
80
|
+
existsSync: exists,
|
|
81
|
+
readFileSync: readFile
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Фільтруємо якщо є path
|
|
85
|
+
let nodes = allNodes
|
|
86
|
+
if (nodePath) {
|
|
87
|
+
nodes = allNodes.filter(n => n.path === nodePath || n.path.startsWith(nodePath + '/'))
|
|
88
|
+
if (nodes.length === 0) {
|
|
89
|
+
log(`status: вузол "${nodePath}" не знайдено`)
|
|
90
|
+
return 1
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const sorted = topoSort(nodes)
|
|
95
|
+
|
|
96
|
+
if (jsonMode) {
|
|
97
|
+
console.log(JSON.stringify(sorted.map(n => ({
|
|
98
|
+
id: n.id,
|
|
99
|
+
path: n.path,
|
|
100
|
+
state: n.state,
|
|
101
|
+
deps: n.deps,
|
|
102
|
+
composite: n.composite,
|
|
103
|
+
children: n.children
|
|
104
|
+
})), null, 2))
|
|
105
|
+
return 0
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Текстовий вивід
|
|
109
|
+
const useColor = process.stdout.isTTY ?? false
|
|
110
|
+
|
|
111
|
+
// Підрахунок по станах
|
|
112
|
+
const stateCounts = {}
|
|
113
|
+
for (const n of sorted) {
|
|
114
|
+
stateCounts[n.state] = (stateCounts[n.state] ?? 0) + 1
|
|
115
|
+
}
|
|
116
|
+
const summary = Object.entries(stateCounts)
|
|
117
|
+
.map(([s, c]) => `${colorState(s, useColor)}:${c}`)
|
|
118
|
+
.join(' ')
|
|
119
|
+
|
|
120
|
+
log(`DAG tasks — ${summary}`)
|
|
121
|
+
log('')
|
|
122
|
+
|
|
123
|
+
for (const node of sorted) {
|
|
124
|
+
const indent = node.path.includes('/') ? ' '.repeat(node.path.split('/').length - 1) : ''
|
|
125
|
+
const composite = node.composite ? ' [composite]' : ''
|
|
126
|
+
const deps = node.deps.length > 0 ? ` ← [${node.deps.join(', ')}]` : ''
|
|
127
|
+
log(`${indent}${node.path} [${colorState(node.state, useColor)}]${composite}${deps}`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return 0
|
|
131
|
+
}
|