@michaelxwb/code-flow 0.8.1 → 1.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 (268) hide show
  1. package/README.md +73 -152
  2. package/bin/code-flow.js +12 -0
  3. package/docs/README.md +19 -0
  4. package/docs/USAGE.md +156 -1417
  5. package/docs/v1-design.md +1858 -0
  6. package/docs/v1-implementation.md +186 -0
  7. package/lib/adapters.js +126 -0
  8. package/lib/cli.js +258 -0
  9. package/lib/config.js +85 -0
  10. package/lib/doctor.js +283 -0
  11. package/lib/frontmatter.js +42 -0
  12. package/lib/fs.js +141 -0
  13. package/lib/glob.js +23 -0
  14. package/lib/hook.js +99 -0
  15. package/lib/init.js +132 -0
  16. package/lib/learn.js +170 -0
  17. package/lib/locks.js +52 -0
  18. package/lib/markdown.js +13 -0
  19. package/lib/process.js +116 -0
  20. package/lib/render.js +115 -0
  21. package/lib/repo.js +48 -0
  22. package/lib/runs.js +155 -0
  23. package/lib/session.js +40 -0
  24. package/lib/spec.js +80 -0
  25. package/lib/task.js +258 -0
  26. package/lib/taskdoc.js +308 -0
  27. package/lib/toml.js +140 -0
  28. package/lib/verify.js +168 -0
  29. package/lib/worktree.js +930 -0
  30. package/package.json +18 -21
  31. package/templates/adapters/opencode.mjs +65 -0
  32. package/templates/config.json +39 -0
  33. package/templates/instructions.md +13 -0
  34. package/templates/launcher/bridge.cjs +295 -0
  35. package/templates/launcher/hook.cmd +15 -0
  36. package/templates/launcher/hook.ps1 +10 -0
  37. package/templates/launcher/hook.sh +20 -0
  38. package/templates/skills/cf-check.md +19 -0
  39. package/templates/skills/cf-init.md +16 -0
  40. package/templates/skills/cf-spec.md +20 -0
  41. package/templates/skills/cf-task-align.md +19 -0
  42. package/templates/skills/cf-task-archive.md +20 -0
  43. package/templates/skills/cf-task-plan.md +31 -0
  44. package/templates/skills/cf-task-prd.md +14 -0
  45. package/templates/skills/cf-task-start.md +41 -0
  46. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +12 -12
  47. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +17 -19
  48. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +12 -12
  49. package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
  50. package/templates/workflow/project-guidelines.md +48 -0
  51. package/templates/workflow/scenarios.md +67 -0
  52. package/templates/workflow/task.md +56 -0
  53. package/docs/acceptance-contract.md +0 -217
  54. package/docs/ci/acceptance-evidence.md +0 -77
  55. package/docs/migrations/runtime-0.7.0.md +0 -41
  56. package/docs/migrations/runtime-0.8.0.md +0 -154
  57. package/docs/migrations/runtime-0.9.0.md +0 -40
  58. package/docs/migrations/spec-workflow-0.6.0.md +0 -42
  59. package/docs/spec-injection-hardening.md +0 -54
  60. package/docs/spec-injection-performance.json +0 -73
  61. package/docs/spec-optimization.md +0 -12
  62. package/src/adapters/claude/CLAUDE.md +0 -45
  63. package/src/adapters/claude/commands/cf-init.md +0 -262
  64. package/src/adapters/claude/commands/cf-learn.md +0 -291
  65. package/src/adapters/claude/commands/cf-spec.md +0 -87
  66. package/src/adapters/claude/commands/cf-stats.md +0 -96
  67. package/src/adapters/claude/commands/cf-sync.md +0 -39
  68. package/src/adapters/claude/commands/cf-task/align.md +0 -331
  69. package/src/adapters/claude/commands/cf-task/archive.md +0 -155
  70. package/src/adapters/claude/commands/cf-task/block.md +0 -49
  71. package/src/adapters/claude/commands/cf-task/graph.md +0 -92
  72. package/src/adapters/claude/commands/cf-task/note.md +0 -248
  73. package/src/adapters/claude/commands/cf-task/plan.md +0 -477
  74. package/src/adapters/claude/commands/cf-task/prd.md +0 -173
  75. package/src/adapters/claude/commands/cf-task/start.md +0 -317
  76. package/src/adapters/claude/commands/cf-task/status.md +0 -71
  77. package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -148
  78. package/src/adapters/claude/commands/cf-validate.md +0 -100
  79. package/src/adapters/claude/settings.local.json +0 -50
  80. package/src/adapters/codex/AGENTS.md +0 -45
  81. package/src/adapters/codex/config.toml +0 -3
  82. package/src/adapters/codex/hooks.json +0 -54
  83. package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
  84. package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
  85. package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -88
  86. package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -97
  87. package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -45
  88. package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -332
  89. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -156
  90. package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -50
  91. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -93
  92. package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -245
  93. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -478
  94. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -174
  95. package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -318
  96. package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -72
  97. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -145
  98. package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -102
  99. package/src/adapters/commands.json +0 -115
  100. package/src/adapters/costrict/CLAUDE.md +0 -45
  101. package/src/adapters/costrict/commands/cf-init.md +0 -262
  102. package/src/adapters/costrict/commands/cf-learn.md +0 -291
  103. package/src/adapters/costrict/commands/cf-spec.md +0 -87
  104. package/src/adapters/costrict/commands/cf-stats.md +0 -96
  105. package/src/adapters/costrict/commands/cf-sync.md +0 -39
  106. package/src/adapters/costrict/commands/cf-task/align.md +0 -331
  107. package/src/adapters/costrict/commands/cf-task/archive.md +0 -155
  108. package/src/adapters/costrict/commands/cf-task/block.md +0 -49
  109. package/src/adapters/costrict/commands/cf-task/graph.md +0 -92
  110. package/src/adapters/costrict/commands/cf-task/note.md +0 -248
  111. package/src/adapters/costrict/commands/cf-task/plan.md +0 -477
  112. package/src/adapters/costrict/commands/cf-task/prd.md +0 -173
  113. package/src/adapters/costrict/commands/cf-task/start.md +0 -317
  114. package/src/adapters/costrict/commands/cf-task/status.md +0 -71
  115. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -148
  116. package/src/adapters/costrict/commands/cf-validate.md +0 -100
  117. package/src/adapters/costrict/settings.local.json +0 -50
  118. package/src/adapters/install-plan.json +0 -100
  119. package/src/adapters/opencode/AGENTS.md +0 -45
  120. package/src/adapters/opencode/commands/cf-init.md +0 -281
  121. package/src/adapters/opencode/commands/cf-learn.md +0 -295
  122. package/src/adapters/opencode/commands/cf-spec.md +0 -91
  123. package/src/adapters/opencode/commands/cf-stats.md +0 -100
  124. package/src/adapters/opencode/commands/cf-sync.md +0 -44
  125. package/src/adapters/opencode/commands/cf-task/align.md +0 -331
  126. package/src/adapters/opencode/commands/cf-task/archive.md +0 -160
  127. package/src/adapters/opencode/commands/cf-task/block.md +0 -54
  128. package/src/adapters/opencode/commands/cf-task/graph.md +0 -97
  129. package/src/adapters/opencode/commands/cf-task/note.md +0 -253
  130. package/src/adapters/opencode/commands/cf-task/plan.md +0 -477
  131. package/src/adapters/opencode/commands/cf-task/prd.md +0 -178
  132. package/src/adapters/opencode/commands/cf-task/start.md +0 -317
  133. package/src/adapters/opencode/commands/cf-task/status.md +0 -76
  134. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -148
  135. package/src/adapters/opencode/commands/cf-validate.md +0 -105
  136. package/src/adapters/opencode/opencode.json +0 -3
  137. package/src/adapters/opencode/plugins/code-flow/index.js +0 -162
  138. package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
  139. package/src/adapters/platform-bindings.json +0 -370
  140. package/src/cli.js +0 -1076
  141. package/src/core/code-flow/config.yml +0 -161
  142. package/src/core/code-flow/runtime-commands.json +0 -259
  143. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -10
  144. package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -10
  145. package/src/core/code-flow/scripts/cf_audit.py +0 -10
  146. package/src/core/code-flow/scripts/cf_codex_hook.py +0 -10
  147. package/src/core/code-flow/scripts/cf_design_lint.py +0 -10
  148. package/src/core/code-flow/scripts/cf_evidence_import.py +0 -10
  149. package/src/core/code-flow/scripts/cf_feedback.py +0 -10
  150. package/src/core/code-flow/scripts/cf_opencode_event.py +0 -10
  151. package/src/core/code-flow/scripts/cf_post_hook.py +0 -11
  152. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -11
  153. package/src/core/code-flow/scripts/cf_scenarios.py +0 -10
  154. package/src/core/code-flow/scripts/cf_spec_context.py +0 -10
  155. package/src/core/code-flow/scripts/cf_spec_gate.py +0 -10
  156. package/src/core/code-flow/scripts/cf_spec_session.py +0 -10
  157. package/src/core/code-flow/scripts/cf_stats.py +0 -10
  158. package/src/core/code-flow/scripts/cf_stop_hook.py +0 -11
  159. package/src/core/code-flow/scripts/cf_sync.py +0 -10
  160. package/src/core/code-flow/scripts/cf_task_index.py +0 -10
  161. package/src/core/code-flow/scripts/cf_task_parallel.py +0 -10
  162. package/src/core/code-flow/scripts/cf_task_workflow.py +0 -10
  163. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -11
  164. package/src/core/code-flow/scripts/cf_validation.py +0 -10
  165. package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
  166. package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
  167. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +0 -347
  168. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +0 -660
  169. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +0 -202
  170. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +0 -60
  171. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +0 -73
  172. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +0 -282
  173. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +0 -266
  174. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +0 -172
  175. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +0 -326
  176. package/src/core/code-flow/scripts/codeflow/acceptance/cf_checks.py +0 -333
  177. package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +0 -197
  178. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +0 -174
  179. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +0 -300
  180. package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +0 -502
  181. package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
  182. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +0 -347
  183. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +0 -1490
  184. package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +0 -533
  185. package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +0 -172
  186. package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +0 -607
  187. package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +0 -53
  188. package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +0 -58
  189. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +0 -1175
  190. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +0 -60
  191. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +0 -46
  192. package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +0 -540
  193. package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +0 -144
  194. package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +0 -177
  195. package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +0 -1142
  196. package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +0 -742
  197. package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +0 -31
  198. package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
  199. package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +0 -67
  200. package/src/core/code-flow/scripts/codeflow/core/cf_core.py +0 -609
  201. package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +0 -325
  202. package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +0 -57
  203. package/src/core/code-flow/scripts/codeflow/core/cf_git.py +0 -92
  204. package/src/core/code-flow/scripts/codeflow/core/cf_log.py +0 -160
  205. package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +0 -77
  206. package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +0 -26
  207. package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +0 -33
  208. package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +0 -32
  209. package/src/core/code-flow/scripts/codeflow/core/cf_session_state.py +0 -37
  210. package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +0 -53
  211. package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +0 -9
  212. package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +0 -408
  213. package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +0 -253
  214. package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
  215. package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +0 -527
  216. package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
  217. package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +0 -95
  218. package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +0 -98
  219. package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +0 -30
  220. package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +0 -41
  221. package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +0 -292
  222. package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +0 -161
  223. package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
  224. package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +0 -412
  225. package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +0 -301
  226. package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +0 -149
  227. package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +0 -111
  228. package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +0 -114
  229. package/src/core/code-flow/scripts/codeflow/spec/cf_scan.py +0 -367
  230. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +0 -55
  231. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +0 -477
  232. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +0 -119
  233. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_metadata.py +0 -387
  234. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_migrate.py +0 -529
  235. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +0 -193
  236. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +0 -187
  237. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_resolver.py +0 -369
  238. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_router.py +0 -304
  239. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_verify.py +0 -687
  240. package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +0 -330
  241. package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +0 -83
  242. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +0 -755
  243. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_transaction.py +0 -110
  244. package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
  245. package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +0 -867
  246. package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +0 -178
  247. package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +0 -635
  248. package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +0 -251
  249. package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +0 -570
  250. package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +0 -73
  251. package/src/core/code-flow/scripts/runtime-resources.json +0 -29
  252. package/src/core/code-flow/specs/backend/_map.md +0 -56
  253. package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
  254. package/src/core/code-flow/specs/backend/database.md +0 -51
  255. package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
  256. package/src/core/code-flow/specs/backend/logging.md +0 -50
  257. package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
  258. package/src/core/code-flow/specs/frontend/_map.md +0 -52
  259. package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
  260. package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
  261. package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
  262. package/src/core/code-flow/specs/shared/_map.md +0 -57
  263. package/src/core/code-flow/specs/shared/scenarios-template.md +0 -55
  264. package/src/core/code-flow/tasks/.gitkeep +0 -0
  265. package/src/core/code-flow/validation.yml +0 -59
  266. package/src/migrate/runtime.js +0 -213
  267. package/src/migrate/spec-workflow.js +0 -253
  268. package/src/runtime.js +0 -219
