cowork-flow 0.0.1

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 (101) hide show
  1. package/README.md +193 -0
  2. package/bin/cowork-flow.js +9 -0
  3. package/package.json +28 -0
  4. package/src/cli.js +61 -0
  5. package/src/commands/init.js +88 -0
  6. package/src/commands/sync.js +33 -0
  7. package/src/commands/update.js +60 -0
  8. package/src/lib/copy-template.js +189 -0
  9. package/src/lib/package-info.js +49 -0
  10. package/src/lib/paths.js +6 -0
  11. package/template/.agent/skills/break-loop/SKILL.md +131 -0
  12. package/template/.agent/skills/check-cross-layer/SKILL.md +158 -0
  13. package/template/.agent/skills/finish-work/SKILL.md +163 -0
  14. package/template/.agent/skills/record-session/SKILL.md +78 -0
  15. package/template/.agent/skills/start/SKILL.md +390 -0
  16. package/template/.agent/skills/update-spec/SKILL.md +335 -0
  17. package/template/.cowork-flow/.version +1 -0
  18. package/template/.cowork-flow/changes/archive/.gitkeep +1 -0
  19. package/template/.cowork-flow/config.yaml +49 -0
  20. package/template/.cowork-flow/plans/.gitkeep +1 -0
  21. package/template/.cowork-flow/scripts/add_session.py +441 -0
  22. package/template/.cowork-flow/scripts/change.py +314 -0
  23. package/template/.cowork-flow/scripts/common/__init__.py +84 -0
  24. package/template/.cowork-flow/scripts/common/config.py +128 -0
  25. package/template/.cowork-flow/scripts/common/developer.py +205 -0
  26. package/template/.cowork-flow/scripts/common/files.py +24 -0
  27. package/template/.cowork-flow/scripts/common/git_context.py +628 -0
  28. package/template/.cowork-flow/scripts/common/paths.py +350 -0
  29. package/template/.cowork-flow/scripts/common/task_utils.py +200 -0
  30. package/template/.cowork-flow/scripts/get_context.py +16 -0
  31. package/template/.cowork-flow/scripts/get_developer.py +26 -0
  32. package/template/.cowork-flow/scripts/init_developer.py +51 -0
  33. package/template/.cowork-flow/scripts/task.py +1378 -0
  34. package/template/.cowork-flow/spec/backend/database-guidelines.md +26 -0
  35. package/template/.cowork-flow/spec/backend/directory-structure.md +27 -0
  36. package/template/.cowork-flow/spec/backend/error-handling.md +20 -0
  37. package/template/.cowork-flow/spec/backend/index.md +36 -0
  38. package/template/.cowork-flow/spec/backend/logging-guidelines.md +20 -0
  39. package/template/.cowork-flow/spec/backend/quality-guidelines.md +24 -0
  40. package/template/.cowork-flow/spec/frontend/component-guidelines.md +19 -0
  41. package/template/.cowork-flow/spec/frontend/directory-structure.md +18 -0
  42. package/template/.cowork-flow/spec/frontend/hook-guidelines.md +17 -0
  43. package/template/.cowork-flow/spec/frontend/index.md +28 -0
  44. package/template/.cowork-flow/spec/frontend/quality-guidelines.md +23 -0
  45. package/template/.cowork-flow/spec/frontend/state-management.md +18 -0
  46. package/template/.cowork-flow/spec/frontend/type-safety.md +18 -0
  47. package/template/.cowork-flow/spec/guides/code-reuse-thinking-guide.md +26 -0
  48. package/template/.cowork-flow/spec/guides/cross-layer-thinking-guide.md +21 -0
  49. package/template/.cowork-flow/spec/guides/index.md +31 -0
  50. package/template/.cowork-flow/spec/guides/pre-implementation-checklist.md +15 -0
  51. package/template/.cowork-flow/tasks/.gitkeep +1 -0
  52. package/template/.cowork-flow/tasks/archive/.gitkeep +1 -0
  53. package/template/.cowork-flow/workflow.md +411 -0
  54. package/template/.cowork-flow/workspace/index.md +95 -0
  55. package/template/.superpowers/brainstorming/SKILL.md +164 -0
  56. package/template/.superpowers/brainstorming/scripts/frame-template.html +214 -0
  57. package/template/.superpowers/brainstorming/scripts/helper.js +88 -0
  58. package/template/.superpowers/brainstorming/scripts/server.cjs +354 -0
  59. package/template/.superpowers/brainstorming/scripts/start-server.sh +148 -0
  60. package/template/.superpowers/brainstorming/scripts/stop-server.sh +56 -0
  61. package/template/.superpowers/brainstorming/spec-document-reviewer-prompt.md +49 -0
  62. package/template/.superpowers/brainstorming/visual-companion.md +287 -0
  63. package/template/.superpowers/dispatching-parallel-agents/SKILL.md +182 -0
  64. package/template/.superpowers/executing-plans/SKILL.md +70 -0
  65. package/template/.superpowers/finishing-a-development-branch/SKILL.md +251 -0
  66. package/template/.superpowers/receiving-code-review/SKILL.md +213 -0
  67. package/template/.superpowers/requesting-code-review/SKILL.md +103 -0
  68. package/template/.superpowers/requesting-code-review/code-reviewer.md +168 -0
  69. package/template/.superpowers/subagent-driven-development/SKILL.md +279 -0
  70. package/template/.superpowers/subagent-driven-development/code-quality-reviewer-prompt.md +25 -0
  71. package/template/.superpowers/subagent-driven-development/implementer-prompt.md +113 -0
  72. package/template/.superpowers/subagent-driven-development/spec-reviewer-prompt.md +61 -0
  73. package/template/.superpowers/systematic-debugging/CREATION-LOG.md +119 -0
  74. package/template/.superpowers/systematic-debugging/SKILL.md +296 -0
  75. package/template/.superpowers/systematic-debugging/condition-based-waiting-example.ts +158 -0
  76. package/template/.superpowers/systematic-debugging/condition-based-waiting.md +115 -0
  77. package/template/.superpowers/systematic-debugging/defense-in-depth.md +122 -0
  78. package/template/.superpowers/systematic-debugging/find-polluter.sh +63 -0
  79. package/template/.superpowers/systematic-debugging/root-cause-tracing.md +169 -0
  80. package/template/.superpowers/systematic-debugging/test-academic.md +14 -0
  81. package/template/.superpowers/systematic-debugging/test-pressure-1.md +58 -0
  82. package/template/.superpowers/systematic-debugging/test-pressure-2.md +68 -0
  83. package/template/.superpowers/systematic-debugging/test-pressure-3.md +69 -0
  84. package/template/.superpowers/test-driven-development/SKILL.md +371 -0
  85. package/template/.superpowers/test-driven-development/testing-anti-patterns.md +299 -0
  86. package/template/.superpowers/using-git-worktrees/SKILL.md +215 -0
  87. package/template/.superpowers/using-superpowers/SKILL.md +117 -0
  88. package/template/.superpowers/using-superpowers/references/codex-tools.md +59 -0
  89. package/template/.superpowers/using-superpowers/references/copilot-tools.md +42 -0
  90. package/template/.superpowers/using-superpowers/references/gemini-tools.md +51 -0
  91. package/template/.superpowers/verification-before-completion/SKILL.md +139 -0
  92. package/template/.superpowers/writing-plans/SKILL.md +152 -0
  93. package/template/.superpowers/writing-plans/plan-document-reviewer-prompt.md +49 -0
  94. package/template/.superpowers/writing-skills/SKILL.md +655 -0
  95. package/template/.superpowers/writing-skills/anthropic-best-practices.md +1150 -0
  96. package/template/.superpowers/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  97. package/template/.superpowers/writing-skills/graphviz-conventions.dot +172 -0
  98. package/template/.superpowers/writing-skills/persuasion-principles.md +187 -0
  99. package/template/.superpowers/writing-skills/render-graphs.js +168 -0
  100. package/template/.superpowers/writing-skills/testing-skills-with-subagents.md +384 -0
  101. package/template/AGENTS.md +116 -0
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # cowork-flow
2
+
3
+ cowork-flow 是一个用于新项目初始化协作流程的模板仓库。它把项目说明、任务流转、规格治理、开发计划和会话记录放在一套可复制的目录结构里,帮助团队在项目早期就建立清晰的工作闭环。
4
+
5
+ 这个仓库本身不绑定具体技术栈,也不提供业务代码脚手架。它提供的是协作与治理基础设施:你可以把 `template/` 复制到目标项目中,再按目标项目的真实情况补充技术栈、验证命令、目录规范和提交策略。
6
+
7
+ ## 适用场景
8
+
9
+ - 新项目需要建立 `AGENTS.md`、任务流、规格文档和开发记录。
10
+ - 已有项目希望补上一套轻量的协作流程,但不想引入完整工程脚手架。
11
+ - 团队希望把需求澄清、规格变更、实现计划、验证和会话记录串成闭环。
12
+
13
+ ## 不适用场景
14
+
15
+ - 只需要一个语言或框架脚手架,例如 React、Spring Boot、Rust crate 模板。
16
+ - 项目已经有成熟且完整的任务、规格和协作系统,并且不希望引入新的目录约定。
17
+ - 只想复制某一段提示词,而不需要维护项目级流程文件。
18
+
19
+ ## 仓库结构
20
+
21
+ ```text
22
+ .
23
+ ├── README.md
24
+ └── template/
25
+ ├── AGENTS.md
26
+ ├── .agent/
27
+ │ └── skills/
28
+ └── .cowork-flow/
29
+ ├── config.yaml
30
+ ├── workflow.md
31
+ ├── scripts/
32
+ ├── spec/
33
+ ├── changes/
34
+ ├── plans/
35
+ ├── tasks/
36
+ └── workspace/
37
+ ```
38
+
39
+ ## 模板内容
40
+
41
+ `template/AGENTS.md`
42
+ 项目级协作入口,包含编码前思考、简单优先、外科手术式改动、验证优先等基础原则。接入项目后,应把项目名称、技术栈、运行命令、测试命令和提交策略补齐。
43
+
44
+ `template/.agent/skills/`
45
+ 本地技能入口,覆盖开始工作、收尾验证、记录 session、更新规范、跨层检查等常见协作动作。这里的 skill 应保持通用,不承载某个业务项目的一次性细节。
46
+
47
+ `template/.cowork-flow/`
48
+ 工作流目录,包含流程说明、任务状态、开发者工作区、项目规范、行为变更规格、实现计划和辅助脚本。`.cowork-flow/config.yaml` 用于填写项目自己的 lint、build、test 等验证命令。
49
+
50
+ `template/.cowork-flow/spec/`
51
+ 项目规范目录,预置了 backend、frontend 和 guides 三类说明。接入时应按项目事实保留、改写或删除对应规范。
52
+
53
+ `template/.cowork-flow/changes/`
54
+ 由 `change.py` 管理 proposal、design、behavior specs 和归档,不维护实现 checklist。
55
+
56
+ `template/.cowork-flow/plans/`
57
+ 保存可执行步骤、验证方式和执行状态。
58
+
59
+ ## 快速开始
60
+
61
+ 使用 CLI 把模板内容安装到目标项目根目录:
62
+
63
+ ```bash
64
+ npx cowork-flow init ./my-project
65
+ ```
66
+
67
+ 也可以先全局安装:
68
+
69
+ ```bash
70
+ npm install -g cowork-flow
71
+ cowork-flow init ./my-project
72
+ ```
73
+
74
+ 初始化后优先完成这些配置:
75
+
76
+ 1. 更新 `AGENTS.md` 中的项目名称、技术栈、命令和提交策略。
77
+ 2. 更新 `.cowork-flow/config.yaml` 中的验证命令。
78
+ 3. 更新 `.cowork-flow/workflow.md` 中与项目流程不一致的门禁、分级和完成定义。
79
+ 4. 按项目实际情况调整 `.cowork-flow/spec/`,删除不存在的 frontend、backend 或行为变更场景。
80
+ 5. 按团队实践使用 `.cowork-flow/changes/` 管理规格变更,使用 `.cowork-flow/plans/` 管理实现计划和验证状态。
81
+
82
+ ## CLI 使用
83
+
84
+ 查看命令:
85
+
86
+ ```bash
87
+ npx cowork-flow --help
88
+ ```
89
+
90
+ 初始化到新项目:
91
+
92
+ ```bash
93
+ cowork-flow init ./my-project
94
+ ```
95
+
96
+ `init` 会询问你是否已经安装了 Superpowers skills。选择未安装时,会把内置的 `.superpowers/` 技能复制到目标项目的 `.agent/skills/` 下。
97
+ 在非交互环境里,这一步会自动跳过询问并默认视为已安装。
98
+
99
+ 初始化到当前项目:
100
+
101
+ ```bash
102
+ cowork-flow init .
103
+ ```
104
+
105
+ 默认不会覆盖已有文件。需要预览时使用:
106
+
107
+ ```bash
108
+ cowork-flow init ./my-project --dry-run
109
+ ```
110
+
111
+ 需要明确覆盖已有文件时使用:
112
+
113
+ ```bash
114
+ cowork-flow init ./my-project --force
115
+ ```
116
+
117
+ 升级 CLI 本身:
118
+
119
+ ```bash
120
+ cowork-flow update
121
+ npm install -g cowork-flow@latest
122
+ ```
123
+
124
+ 同步已初始化项目中的模板脚本和本地技能:
125
+
126
+ ```bash
127
+ cowork-flow sync .
128
+ cowork-flow sync . --dry-run
129
+ ```
130
+
131
+ `sync` 默认保护 `AGENTS.md`、`.cowork-flow/config.yaml`、`.cowork-flow/workflow.md`、`.cowork-flow/spec/`、任务、计划、变更和 workspace 记录。只有明确传入 `--force` 时才覆盖保护文件。
132
+
133
+ ## 常用入口
134
+
135
+ 初始化或查看开发者身份:
136
+
137
+ ```bash
138
+ python3 ./.cowork-flow/scripts/get_developer.py
139
+ python3 ./.cowork-flow/scripts/init_developer.py <developer-name>
140
+ ```
141
+
142
+ 查看当前上下文:
143
+
144
+ ```bash
145
+ python3 ./.cowork-flow/scripts/get_context.py
146
+ python3 ./.cowork-flow/scripts/task.py list
147
+ ```
148
+
149
+ 创建并验证行为变更:
150
+
151
+ ```bash
152
+ python3 ./.cowork-flow/scripts/change.py create <slug>
153
+ python3 ./.cowork-flow/scripts/change.py validate <slug>
154
+ ```
155
+
156
+ 创建并启动任务:
157
+
158
+ ```bash
159
+ python3 ./.cowork-flow/scripts/task.py create "<title>" --slug <task-name>
160
+ python3 ./.cowork-flow/scripts/task.py start <task-dir>
161
+ ```
162
+
163
+ 记录 session:
164
+
165
+ ```bash
166
+ python3 ./.cowork-flow/scripts/get_context.py --mode record
167
+ python3 ./.cowork-flow/scripts/add_session.py \
168
+ --title "<session-title>" \
169
+ --commit "<commit-or-handoff-ref>" \
170
+ --summary "<summary>"
171
+ ```
172
+
173
+ ## 接入原则
174
+
175
+ - 以目标项目事实为准,不把模板内容当成项目事实。
176
+ - 保留有价值的流程骨架,删除目标项目不存在的场景。
177
+ - 项目差异写入 `AGENTS.md`、`.cowork-flow/workflow.md`、`.cowork-flow/config.yaml` 和 `.cowork-flow/spec/`。
178
+ - 不为了替换项目命令或一次性任务而改写通用 skill。
179
+ - README 只说明项目定位和使用方式;具体协作规则应沉淀在模板内对应文件中。
180
+
181
+ ## 维护建议
182
+
183
+ 更新模板时,优先确认这几件事:
184
+
185
+ - `template/` 中的目录结构和 README 描述一致。
186
+ - 新增流程规则有明确承载位置,不散落在多个文件里互相冲突。
187
+ - 脚本入口和文档中的命令保持一致。
188
+ - 通用 skill 保持可复用,项目专用规则不混入 starter 模板。
189
+ - 示例命令尽量保持最小可用,避免把模板写成某个具体项目的实现方案。
190
+
191
+ ## 发布流程
192
+
193
+ CI 会运行 Node CLI 测试、npm pack 内容检查和现有 Python 模板测试。发布到 npm 通过 GitHub Actions 的 `Publish npm Package` workflow 完成,需要在仓库 secrets 中配置 `NPM_TOKEN`。
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/cli.js';
3
+
4
+ try {
5
+ process.exitCode = await main(process.argv.slice(2));
6
+ } catch (error) {
7
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
8
+ process.exitCode = 1;
9
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "cowork-flow",
3
+ "version": "0.0.1",
4
+ "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
+ "type": "module",
6
+ "bin": {
7
+ "cowork-flow": "./bin/cowork-flow.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "template",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "test": "node --test",
17
+ "test:template": "python3 -m unittest discover tests -v",
18
+ "pack:check": "npm_config_cache=/tmp/cowork-flow-npm-cache npm pack --dry-run --json",
19
+ "test:all": "npm test && npm run test:template && npm run pack:check"
20
+ },
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "license": "MIT",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ }
28
+ }
package/src/cli.js ADDED
@@ -0,0 +1,61 @@
1
+ import { runInit } from './commands/init.js';
2
+ import { runSync } from './commands/sync.js';
3
+ import { runUpdate } from './commands/update.js';
4
+ import { readPackageInfo } from './lib/package-info.js';
5
+
6
+ const HELP = `cowork-flow
7
+
8
+ Usage:
9
+ cowork-flow init [target] [--dry-run] [--force]
10
+ cowork-flow update [--global] [--yes]
11
+ cowork-flow sync [target] [--dry-run] [--force]
12
+ cowork-flow --version
13
+ cowork-flow --help
14
+ `;
15
+
16
+ function defaultIo() {
17
+ return {
18
+ writeOut(message) {
19
+ process.stdout.write(message);
20
+ },
21
+ writeErr(message) {
22
+ process.stderr.write(message);
23
+ }
24
+ };
25
+ }
26
+
27
+ export async function main(argv = process.argv.slice(2), options = {}) {
28
+ const io = options.io ?? defaultIo();
29
+ const [command, ...args] = argv;
30
+
31
+ try {
32
+ if (!command || command === '--help' || command === '-h') {
33
+ io.writeOut(HELP);
34
+ return 0;
35
+ }
36
+
37
+ if (command === '--version' || command === '-v') {
38
+ const packageInfo = await readPackageInfo();
39
+ io.writeOut(`${packageInfo.version}\n`);
40
+ return 0;
41
+ }
42
+
43
+ if (command === 'init') {
44
+ return await runInit(args, { io, prompt: options.prompt });
45
+ }
46
+
47
+ if (command === 'sync') {
48
+ return await runSync(args, { io });
49
+ }
50
+
51
+ if (command === 'update') {
52
+ return await runUpdate(args, { io });
53
+ }
54
+
55
+ io.writeErr(`Unknown command: ${command}\n`);
56
+ return 1;
57
+ } catch (error) {
58
+ io.writeErr(`${error instanceof Error ? error.message : String(error)}\n`);
59
+ return 1;
60
+ }
61
+ }
@@ -0,0 +1,88 @@
1
+ import { resolve } from 'node:path';
2
+ import { createInterface } from 'node:readline/promises';
3
+
4
+ import {
5
+ applyPlan,
6
+ buildInitPlan,
7
+ buildSuperpowersPlan,
8
+ summarizePlan
9
+ } from '../lib/copy-template.js';
10
+ import { readPackageInfo } from '../lib/package-info.js';
11
+
12
+ function parseInitArgs(args) {
13
+ const options = { dryRun: false, force: false, target: process.cwd() };
14
+ for (const arg of args) {
15
+ if (arg === '--dry-run') {
16
+ options.dryRun = true;
17
+ } else if (arg === '--force') {
18
+ options.force = true;
19
+ } else if (arg.startsWith('--')) {
20
+ throw new Error(`Unknown init option: ${arg}`);
21
+ } else {
22
+ options.target = resolve(arg);
23
+ }
24
+ }
25
+ return options;
26
+ }
27
+
28
+ function isAffirmative(answer) {
29
+ const normalized = answer.trim().toLowerCase();
30
+ return normalized === 'y' || normalized === 'yes' || normalized === 'true' || normalized === '1';
31
+ }
32
+
33
+ async function defaultSuperpowersPrompt(io) {
34
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
35
+ io.writeOut('Superpowers prompt skipped because stdin is not interactive.\n');
36
+ return true;
37
+ }
38
+
39
+ const rl = createInterface({
40
+ input: process.stdin,
41
+ output: process.stdout
42
+ });
43
+
44
+ try {
45
+ const answer = await rl.question('Have you already installed Superpowers skills? [y/N] ');
46
+ return isAffirmative(answer);
47
+ } finally {
48
+ rl.close();
49
+ }
50
+ }
51
+
52
+ export async function runInit(args, { io, prompt }) {
53
+ return runInitWithOptions(args, { io, prompt });
54
+ }
55
+
56
+ export async function runInitWithOptions(args, { io, prompt = defaultSuperpowersPrompt }) {
57
+ const options = parseInitArgs(args);
58
+ const packageInfo = await readPackageInfo();
59
+ const plan = await buildInitPlan(options.target, {
60
+ force: options.force,
61
+ version: packageInfo.version
62
+ });
63
+
64
+ await applyPlan(plan, { dryRun: options.dryRun });
65
+ io.writeOut(summarizePlan(plan, options.dryRun));
66
+
67
+ if (options.dryRun) {
68
+ io.writeOut('Superpowers prompt skipped in dry-run.\n');
69
+ io.writeOut('Next: update AGENTS.md and .cowork-flow/config.yaml for this project.\n');
70
+ return 0;
71
+ }
72
+
73
+ const alreadyInstalled = await prompt(io);
74
+ if (alreadyInstalled) {
75
+ io.writeOut('Superpowers skills already installed; skipping bundled skills.\n');
76
+ io.writeOut('Next: update AGENTS.md and .cowork-flow/config.yaml for this project.\n');
77
+ return 0;
78
+ }
79
+
80
+ const superpowersPlan = await buildSuperpowersPlan(options.target, {
81
+ force: options.force
82
+ });
83
+ await applyPlan(superpowersPlan, { dryRun: false });
84
+ io.writeOut(summarizePlan(superpowersPlan, false));
85
+ io.writeOut('Superpowers skills were bundled into .agent/skills.\n');
86
+ io.writeOut('Next: update AGENTS.md and .cowork-flow/config.yaml for this project.\n');
87
+ return 0;
88
+ }
@@ -0,0 +1,33 @@
1
+ import { resolve } from 'node:path';
2
+
3
+ import { applyPlan, buildSyncPlan, summarizePlan } from '../lib/copy-template.js';
4
+ import { readPackageInfo } from '../lib/package-info.js';
5
+
6
+ function parseSyncArgs(args) {
7
+ const options = { dryRun: false, force: false, target: process.cwd() };
8
+ for (const arg of args) {
9
+ if (arg === '--dry-run') {
10
+ options.dryRun = true;
11
+ } else if (arg === '--force') {
12
+ options.force = true;
13
+ } else if (arg.startsWith('--')) {
14
+ throw new Error(`Unknown sync option: ${arg}`);
15
+ } else {
16
+ options.target = resolve(arg);
17
+ }
18
+ }
19
+ return options;
20
+ }
21
+
22
+ export async function runSync(args, { io }) {
23
+ const options = parseSyncArgs(args);
24
+ const packageInfo = await readPackageInfo();
25
+ const plan = await buildSyncPlan(options.target, {
26
+ force: options.force,
27
+ version: packageInfo.version
28
+ });
29
+
30
+ await applyPlan(plan, { dryRun: options.dryRun });
31
+ io.writeOut(summarizePlan(plan, options.dryRun));
32
+ return 0;
33
+ }
@@ -0,0 +1,60 @@
1
+ import {
2
+ compareVersions,
3
+ fetchLatestVersion,
4
+ readPackageInfo,
5
+ runGlobalInstall
6
+ } from '../lib/package-info.js';
7
+
8
+ function parseUpdateArgs(args) {
9
+ const options = { global: false, yes: false };
10
+ for (const arg of args) {
11
+ if (arg === '--global') {
12
+ options.global = true;
13
+ } else if (arg === '--yes') {
14
+ options.yes = true;
15
+ } else {
16
+ throw new Error(`Unknown update option: ${arg}`);
17
+ }
18
+ }
19
+ return options;
20
+ }
21
+
22
+ export async function runUpdate(args, deps = {}) {
23
+ const io = deps.io;
24
+ const readInfo = deps.readPackageInfo ?? readPackageInfo;
25
+ const fetchLatest = deps.fetchLatestVersion ?? fetchLatestVersion;
26
+ const installGlobal = deps.runGlobalInstall ?? runGlobalInstall;
27
+ const options = parseUpdateArgs(args);
28
+ const packageInfo = await readInfo();
29
+ const current = packageInfo.version;
30
+ const installCommand = 'npm install -g cowork-flow@latest';
31
+
32
+ let latest = null;
33
+ try {
34
+ latest = await fetchLatest('cowork-flow');
35
+ } catch (error) {
36
+ io.writeErr(`${error instanceof Error ? error.message : String(error)}\n`);
37
+ io.writeOut(`current=${current}\n`);
38
+ io.writeOut(`Unable to query npm latest. Run: ${installCommand}\n`);
39
+ return 0;
40
+ }
41
+
42
+ io.writeOut(`current=${current}\n`);
43
+ io.writeOut(`latest=${latest}\n`);
44
+
45
+ if (compareVersions(current, latest) >= 0) {
46
+ io.writeOut('cowork-flow is already up to date.\n');
47
+ return 0;
48
+ }
49
+
50
+ if (options.global && options.yes) {
51
+ const code = await installGlobal('cowork-flow@latest');
52
+ if (code === 0) {
53
+ io.writeOut('installed cowork-flow@latest\n');
54
+ }
55
+ return code;
56
+ }
57
+
58
+ io.writeOut(`Run: ${installCommand}\n`);
59
+ return 0;
60
+ }
@@ -0,0 +1,189 @@
1
+ import { constants } from 'node:fs';
2
+ import { access, copyFile, mkdir, readdir, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, relative } from 'node:path';
4
+
5
+ import { templateRoot } from './paths.js';
6
+
7
+ async function pathExists(path) {
8
+ try {
9
+ await access(path, constants.F_OK);
10
+ return true;
11
+ } catch (error) {
12
+ if (error && error.code === 'ENOENT') {
13
+ return false;
14
+ }
15
+ throw error;
16
+ }
17
+ }
18
+
19
+ async function listFiles(root, current = root) {
20
+ const entries = await readdir(current, { withFileTypes: true });
21
+ const files = [];
22
+
23
+ for (const entry of entries) {
24
+ const absolute = join(current, entry.name);
25
+ if (entry.isDirectory()) {
26
+ files.push(...await listFiles(root, absolute));
27
+ } else if (entry.isFile()) {
28
+ files.push(relative(root, absolute));
29
+ }
30
+ }
31
+
32
+ return files.sort();
33
+ }
34
+
35
+ export async function buildInitPlan(targetDir, options = {}) {
36
+ const files = await listFiles(templateRoot);
37
+ const actions = [];
38
+
39
+ for (const file of files) {
40
+ if (file === '.cowork-flow/.version' || file.startsWith('.superpowers/')) {
41
+ continue;
42
+ }
43
+
44
+ const source = join(templateRoot, file);
45
+ const destination = join(targetDir, file);
46
+ const exists = await pathExists(destination);
47
+ const action = exists ? (options.force ? 'update' : 'skip') : 'create';
48
+ actions.push({ action, source, destination, relativePath: file });
49
+ }
50
+
51
+ const versionDestination = join(targetDir, '.cowork-flow', '.version');
52
+ const versionExists = await pathExists(versionDestination);
53
+ actions.push({
54
+ action: versionExists ? (options.force ? 'update' : 'skip') : 'create',
55
+ source: null,
56
+ destination: versionDestination,
57
+ relativePath: '.cowork-flow/.version',
58
+ content: `${options.version}\n`
59
+ });
60
+
61
+ return actions;
62
+ }
63
+
64
+ export async function buildSuperpowersPlan(targetDir, options = {}) {
65
+ const sourceRoot = join(templateRoot, '.superpowers');
66
+ if (!await pathExists(sourceRoot)) {
67
+ return [];
68
+ }
69
+
70
+ const files = await listFiles(sourceRoot);
71
+ const actions = [];
72
+
73
+ for (const file of files) {
74
+ const source = join(sourceRoot, file);
75
+ const destination = join(targetDir, '.agent', 'skills', file);
76
+ const exists = await pathExists(destination);
77
+ const action = exists ? (options.force ? 'update' : 'skip') : 'create';
78
+ actions.push({ action, source, destination, relativePath: `.agent/skills/${file}` });
79
+ }
80
+
81
+ return actions;
82
+ }
83
+
84
+ const PROTECTED_SYNC_FILES = new Set([
85
+ 'AGENTS.md',
86
+ '.cowork-flow/config.yaml',
87
+ '.cowork-flow/workflow.md'
88
+ ]);
89
+
90
+ const PROTECTED_SYNC_PREFIXES = [
91
+ '.cowork-flow/spec/',
92
+ '.cowork-flow/workspace/',
93
+ '.cowork-flow/tasks/',
94
+ '.cowork-flow/changes/',
95
+ '.cowork-flow/plans/'
96
+ ];
97
+
98
+ const SAFE_SYNC_PREFIXES = [
99
+ '.agent/skills/',
100
+ '.cowork-flow/scripts/'
101
+ ];
102
+
103
+ const SAFE_SYNC_FILES = new Set([
104
+ '.cowork-flow/.gitignore',
105
+ '.cowork-flow/.version'
106
+ ]);
107
+
108
+ function isProtectedSyncFile(relativePath) {
109
+ return PROTECTED_SYNC_FILES.has(relativePath)
110
+ || PROTECTED_SYNC_PREFIXES.some((prefix) => relativePath.startsWith(prefix));
111
+ }
112
+
113
+ function isSafeSyncFile(relativePath) {
114
+ return SAFE_SYNC_FILES.has(relativePath)
115
+ || SAFE_SYNC_PREFIXES.some((prefix) => relativePath.startsWith(prefix))
116
+ || relativePath.endsWith('/.gitkeep');
117
+ }
118
+
119
+ export async function buildSyncPlan(targetDir, options = {}) {
120
+ if (!await pathExists(join(targetDir, '.cowork-flow'))) {
121
+ throw new Error(`Target is not initialized: ${targetDir}`);
122
+ }
123
+
124
+ const files = await listFiles(templateRoot);
125
+ const actions = [];
126
+
127
+ for (const file of files) {
128
+ if (file === '.cowork-flow/.version' || file.startsWith('.superpowers/')) {
129
+ continue;
130
+ }
131
+
132
+ const source = join(templateRoot, file);
133
+ const destination = join(targetDir, file);
134
+ const exists = await pathExists(destination);
135
+ const protectedFile = isProtectedSyncFile(file) && !options.force && exists;
136
+ const safeFile = isSafeSyncFile(file);
137
+
138
+ if (protectedFile) {
139
+ actions.push({ action: 'protected', source, destination, relativePath: file });
140
+ } else if (exists && (safeFile || options.force)) {
141
+ actions.push({ action: 'update', source, destination, relativePath: file });
142
+ } else if (!exists) {
143
+ actions.push({ action: 'create', source, destination, relativePath: file });
144
+ } else {
145
+ actions.push({ action: 'protected', source, destination, relativePath: file });
146
+ }
147
+ }
148
+
149
+ actions.push({
150
+ action: 'update',
151
+ source: null,
152
+ destination: join(targetDir, '.cowork-flow', '.version'),
153
+ relativePath: '.cowork-flow/.version',
154
+ content: `${options.version}\n`
155
+ });
156
+
157
+ return actions;
158
+ }
159
+
160
+ export function summarizePlan(actions, dryRun = false) {
161
+ const counts = { create: 0, update: 0, skip: 0, protected: 0 };
162
+ for (const action of actions) {
163
+ counts[action.action] += 1;
164
+ }
165
+
166
+ const prefix = dryRun ? 'dry-run ' : '';
167
+ const createLabel = dryRun ? 'would-create' : 'created';
168
+ const updateLabel = dryRun ? 'would-update' : 'updated';
169
+ return `${prefix}${createLabel}=${counts.create} ${updateLabel}=${counts.update} skipped=${counts.skip} protected=${counts.protected}\n`;
170
+ }
171
+
172
+ export async function applyPlan(actions, options = {}) {
173
+ if (options.dryRun) {
174
+ return;
175
+ }
176
+
177
+ for (const item of actions) {
178
+ if (item.action === 'skip' || item.action === 'protected') {
179
+ continue;
180
+ }
181
+
182
+ await mkdir(dirname(item.destination), { recursive: true });
183
+ if (item.content !== undefined) {
184
+ await writeFile(item.destination, item.content, 'utf8');
185
+ } else {
186
+ await copyFile(item.source, item.destination);
187
+ }
188
+ }
189
+ }