package/lib/config.js ADDED
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const f = require('./fs');
5
+ const glob = require('./glob');
6
+
7
+ function defaults() {
8
+ return { version: 1, workflow: 'hook', always: [], paths: {},
9
+ budget: { maxTokens: 2500, maxBytes: 16384 },
10
+ tests: { timeoutSeconds: 120, jobs: 2, regression: null },
11
+ capture: { enabled: false, path: '.code-flow/process' } };
12
+ }
13
+ function reference(value) {
14
+ f.string(value, 'Spec reference');
15
+ const parts = value.split('#');
16
+ f.demand(parts.length <= 2 && /^[a-z0-9][a-z0-9/_-]*$/.test(parts[0]) &&
17
+ parts[0].split('/').every(Boolean) && (!parts[1] || /^RULE-[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/.test(parts[1])) &&
18
+ (parts.length === 1 || Boolean(parts[1])), 'invalid_reference', 'Invalid Spec reference: ' + value);
19
+ return { id: parts[0], rule: parts[1] || null };
20
+ }
21
+ function validate(raw) {
22
+ f.object(raw, 'config');
23
+ f.demand(raw.version === 1, 'invalid_config', 'config.version must be 1');
24
+ const base = defaults();
25
+ const result = { ...raw, workflow: raw.workflow ?? base.workflow,
26
+ always: raw.always ?? [], paths: raw.paths ?? {},
27
+ budget: { ...base.budget, ...f.object(raw.budget ?? {}, 'budget') },
28
+ tests: { ...base.tests, ...f.object(raw.tests ?? {}, 'tests') },
29
+ capture: { ...base.capture, ...f.object(raw.capture ?? {}, 'capture') } };
30
+ f.demand(['hook', 'full'].includes(result.workflow), 'invalid_config',
31
+ 'workflow must be hook or full');
32
+ f.demand(typeof result.capture.enabled === 'boolean', 'invalid_config',
33
+ 'capture.enabled must be boolean');
34
+ f.string(result.capture.path, 'capture.path');
35
+ f.demand(/^\.code-flow\/[A-Za-z0-9._/-]+$/.test(result.capture.path) &&
36
+ !result.capture.path.split('/').includes('..') && !result.capture.path.endsWith('/'),
37
+ 'invalid_config', 'capture.path must be a relative path under .code-flow');
38
+ f.strings(result.always, 'always').forEach(reference);
39
+ f.object(result.paths, 'paths');
40
+ for (const [pattern, refs] of Object.entries(result.paths)) {
41
+ glob.compile(pattern); f.strings(refs, 'paths references').forEach(reference);
42
+ }
43
+ f.positive(result.budget.maxTokens, 'maxTokens', true);
44
+ f.positive(result.budget.maxBytes, 'maxBytes', true);
45
+ f.positive(result.tests.timeoutSeconds, 'timeoutSeconds');
46
+ f.positive(result.tests.jobs, 'jobs', true);
47
+ if (result.tests.regression !== null) result.tests.regression = regression(result.tests.regression);
48
+ return result;
49
+ }
50
+
51
+ function regression(value) {
52
+ f.object(value, 'tests.regression');
53
+ f.demand((value.argv !== undefined) !== (value.run !== undefined), 'invalid_config',
54
+ 'tests.regression requires exactly one of argv or run');
55
+ if (value.argv !== undefined) f.strings(value.argv, 'tests.regression argv');
56
+ else f.string(value.run, 'tests.regression run');
57
+ if (value.cwd !== undefined) f.string(value.cwd, 'tests.regression cwd');
58
+ if (value.timeout !== undefined) f.positive(value.timeout, 'tests.regression timeout');
59
+ if (value.parallel !== undefined) f.demand(typeof value.parallel === 'boolean',
60
+ 'invalid_config', 'tests.regression parallel must be boolean');
61
+ if (value.group !== undefined) f.string(value.group, 'tests.regression group');
62
+ if (value.env !== undefined) f.object(value.env, 'tests.regression env');
63
+ return { ...value, id: 'regression' };
64
+ }
65
+ function findRoot(cwd, required = true) {
66
+ let current = f.realpath(cwd);
67
+ while (true) {
68
+ if (fs.existsSync(path.join(current, '.code-flow/config.json'))) return current;
69
+ const parent = path.dirname(current);
70
+ if (parent === current || fs.existsSync(path.join(current, '.git'))) break;
71
+ current = parent;
72
+ }
73
+ f.demand(!required, 'project_missing', 'Run inside a project with .code-flow/config.json');
74
+ return null;
75
+ }
76
+ function load(root) {
77
+ const raw = f.readText(f.inside(root, '.code-flow/config.json'), 65536);
78
+ f.demand(raw !== null, 'config_missing', 'Missing config.json');
79
+ let data;
80
+ try { data = JSON.parse(raw); } catch (error) {
81
+ throw new f.FlowError('config_invalid', 'Invalid config JSON: ' + error.message);
82
+ }
83
+ return validate(data);
84
+ }
85
+ module.exports = { defaults, reference, validate, findRoot, load };
package/lib/doctor.js ADDED
@@ -0,0 +1,283 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { spawnSync } = require('node:child_process');
5
+ const f = require('./fs');
6
+ const config = require('./config');
7
+ const spec = require('./spec');
8
+ const task = require('./task');
9
+ const taskdoc = require('./taskdoc');
10
+ const init = require('./init');
11
+ const runs = require('./runs');
12
+ const toml = require('./toml');
13
+
14
+ const TEMPLATES = init.WORKFLOW_TEMPLATES;
15
+ const LAUNCHER = init.LAUNCHER_TEMPLATES;
16
+
17
+ function check(code, severity, status, message, target = null, nextAction = '') {
18
+ return { code, severity, status, path: target, message, next_action: nextAction };
19
+ }
20
+
21
+ function pass(code, message, target = null) {
22
+ return check(code, 'info', 'pass', message, target, '');
23
+ }
24
+
25
+ function gitRead(root, args) {
26
+ const result = spawnSync('git', args, { cwd: root, encoding: 'utf8',
27
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' } });
28
+ return result.status === 0 ? result.stdout.trim() : null;
29
+ }
30
+
31
+ function configChecks(root) {
32
+ try { return [{ value: config.load(root), checks: [pass('CONFIG_OK', 'config 可读取。', '.code-flow/config.json')] }]; }
33
+ catch (error) {
34
+ return [{ value: null, checks: [check('CONFIG_INVALID', 'error', 'fail', error.message,
35
+ '.code-flow/config.json', '修复 config.json 后重试')] }];
36
+ }
37
+ }
38
+
39
+ function templateChecks(root) {
40
+ const directory = path.join(root, '.code-flow/templates');
41
+ const checks = [];
42
+ for (const name of TEMPLATES) {
43
+ if (!fs.existsSync(path.join(directory, name))) {
44
+ checks.push(check('TEMPLATE_MISSING', 'error', 'fail', '缺少工作流模板: ' + name,
45
+ '.code-flow/templates/' + name, '运行 code-flow init'));
46
+ }
47
+ }
48
+ const taskTemplate = f.readText(path.join(directory, 'task.md'));
49
+ if (taskTemplate !== null && !/^## TASK-\d+:/m.test(taskTemplate)) {
50
+ checks.push(check('TEMPLATE_INSTRUCTION_INVALID', 'error', 'fail',
51
+ '任务模板缺少多 TASK 段格式。', '.code-flow/templates/task.md', '运行 code-flow init'));
52
+ }
53
+ return checks.length ? checks : [pass('TEMPLATE_OK', '工作流模板完整。', '.code-flow/templates')];
54
+ }
55
+
56
+ function fallbackFindings(root) {
57
+ const text = f.readText(path.join(root, '.code-flow/launcher/hook.sh'));
58
+ if (text === null) return [];
59
+ const node = /--node '([^']+)'/.exec(text)?.[1];
60
+ const entry = /--entry '([^']+)'/.exec(text)?.[1];
61
+ if (!node || !entry || (fs.existsSync(node) && fs.existsSync(entry))) return [];
62
+ return [check('HOOK_ENTRY_FALLBACK', 'warning', 'fail',
63
+ '固定回退 Node/JS 入口不存在;PATH 全局包仍可能可用。', '.code-flow/launcher/hook.sh',
64
+ '重新 code-flow init 或修复全局安装')];
65
+ }
66
+
67
+ function launcherChecks(root) {
68
+ const checks = [];
69
+ for (const name of LAUNCHER) {
70
+ if (!fs.existsSync(path.join(root, '.code-flow/launcher', name))) {
71
+ checks.push(check('HOOK_ENTRY_MISSING', 'error', 'fail', '项目启动器缺失: ' + name,
72
+ '.code-flow/launcher/' + name, '运行 code-flow init'));
73
+ }
74
+ }
75
+ checks.push(...fallbackFindings(root));
76
+ let platforms = [];
77
+ try { platforms = init.installed(root); } catch (error) {
78
+ checks.push(check('HOOK_CONFIG_INVALID', 'error', 'fail', error.message,
79
+ 'platform configuration', '修复平台 Hook 配置'));
80
+ }
81
+ if (!platforms.length && !checks.some(item => item.code === 'HOOK_CONFIG_INVALID')) {
82
+ checks.push(check('HOOK_CONFIG_INVALID', 'warning', 'fail', '未发现 v1 平台 Hook。',
83
+ '.claude/.codex/.costrict/.opencode', '运行 code-flow init --platform=all'));
84
+ }
85
+ if (platforms.length) {
86
+ const target = require('./adapters').PLATFORMS[platforms[0]].hooks;
87
+ checks.push(check('HOST_TRUST_UNKNOWN', 'warning', 'unknown',
88
+ '无法从文件确定宿主是否已加载并信任此 Hook。', target,
89
+ '在宿主审阅 Hook,并提交一次 prompt 检查注入。'));
90
+ }
91
+ const errors = checks.filter(item => item.severity === 'error');
92
+ const unhealthy = checks.some(item => item.code === 'HOOK_ENTRY_MISSING' ||
93
+ item.code === 'HOOK_ENTRY_FALLBACK');
94
+ if (!errors.length && !unhealthy) {
95
+ checks.push(pass('HOOK_ENTRY_OK', '项目启动器与平台入口可静态读取。', '.code-flow/launcher'));
96
+ }
97
+ return checks;
98
+ }
99
+
100
+ function codexChecks(root) {
101
+ const relative = '.codex/config.toml';
102
+ const text = f.readText(path.join(root, relative));
103
+ if (text === null) return [];
104
+ const found = toml.featuresHooks(text);
105
+ if (found.unsupported) return [check('CODEX_CONFIG_FORM', 'error', 'fail',
106
+ 'Codex config.toml 使用不支持的 features.hooks 形式: ' + found.unsupported,
107
+ relative, '改成规范 [features] 表 + hooks = true,或根级单行 features.hooks = true')];
108
+ if (found.header < 0 && found.key < 0) return [];
109
+ if (found.value !== 'true') return [check('CODEX_HOOKS_DISABLED', 'warning', 'fail',
110
+ 'Codex config.toml 未启用 features.hooks。', relative,
111
+ '在 [features] 表中加入 hooks = true,或运行 code-flow init')];
112
+ return [pass('CODEX_CONFIG_OK', 'Codex config.toml 的 features.hooks 已启用。', relative)];
113
+ }
114
+
115
+ function specChecks(root, settings) {
116
+ let listing;
117
+ try { listing = spec.list(root); } catch (error) {
118
+ return { ids: new Set(), checks: [check('SPEC_INVALID', 'error', 'fail', error.message,
119
+ '.code-flow/specs', '修复 Spec 树')] };
120
+ }
121
+ const checks = listing.diagnostics.map(item => check('SPEC_INVALID', 'error', 'fail',
122
+ item.message, item.source ? '.code-flow/specs/' + item.source + '.md' : '.code-flow/specs',
123
+ '修复 Spec 文件'));
124
+ const ids = new Set(listing.items.map(item => item.id));
125
+ const refs = [];
126
+ if (settings) {
127
+ for (const value of settings.always) refs.push(config.reference(value));
128
+ for (const values of Object.values(settings.paths)) {
129
+ for (const value of values) refs.push(config.reference(value));
130
+ }
131
+ }
132
+ for (const ref of refs) {
133
+ if (!ids.has(ref.id)) checks.push(check('SPEC_REF_MISSING', 'error', 'fail',
134
+ 'config 引用了不存在的 Spec: ' + ref.id, '.code-flow/config.json', '修复引用或补充 Spec'));
135
+ }
136
+ if (!checks.length) checks.push(pass('SPEC_OK', 'Spec 格式与 config 引用有效。', '.code-flow/specs'));
137
+ return { ids, checks };
138
+ }
139
+
140
+ function taskChecks(root, specIds) {
141
+ let listing;
142
+ try { listing = task.list(root); } catch (error) {
143
+ return [check('TASK_INVALID', 'error', 'fail', error.message,
144
+ '.code-flow/tasks', '修复任务文档')];
145
+ }
146
+ const checks = listing.diagnostics.map(item => check(
147
+ item.code && item.code.startsWith('task_depends') ? 'TASK_DEPENDENCY_INVALID' : 'TASK_INVALID',
148
+ 'error', 'fail', item.message, item.source || '.code-flow/tasks', '修复任务文档'));
149
+ for (const item of listing.items) {
150
+ for (const ref of item.spec_refs) {
151
+ if (!specIds.has(ref.split('#')[0])) checks.push(check('SPEC_REF_MISSING', 'error', 'fail',
152
+ item.task + ' 引用了不存在的 Spec: ' + ref, item.task_file, '修复 Spec-Refs'));
153
+ }
154
+ }
155
+ const files = [...new Set(listing.items.map(item => item.task_file))];
156
+ for (const file of files) {
157
+ try { taskdoc.batches(task.loadDocument(root, file).tasks); } catch (error) {
158
+ checks.push(check('TASK_DEPENDENCY_INVALID', 'error', 'fail', error.message,
159
+ file, '修复 Depends 或任务状态'));
160
+ }
161
+ }
162
+ if (!checks.length) checks.push(pass('TASK_OK', '任务文档与依赖有效。', '.code-flow/tasks'));
163
+ return checks;
164
+ }
165
+
166
+ function worktreeList(root) {
167
+ const raw = gitRead(root, ['worktree', 'list', '--porcelain']);
168
+ if (raw === null) return null;
169
+ const entries = [];
170
+ let current = null;
171
+ for (const line of raw.split('\n')) {
172
+ if (line.startsWith('worktree ')) { current = { path: line.slice(9), branch: null, head: null }; entries.push(current); }
173
+ else if (current && line.startsWith('HEAD ')) current.head = line.slice(5);
174
+ else if (current && line.startsWith('branch ')) current.branch = line.slice(7);
175
+ }
176
+ return entries;
177
+ }
178
+
179
+ function registeredWorker(registered, absolute) {
180
+ return registered.find(item => {
181
+ try { return f.realpath(item.path) === f.realpath(absolute); } catch (_) { return false; }
182
+ });
183
+ }
184
+
185
+ function workerFindings(root, ledger, registered, record) {
186
+ const checks = [];
187
+ for (const worker of ledger.worktrees) {
188
+ if (worker.status === 'removed') continue;
189
+ const absolute = path.join(root, worker.path);
190
+ if (!fs.existsSync(absolute) || !registeredWorker(registered, absolute)) {
191
+ checks.push(check('WORKTREE_RECOVERY_REQUIRED', 'warning', 'fail',
192
+ '缺少注册的 worker worktree: ' + worker.task, worker.path || record.file,
193
+ '按 ledger 重建或显式放弃该 run'));
194
+ } else if (worker.status === 'merged' && worker.collected_head &&
195
+ gitRead(root, ['merge-base', '--is-ancestor', worker.collected_head, 'HEAD']) === null) {
196
+ checks.push(check('WORKTREE_RECOVERY_REQUIRED', 'warning', 'fail',
197
+ '已合并 worker 的提交不在协调历史中: ' + worker.task, worker.path,
198
+ '检查分支重写并显式恢复 ledger'));
199
+ }
200
+ }
201
+ return checks;
202
+ }
203
+
204
+ function runFindings(root, ledger, registered, record) {
205
+ const checks = [];
206
+ let rootMatches = false;
207
+ try { rootMatches = f.realpath(ledger.coordinator.root) === f.realpath(root); } catch (_) { rootMatches = false; }
208
+ if (!rootMatches) {
209
+ checks.push(check('WORKTREE_IDENTITY_MISMATCH', 'error', 'fail',
210
+ 'ledger 协调工作区与当前项目不一致: ' + record.run_id, record.file,
211
+ '在记录中的协调工作区运行 doctor'));
212
+ return checks;
213
+ }
214
+ if (ledger.status !== 'done' && ledger.status !== 'aborted') {
215
+ checks.push(check('WORKTREE_RECOVERY_REQUIRED', 'warning', 'fail',
216
+ 'worktree run 尚未完成: ' + record.status, record.file,
217
+ '执行 code-flow task parallel status 后继续 merge/cleanup'));
218
+ }
219
+ if (!registered) return checks.concat(check('WORKTREE_RECOVERY_REQUIRED', 'warning', 'fail',
220
+ 'Git worktree 注册信息不可读取: ' + record.run_id, record.file, '检查 Git worktree 注册'));
221
+ if (ledger.coordinator.branch && gitRead(root, ['symbolic-ref', '-q', 'HEAD']) !== ledger.coordinator.branch) {
222
+ checks.push(check('WORKTREE_IDENTITY_MISMATCH', 'error', 'fail',
223
+ '协调分支已切换: ' + record.run_id, record.file, '切回原分支或显式放弃 run'));
224
+ }
225
+ return checks.concat(workerFindings(root, ledger, registered, record));
226
+ }
227
+
228
+ function worktreeChecks(root) {
229
+ let records;
230
+ try { records = runs.list(root); } catch (error) {
231
+ return [check('WORKTREE_LEDGER_INVALID', 'error', 'fail', error.message,
232
+ '.code-flow/worktrees', '检查 worktree run 目录')];
233
+ }
234
+ if (!records.length) return [pass('WORKTREE_OK', '没有未完成的 worktree run。', '.code-flow/worktrees')];
235
+ const checks = [], registered = worktreeList(root);
236
+ for (const record of records) {
237
+ if (record.status === 'invalid') {
238
+ checks.push(check('WORKTREE_LEDGER_INVALID', 'error', 'fail',
239
+ record.error || 'ledger 无效', record.file, '修复或显式放弃该 run'));
240
+ continue;
241
+ }
242
+ checks.push(...runFindings(root, record.ledger, registered, record));
243
+ }
244
+ return checks.length ? checks : [pass('WORKTREE_OK', 'worktree run 状态可读取。', '.code-flow/worktrees')];
245
+ }
246
+
247
+ function summary(checks) {
248
+ const result = { pass: 0, warning: 0, error: 0, unknown: 0 };
249
+ for (const item of checks) {
250
+ if (item.status === 'pass') result.pass++;
251
+ else if (item.status === 'unknown') result.unknown++;
252
+ if (item.severity === 'warning' && item.status !== 'pass') result.warning++;
253
+ if (item.severity === 'error' && item.status !== 'pass') result.error++;
254
+ }
255
+ return result;
256
+ }
257
+
258
+ function text(checks, totals) {
259
+ return checks.map(item => '[' + item.status.toUpperCase() + '] ' + item.code + ' ' +
260
+ item.message + (item.path ? ' (' + item.path + ')' : '')).join('\n') + '\n' +
261
+ 'pass=' + totals.pass + ' warning=' + totals.warning +
262
+ ' error=' + totals.error + ' unknown=' + totals.unknown + '\n';
263
+ }
264
+
265
+ function run(root) {
266
+ const real = f.realpath(root);
267
+ const configured = configChecks(real), settings = configured[0].value;
268
+ const specs = specChecks(real, settings);
269
+ const checks = [
270
+ ...configured[0].checks,
271
+ ...templateChecks(real),
272
+ ...launcherChecks(real),
273
+ ...codexChecks(real),
274
+ ...specs.checks,
275
+ ...taskChecks(real, specs.ids),
276
+ ...worktreeChecks(real)
277
+ ];
278
+ const totals = summary(checks);
279
+ return { schema_version: 1, root: real, ok: totals.error === 0, summary: totals,
280
+ checks, text: text(checks, totals) };
281
+ }
282
+
283
+ module.exports = { run, TEMPLATES, LAUNCHER };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const { object, demand, FlowError } = require('./fs');
4
+
5
+ function parse(raw) {
6
+ const bom = raw.startsWith('\ufeff') ? '\ufeff' : '';
7
+ const text = raw.slice(bom.length);
8
+ const match = text.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/);
9
+ demand(match, 'frontmatter_missing', 'Expected JSON frontmatter');
10
+ let meta;
11
+ try { meta = JSON.parse(match[1]); } catch (error) {
12
+ throw new FlowError('frontmatter_invalid', 'Invalid JSON frontmatter: ' + error.message);
13
+ }
14
+ object(meta, 'frontmatter');
15
+ return { meta, body: text.slice(match[0].length), header: match[0], bom,
16
+ eol: match[0].includes('\r\n') ? '\r\n' : '\n', raw };
17
+ }
18
+ function stringify(document, meta = document.meta, body = document.body) {
19
+ const eol = document.eol || '\n';
20
+ const header = document.header && JSON.stringify(meta) === JSON.stringify(document.meta)
21
+ ? document.header : '---' + eol + JSON.stringify(meta, null, 2).replace(/\n/g, eol) + eol + '---' + eol;
22
+ return (document.bom || '') + header + body;
23
+ }
24
+ function create(meta, body = '') {
25
+ return stringify({ meta: {}, body, eol: '\n', bom: '' }, meta, body);
26
+ }
27
+ function readHeader(file) {
28
+ const fd = fs.openSync(file, 'r');
29
+ const chunks = [];
30
+ try {
31
+ for (let total = 0; total < 65536; total += 1024) {
32
+ const buffer = Buffer.alloc(1024);
33
+ const size = fs.readSync(fd, buffer, 0, buffer.length, null);
34
+ if (!size) break;
35
+ chunks.push(buffer.subarray(0, size));
36
+ const text = Buffer.concat(chunks).toString('utf8');
37
+ if (/^\ufeff?---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/.test(text)) return parse(text).meta;
38
+ }
39
+ throw new FlowError('frontmatter_missing', 'Missing or oversized JSON frontmatter: ' + file);
40
+ } finally { fs.closeSync(fd); }
41
+ }
42
+ module.exports = { parse, stringify, create, readHeader };
package/lib/fs.js ADDED
@@ -0,0 +1,141 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const os = require('node:os');
5
+ const crypto = require('node:crypto');
6
+
7
+ class FlowError extends Error {
8
+ constructor(code, message, exitCode = 2) {
9
+ super(message); this.name = 'FlowError'; this.code = code; this.exitCode = exitCode;
10
+ }
11
+ }
12
+ function demand(condition, code, message, exitCode = 2) {
13
+ if (!condition) throw new FlowError(code, message, exitCode);
14
+ }
15
+ function object(value, label) {
16
+ demand(value !== null && typeof value === 'object' && !Array.isArray(value),
17
+ 'invalid_data', label + ' must be an object');
18
+ return value;
19
+ }
20
+ function string(value, label) {
21
+ demand(typeof value === 'string' && value.trim().length > 0 && !value.includes('\0'),
22
+ 'invalid_data', label + ' must be a nonempty string');
23
+ return value;
24
+ }
25
+ function strings(value, label) {
26
+ demand(Array.isArray(value), 'invalid_data', label + ' must be an array');
27
+ return value.map(item => string(item, label));
28
+ }
29
+ function positive(value, label, integer = false) {
30
+ demand(typeof value === 'number' && Number.isFinite(value) && value > 0 &&
31
+ (!integer || Number.isSafeInteger(value)), 'invalid_data', label + ' must be positive' + (integer ? ' integer' : ''));
32
+ return value;
33
+ }
34
+ function hash(value) { return crypto.createHash('sha256').update(value).digest('hex'); }
35
+ function readText(file, maximum = 4 * 1024 * 1024) {
36
+ try {
37
+ demand(fs.statSync(file).size <= maximum, 'file_too_large', 'File too large: ' + file);
38
+ return fs.readFileSync(file, 'utf8');
39
+ } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
40
+ }
41
+ function realpath(value) {
42
+ return fs.realpathSync.native(value);
43
+ }
44
+ function within(root, candidate) {
45
+ const relative = path.relative(root, candidate);
46
+ return relative === '' || (!relative.startsWith('..' + path.sep) && relative !== '..' && !path.isAbsolute(relative));
47
+ }
48
+ function inside(root, relative) {
49
+ string(relative, 'path');
50
+ const clean = relative.replace(/\\/g, '/');
51
+ demand(!path.posix.isAbsolute(clean) && !path.win32.isAbsolute(relative) &&
52
+ !clean.split('/').includes('..') && !/[\r\n]/.test(clean), 'invalid_path', 'Invalid path: ' + relative);
53
+ const base = realpath(root);
54
+ let current = base;
55
+ for (const part of clean.split('/').filter(p => p && p !== '.')) {
56
+ const next = path.join(current, part);
57
+ if (!fs.existsSync(next) && fs.existsSync(current) && fs.statSync(current).isDirectory()) {
58
+ const entries = fs.readdirSync(current);
59
+ demand(!entries.some(name => name !== part && name.toLowerCase() === part.toLowerCase()) ||
60
+ entries.includes(part), 'invalid_path', 'Path case mismatch: ' + relative);
61
+ }
62
+ current = next;
63
+ if (fs.existsSync(current)) {
64
+ const real = realpath(current);
65
+ demand(real === current || real.toLowerCase() !== current.toLowerCase(),
66
+ 'invalid_path', 'Path case mismatch: ' + relative);
67
+ demand(within(base, real), 'invalid_path', 'Path escapes workspace: ' + relative);
68
+ }
69
+ }
70
+ demand(within(base, current), 'invalid_path', 'Path escapes workspace: ' + relative);
71
+ return current;
72
+ }
73
+ function removeIfPresent(file) {
74
+ try { fs.unlinkSync(file); } catch (error) { if (error.code !== 'ENOENT') throw error; }
75
+ }
76
+ function atomicWrite(file, text, before = undefined) {
77
+ fs.mkdirSync(path.dirname(file), { recursive: true });
78
+ const temporary = path.join(path.dirname(file), '.' + path.basename(file) + '.' + crypto.randomUUID() + '.tmp');
79
+ let fd;
80
+ try {
81
+ fd = fs.openSync(temporary, 'wx', 0o600);
82
+ fs.writeFileSync(fd, text, 'utf8'); fs.fsyncSync(fd); fs.closeSync(fd); fd = undefined;
83
+ demand(before === undefined || readText(file) === before, 'concurrent_change', 'Concurrent edit: ' + file, 1);
84
+ fs.renameSync(temporary, file);
85
+ } finally {
86
+ if (fd !== undefined) fs.closeSync(fd);
87
+ removeIfPresent(temporary);
88
+ }
89
+ }
90
+ function transientRoot(root) {
91
+ return path.join(os.tmpdir(), 'code-flow', hash(realpath(root)));
92
+ }
93
+ function lockDirectory(root) {
94
+ return path.join(transientRoot(root), 'locks');
95
+ }
96
+ function acquireIn(directory, key) {
97
+ fs.mkdirSync(directory, { recursive: true });
98
+ const file = path.join(directory, hash(key) + '.lock');
99
+ const owner = crypto.randomUUID();
100
+ for (let attempt = 0; attempt < 2; attempt++) {
101
+ try {
102
+ const fd = fs.openSync(file, 'wx', 0o600);
103
+ try { fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, owner })); } finally { fs.closeSync(fd); }
104
+ return { file, owner, key };
105
+ } catch (error) {
106
+ if (error.code !== 'EEXIST') throw error;
107
+ const raw = readText(file);
108
+ if (raw === null) continue;
109
+ let record;
110
+ try { record = JSON.parse(raw); } catch (failure) {
111
+ throw new FlowError('lock_invalid', 'Invalid lock JSON: ' + failure.message, 1);
112
+ }
113
+ demand(Number.isInteger(record.pid) && record.pid > 0, 'lock_invalid', 'Invalid lock owner: ' + file, 1);
114
+ try { process.kill(record.pid, 0); } catch (failure) {
115
+ if (failure.code === 'ESRCH' && readText(file) === raw) { removeIfPresent(file); continue; }
116
+ if (failure.code !== 'EPERM') throw failure;
117
+ }
118
+ throw new FlowError('locked', 'Resource lock is held: ' + key, 1);
119
+ }
120
+ }
121
+ throw new FlowError('locked', 'Cannot acquire lock: ' + key, 1);
122
+ }
123
+ function acquire(root, key) {
124
+ return acquireIn(lockDirectory(root), key);
125
+ }
126
+ function releaseLock(lock) {
127
+ const raw = readText(lock.file);
128
+ if (raw === null) return;
129
+ demand(JSON.parse(raw).owner === lock.owner, 'lock_changed', 'Lock ownership changed', 1);
130
+ removeIfPresent(lock.file);
131
+ }
132
+ async function withLockIn(directory, key, operation) {
133
+ const lock = acquireIn(directory, key);
134
+ try { return await operation(); } finally { releaseLock(lock); }
135
+ }
136
+ async function withLock(root, key, operation) {
137
+ return withLockIn(lockDirectory(root), key, operation);
138
+ }
139
+ module.exports = { FlowError, demand, object, string, strings, positive, hash,
140
+ readText, realpath, within, inside, removeIfPresent, atomicWrite, transientRoot,
141
+ acquireIn, releaseLock, withLockIn, withLock };
package/lib/glob.js ADDED
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+ const { demand, string } = require('./fs');
3
+
4
+ function compile(pattern) {
5
+ string(pattern, 'glob');
6
+ demand(!/[\\{}[\]()!]/.test(pattern) && !pattern.startsWith('/') &&
7
+ !pattern.split('/').includes('..'), 'invalid_glob', 'Unsupported glob: ' + pattern);
8
+ let result = '^';
9
+ for (let i = 0; i < pattern.length; i++) {
10
+ const char = pattern[i];
11
+ if (char === '*' && pattern[i + 1] === '*') {
12
+ demand((i === 0 || pattern[i - 1] === '/') &&
13
+ (i + 2 === pattern.length || pattern[i + 2] === '/'), 'invalid_glob', 'Invalid ** glob: ' + pattern);
14
+ if (pattern[i + 2] === '/') { result += '(?:[^/]+/)*'; i += 2; }
15
+ else { result += '.*'; i++; }
16
+ } else if (char === '*') result += '[^/]*';
17
+ else if (char === '?') result += '[^/]';
18
+ else result += char.replace(/[.+^$|]/g, '\\$&');
19
+ }
20
+ return new RegExp(result + '$');
21
+ }
22
+ function matches(pattern, file) { return compile(pattern).test(file.replace(/\\/g, '/')); }
23
+ module.exports = { compile, matches };