cowork-flow 0.0.19 → 0.0.21
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/README.md +65 -20
- package/package.json +1 -1
- package/src/cli.js +109 -2
- package/src/commands/init.js +61 -4
- package/src/commands/sync.js +10 -1
- package/src/lib/copy-template.js +38 -7
- package/src/lib/platforms.js +73 -0
- package/template/.agent/skills/brainstorming/SKILL.md +13 -9
- package/template/.agent/skills/finish-work/SKILL.md +3 -3
- package/template/.agent/skills/meta/SKILL.md +1 -1
- package/template/.agent/skills/start/SKILL.md +38 -14
- package/template/.agent/skills/writing-plans/SKILL.md +1 -1
- package/template/.claude/agents/cowork-check.md +41 -0
- package/template/.claude/agents/cowork-implement.md +40 -0
- package/template/.claude/agents/cowork-research.md +40 -0
- package/template/.claude/commands/cowork-check.md +27 -0
- package/template/.claude/commands/cowork-implement.md +27 -0
- package/template/.claude/commands/cowork-research.md +27 -0
- package/template/.claude/hooks/inject-workflow-state.py +298 -0
- package/template/.claude/settings.json +24 -0
- package/template/.claude/skills/before-dev/SKILL.md +31 -0
- package/template/.claude/skills/brainstorming/SKILL.md +32 -0
- package/template/.claude/skills/break-loop/SKILL.md +26 -0
- package/template/.claude/skills/check/SKILL.md +29 -0
- package/template/.claude/skills/continue/SKILL.md +29 -0
- package/template/.claude/skills/finish-work/SKILL.md +32 -0
- package/template/.claude/skills/meta/SKILL.md +30 -0
- package/template/.claude/skills/python-design/SKILL.md +27 -0
- package/template/.claude/skills/start/SKILL.md +78 -0
- package/template/.claude/skills/update-spec/SKILL.md +39 -0
- package/template/.claude/skills/writing-plans/SKILL.md +70 -0
- package/template/.codex/agents/cowork-check.toml +17 -22
- package/template/.codex/agents/cowork-implement.toml +18 -23
- package/template/.codex/agents/cowork-research.toml +14 -19
- package/template/.codex/agents/default.toml +27 -0
- package/template/.codex/agents/explorer.toml +28 -0
- package/template/.codex/agents/worker.toml +29 -0
- package/template/.codex/hooks/inject-workflow-state.py +172 -90
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/adapters/claude-code/adapter.yaml +35 -0
- package/template/.cowork-flow/adapters/codex/adapter.yaml +35 -0
- package/template/.cowork-flow/adapters/opencode/adapter.yaml +33 -0
- package/template/.cowork-flow/config.yaml +4 -1
- package/template/.cowork-flow/scripts/add_session.py +20 -7
- package/template/.cowork-flow/scripts/common/active_task.py +212 -1
- package/template/.cowork-flow/scripts/common/entry_classifier.py +122 -0
- package/template/.cowork-flow/scripts/common/execution_context.py +10 -17
- package/template/.cowork-flow/scripts/common/readiness.py +249 -0
- package/template/.cowork-flow/scripts/doctor.py +417 -29
- package/template/.cowork-flow/scripts/project_context.py +230 -0
- package/template/.cowork-flow/scripts/run.py +3 -0
- package/template/.cowork-flow/scripts/subagent.py +152 -238
- package/template/.cowork-flow/scripts/task.py +330 -17
- package/template/.cowork-flow/spec/adapter.schema.json +132 -0
- package/template/.cowork-flow/spec/capabilities.md +34 -0
- package/template/.cowork-flow/spec/entry-contract.md +45 -0
- package/template/.cowork-flow/spec/registry.json +54 -0
- package/template/.cowork-flow/spec/subagent-dispatch.md +77 -0
- package/template/.cowork-flow/spec/workflow-state-templates.md +66 -0
- package/template/.cowork-flow/workflow.md +154 -190
- package/template/.opencode/agents/cowork-check.md +46 -0
- package/template/.opencode/agents/cowork-implement.md +45 -0
- package/template/.opencode/agents/cowork-research.md +46 -0
- package/template/.opencode/commands/cowork-check.md +26 -0
- package/template/.opencode/commands/cowork-implement.md +26 -0
- package/template/.opencode/commands/cowork-research.md +26 -0
- package/template/.opencode/plugins/cowork-flow.js +317 -0
- package/template/CLAUDE.md +5 -0
- package/template/.agent/skills/entry-boundary/SKILL.md +0 -81
package/README.md
CHANGED
|
@@ -23,8 +23,19 @@ cowork-flow 是一个用于新项目初始化协作流程的模板仓库。它
|
|
|
23
23
|
├── README.md
|
|
24
24
|
└── template/
|
|
25
25
|
├── AGENTS.md
|
|
26
|
+
├── CLAUDE.md
|
|
26
27
|
├── .codex/
|
|
27
28
|
│ └── agents/
|
|
29
|
+
├── .claude/
|
|
30
|
+
│ ├── settings.json
|
|
31
|
+
│ ├── agents/
|
|
32
|
+
│ ├── skills/
|
|
33
|
+
│ ├── commands/
|
|
34
|
+
│ └── hooks/
|
|
35
|
+
├── .opencode/
|
|
36
|
+
│ ├── agents/
|
|
37
|
+
│ ├── commands/
|
|
38
|
+
│ └── plugins/
|
|
28
39
|
├── .agent/
|
|
29
40
|
│ └── skills/
|
|
30
41
|
└── .cowork-flow/
|
|
@@ -47,10 +58,16 @@ cowork-flow 是一个用于新项目初始化协作流程的模板仓库。它
|
|
|
47
58
|
本地技能入口,覆盖 start、before-dev、brainstorming、writing-plans、check、finish-work、continue、meta、python-design、update-spec 和 break-loop 等协作动作。这里的 skill 应保持通用,不承载某个业务项目的一次性细节。
|
|
48
59
|
|
|
49
60
|
`template/.codex/agents/`
|
|
50
|
-
固定角色 agent 定义,包含 `cowork-research`、`cowork-implement` 和 `cowork-check
|
|
61
|
+
固定角色 agent 定义,包含 `cowork-research`、`cowork-implement` 和 `cowork-check`,以及对 Codex 默认 `worker`、`default`、`explorer` 的项目级防漂移约束。主会话负责计划与收口,并通过 host adapter 派发固定 agent;正式派发必须先创建 runtime context,再把 `cowork_runtime_context_id` 和 `cowork_host_context_key` 传给子代理。
|
|
51
62
|
|
|
52
63
|
`template/.codex/config.toml`、`template/.codex/hooks.json`、`template/.codex/hooks/`
|
|
53
|
-
Codex 项目级配置与每轮上下文注入入口。hook
|
|
64
|
+
Codex 项目级配置与每轮上下文注入入口。hook 会先用共享 entry classifier 判定当前输入,再读取当前 session task、`.cowork-flow/spec/workflow-state-templates.md` 中的 `workflow-state` 片段和 `codex.dispatch_mode`,把流程状态注入到当前轮对话。注入的 `codex-dispatch-mode` 只是主会话调度提示,不代表当前线程身份。
|
|
65
|
+
|
|
66
|
+
`template/CLAUDE.md`、`template/.claude/settings.json`、`template/.claude/agents/`、`template/.claude/skills/`、`template/.claude/commands/`、`template/.claude/hooks/`
|
|
67
|
+
Claude Code 项目级记忆、hook、固定 agent、skills 和 slash command。`CLAUDE.md` 显式导入 `AGENTS.md`。`.claude/settings.json` 注册 `UserPromptSubmit` 和 `SessionStart` hook,注入当前 workflow state 与 contract digest。固定 agent 通过 `.cowork-flow/run subagent init` 生成 runtime context,并在 prompt、env 或 metadata transport 中传递 `cowork_runtime_context_id`;hook 绑定成功后才进入叶子执行,绑定失败则 fail closed,避免被项目 bootstrap 或无任务首屏上下文拉偏。Claude Code 不自动加载 `.agent/skills/`,所以项目技能要放进 `.claude/skills/`。
|
|
68
|
+
|
|
69
|
+
`template/.opencode/agents/`、`template/.opencode/commands/`、`template/.opencode/plugins/`
|
|
70
|
+
OpenCode 项目级固定 agent、slash command 和系统上下文注入插件。插件会读取 `.cowork-flow/spec/registry.json`,注入短 contract digest。
|
|
54
71
|
|
|
55
72
|
`template/.cowork-flow/`
|
|
56
73
|
工作流目录,包含流程说明、任务状态、开发者工作区、项目规范、行为变更规格、实现计划和辅助脚本。`.cowork-flow/config.yaml` 只承载真实接线的 session/journal, Codex hint, and task lifecycle hook settings。
|
|
@@ -69,20 +86,20 @@ Codex 项目级配置与每轮上下文注入入口。hook 会读取当前 sessi
|
|
|
69
86
|
使用 CLI 把模板内容安装到目标项目根目录:
|
|
70
87
|
|
|
71
88
|
```bash
|
|
72
|
-
npx cowork-flow init ./my-project
|
|
89
|
+
npx cowork-flow init ./my-project --platform codex
|
|
73
90
|
```
|
|
74
91
|
|
|
75
|
-
`init`
|
|
92
|
+
`init` 会显式要求平台和开发者身份。平台可选 `codex`、`opencode`、`claude-code`、`all`,其中 `both` 仍作为兼容别名表示 `codex,opencode`。交互式终端会提示输入;脚本或 CI 请传入:
|
|
76
93
|
|
|
77
94
|
```bash
|
|
78
|
-
npx cowork-flow init ./my-project --developer <your-name>
|
|
95
|
+
npx cowork-flow init ./my-project --platform codex --developer <your-name>
|
|
79
96
|
```
|
|
80
97
|
|
|
81
98
|
也可以先全局安装:
|
|
82
99
|
|
|
83
100
|
```bash
|
|
84
101
|
npm install -g cowork-flow
|
|
85
|
-
cowork-flow init ./my-project --developer <your-name>
|
|
102
|
+
cowork-flow init ./my-project --platform codex --developer <your-name>
|
|
86
103
|
```
|
|
87
104
|
|
|
88
105
|
初始化后优先完成这些配置:
|
|
@@ -104,27 +121,27 @@ npx cowork-flow --help
|
|
|
104
121
|
初始化到新项目:
|
|
105
122
|
|
|
106
123
|
```bash
|
|
107
|
-
cowork-flow init ./my-project
|
|
124
|
+
cowork-flow init ./my-project --platform codex
|
|
108
125
|
```
|
|
109
126
|
|
|
110
|
-
`init`
|
|
127
|
+
`init` 会直接复制模板中的通用 `.cowork-flow/`,并按 `--platform` 只复制对应 host 资产:`codex` 复制 `.codex/`、`.agent/skills/` 与 `.cowork-flow/adapters/codex/`,`opencode` 复制 `.opencode/`、`.agent/skills/` 与 `.cowork-flow/adapters/opencode/`,`claude-code` 复制 `CLAUDE.md`、`.claude/`(含 `.claude/settings.json`、`.claude/hooks/`、`.claude/skills/`)与 `.cowork-flow/adapters/claude-code/`,不复制 `.agent/skills/`。`all` 或 `--platform codex,opencode,claude-code` 同时复制三者和 `.agent/skills/`。`both` 保持兼容,只复制 Codex 和 OpenCode。它还会初始化 `.cowork-flow/.developer` 与 `.cowork-flow/workspace/<developer>/`。交互式终端会先显示 checkbox 多选界面选择平台,再提示开发者名称;非交互式环境必须传入 `--platform <codex|opencode|claude-code|all>` 与 `--developer <name>`。`init` 不会复制额外技能包。
|
|
111
128
|
|
|
112
129
|
初始化到当前项目:
|
|
113
130
|
|
|
114
131
|
```bash
|
|
115
|
-
cowork-flow init .
|
|
132
|
+
cowork-flow init . --platform opencode
|
|
116
133
|
```
|
|
117
134
|
|
|
118
135
|
默认不会覆盖已有文件。需要预览时使用:
|
|
119
136
|
|
|
120
137
|
```bash
|
|
121
|
-
cowork-flow init ./my-project --dry-run
|
|
138
|
+
cowork-flow init ./my-project --platform all --dry-run
|
|
122
139
|
```
|
|
123
140
|
|
|
124
141
|
需要明确覆盖已有文件时使用:
|
|
125
142
|
|
|
126
143
|
```bash
|
|
127
|
-
cowork-flow init ./my-project --force --developer <your-name>
|
|
144
|
+
cowork-flow init ./my-project --platform codex --force --developer <your-name>
|
|
128
145
|
```
|
|
129
146
|
|
|
130
147
|
`--force` 不会静默覆盖既有 `.cowork-flow/.developer`;如需重设身份,请先显式处理旧身份文件。
|
|
@@ -142,7 +159,7 @@ cowork-flow sync .
|
|
|
142
159
|
cowork-flow sync . --dry-run
|
|
143
160
|
```
|
|
144
161
|
|
|
145
|
-
`sync`
|
|
162
|
+
`sync` 会自动识别目标项目已安装的 host 目录:只有 `.codex/` 时只同步 Codex 资产,只有 `.opencode/` 时只同步 OpenCode 资产,两者都有时同步两者;对应 `.cowork-flow/adapters/<host>/` 也按识别出的平台同步。检测到 Codex 或 OpenCode 时会刷新 `.agent/skills/`;Claude Code-only 项目只刷新 `.claude/settings.json`、`.claude/hooks/`、`.claude/skills/`,不会创建 `.agent/skills/`。通用部分默认刷新 `.cowork-flow/scripts/`、`.cowork-flow/spec/workflow-state-templates.md`,以及 `AGENTS.md` 和 `CLAUDE.md` 中的 `<!-- COWORK-FLOW:START --> ... <!-- COWORK-FLOW:END -->` 托管块,保留托管块之外的项目自定义内容。`.cowork-flow/config.yaml`、`.cowork-flow/workflow.md`、除 `workflow-state-templates.md` 外的 `.cowork-flow/spec/`、任务、计划、变更和 workspace 记录默认受保护。只有明确传入 `--force` 时才整文件覆盖保护文件。
|
|
146
163
|
|
|
147
164
|
## 常用入口
|
|
148
165
|
|
|
@@ -169,7 +186,9 @@ cowork-flow sync . --dry-run
|
|
|
169
186
|
|
|
170
187
|
```bash
|
|
171
188
|
./.cowork-flow/run get-context
|
|
189
|
+
./.cowork-flow/run project-context refresh
|
|
172
190
|
./.cowork-flow/run task list
|
|
191
|
+
./.cowork-flow/run task next
|
|
173
192
|
```
|
|
174
193
|
|
|
175
194
|
创建并验证行为变更:
|
|
@@ -183,24 +202,50 @@ cowork-flow sync . --dry-run
|
|
|
183
202
|
|
|
184
203
|
```bash
|
|
185
204
|
./.cowork-flow/run task create "<title>" --slug <task-name>
|
|
205
|
+
./.cowork-flow/run task next
|
|
186
206
|
./.cowork-flow/run task start <task-dir>
|
|
187
207
|
```
|
|
188
208
|
|
|
209
|
+
`task next` is read-only. It reports the active task, status, blockers, and the
|
|
210
|
+
next safe command before the main session starts work, dispatches fixed agents,
|
|
211
|
+
checks, archives, or records the session.
|
|
212
|
+
|
|
213
|
+
`project-context refresh` 会创建或刷新 `.cowork-flow/project-context.md`。生成块由
|
|
214
|
+
本地项目文件确定,`Manual Notes` 区保留人工补充;该文件是项目索引,不替代
|
|
215
|
+
`AGENTS.md`、`.cowork-flow/workflow.md` 或 `.cowork-flow/spec/`。
|
|
216
|
+
|
|
217
|
+
L2 任务的 readiness gate 会在 `task start` 前阻塞缺失的 proposal/spec/design、
|
|
218
|
+
计划、任务链接、边界、假设、验收标准或 verification commands;同一 blocker
|
|
219
|
+
会出现在 `task next` 输出中。
|
|
220
|
+
|
|
221
|
+
任务状态由阶段命令推进,`task next` 只读取状态:
|
|
222
|
+
|
|
223
|
+
| 阶段 | 命令 | `task.json.status` |
|
|
224
|
+
| --- | --- | --- |
|
|
225
|
+
| 创建/计划 | `task create` | `planning` |
|
|
226
|
+
| 开始执行 | `task start <task-dir>` | `in_progress` |
|
|
227
|
+
| 进入检查 | `task review [task-dir]` | `review` |
|
|
228
|
+
| 检查完成 | `task complete [task-dir]` | `completed` |
|
|
229
|
+
| 清会话指针 | `task finish` | 不改变状态 |
|
|
230
|
+
|
|
189
231
|
执行 plan 时使用固定 cowork agents:
|
|
190
232
|
|
|
191
233
|
主会话负责创建/启动任务、维护计划与收口验证;可并行的实现或检查工作通过 Codex subagent 交给固定角色 agent。
|
|
192
234
|
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
235
|
+
```text
|
|
236
|
+
./.cowork-flow/run subagent init --role implement --agent-type cowork-implement --execution-task-dir <task-dir> --title "<title>"
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
将命令返回的 prompt transport 放入子代理 prompt:
|
|
240
|
+
|
|
241
|
+
```text
|
|
242
|
+
cowork_runtime_context_id: <runtime_context_id>
|
|
243
|
+
cowork_host_context_key: <host_context_key>
|
|
199
244
|
```
|
|
200
245
|
|
|
201
|
-
|
|
246
|
+
子代理首步执行 `./.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>`;主会话验收前必须确认 runtime context 中 `status=bound` 且 `bound_context_key` 匹配。默认角色为 `cowork-research`、`cowork-implement`、`cowork-check`,任务上下文来自已绑定 runtime context,而不是 prompt 形状。
|
|
202
247
|
|
|
203
|
-
Codex hook 启用后,每轮会自动注入 `<workflow-state
|
|
248
|
+
Codex hook 启用后,每轮会自动注入 `<workflow-state>`,其中包含入口分类、当前任务、状态来源和下一步流程提示。状态文本来自 `.cowork-flow/spec/workflow-state-templates.md`;`workflow.md` 只描述流程,不内联状态片段。hook 不替代主会话验收;它只负责把 task 状态和 workflow gate 放进上下文。
|
|
204
249
|
|
|
205
250
|
记录 session:
|
|
206
251
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stdin as input, stdout as output } from 'node:process';
|
|
2
|
+
import { emitKeypressEvents } from 'node:readline';
|
|
2
3
|
import { createInterface } from 'node:readline/promises';
|
|
3
4
|
|
|
4
5
|
import { runInit } from './commands/init.js';
|
|
@@ -9,7 +10,7 @@ import { readPackageInfo } from './lib/package-info.js';
|
|
|
9
10
|
const HELP = `cowork-flow
|
|
10
11
|
|
|
11
12
|
Usage:
|
|
12
|
-
cowork-flow init [target] [--developer <name>] [--dry-run] [--force]
|
|
13
|
+
cowork-flow init [target] --platform <codex|opencode|claude-code|all> [--developer <name>] [--dry-run] [--force]
|
|
13
14
|
cowork-flow update
|
|
14
15
|
cowork-flow sync [target] [--dry-run] [--force]
|
|
15
16
|
cowork-flow --version
|
|
@@ -39,9 +40,115 @@ async function defaultPrompt(message) {
|
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
async function defaultSelectPlatforms({ message, choices, defaultSelected = [] }) {
|
|
44
|
+
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== 'function') {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const selected = new Set(defaultSelected);
|
|
49
|
+
let activeIndex = 0;
|
|
50
|
+
let search = '';
|
|
51
|
+
let renderedLines = 0;
|
|
52
|
+
|
|
53
|
+
const filteredChoices = () => {
|
|
54
|
+
const term = search.toLowerCase();
|
|
55
|
+
return choices.filter((choice) => choice.label.toLowerCase().includes(term));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const render = () => {
|
|
59
|
+
const visibleChoices = filteredChoices();
|
|
60
|
+
if (activeIndex >= visibleChoices.length) {
|
|
61
|
+
activeIndex = Math.max(0, visibleChoices.length - 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (renderedLines > 0) {
|
|
65
|
+
output.write(`\x1b[${renderedLines}A\x1b[0J`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const selectedLabels = choices
|
|
69
|
+
.filter((choice) => selected.has(choice.value))
|
|
70
|
+
.map((choice) => choice.label);
|
|
71
|
+
const lines = [
|
|
72
|
+
`? ${message} (${choices.length} available)`,
|
|
73
|
+
`Selected: ${selectedLabels.length > 0 ? selectedLabels.join(', ') : '(none)'}`,
|
|
74
|
+
`Search: ${search || '[type to filter]'}`,
|
|
75
|
+
'↑↓ navigate • Space toggle • Backspace remove • Enter confirm'
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
if (visibleChoices.length === 0) {
|
|
79
|
+
lines.push(' (no matches)');
|
|
80
|
+
} else {
|
|
81
|
+
for (let index = 0; index < visibleChoices.length; index += 1) {
|
|
82
|
+
const choice = visibleChoices[index];
|
|
83
|
+
const cursor = index === activeIndex ? '›' : ' ';
|
|
84
|
+
const marker = selected.has(choice.value) ? '◉' : '○';
|
|
85
|
+
const suffix = selected.has(choice.value) ? ' (selected)' : '';
|
|
86
|
+
lines.push(`${cursor} ${marker} ${choice.label}${suffix}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
output.write(`${lines.join('\n')}\n`);
|
|
91
|
+
renderedLines = lines.length;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return await new Promise((resolve, reject) => {
|
|
95
|
+
emitKeypressEvents(input);
|
|
96
|
+
input.setRawMode(true);
|
|
97
|
+
input.resume();
|
|
98
|
+
|
|
99
|
+
const cleanup = () => {
|
|
100
|
+
input.off('keypress', onKeypress);
|
|
101
|
+
input.setRawMode(false);
|
|
102
|
+
output.write('\n');
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const onKeypress = (text, key = {}) => {
|
|
106
|
+
const visibleChoices = filteredChoices();
|
|
107
|
+
if (key.ctrl && key.name === 'c') {
|
|
108
|
+
cleanup();
|
|
109
|
+
reject(new Error('Platform selection cancelled'));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
113
|
+
if (selected.size === 0 && visibleChoices[activeIndex]) {
|
|
114
|
+
selected.add(visibleChoices[activeIndex].value);
|
|
115
|
+
}
|
|
116
|
+
cleanup();
|
|
117
|
+
resolve([...selected]);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (key.name === 'up') {
|
|
121
|
+
activeIndex = Math.max(0, activeIndex - 1);
|
|
122
|
+
} else if (key.name === 'down') {
|
|
123
|
+
activeIndex = Math.min(Math.max(visibleChoices.length - 1, 0), activeIndex + 1);
|
|
124
|
+
} else if (key.name === 'space' && visibleChoices[activeIndex]) {
|
|
125
|
+
const value = visibleChoices[activeIndex].value;
|
|
126
|
+
if (selected.has(value)) {
|
|
127
|
+
selected.delete(value);
|
|
128
|
+
} else {
|
|
129
|
+
selected.add(value);
|
|
130
|
+
}
|
|
131
|
+
} else if (key.name === 'backspace') {
|
|
132
|
+
search = search.slice(0, -1);
|
|
133
|
+
activeIndex = 0;
|
|
134
|
+
} else if (text && text.trim() && text.length === 1) {
|
|
135
|
+
search += text;
|
|
136
|
+
activeIndex = 0;
|
|
137
|
+
}
|
|
138
|
+
render();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
input.on('keypress', onKeypress);
|
|
142
|
+
render();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
42
146
|
export async function main(argv = process.argv.slice(2), options = {}) {
|
|
43
147
|
const io = options.io ?? defaultIo();
|
|
44
148
|
const prompt = Object.hasOwn(options, 'prompt') ? options.prompt : defaultPrompt;
|
|
149
|
+
const selectPlatforms = Object.hasOwn(options, 'selectPlatforms')
|
|
150
|
+
? options.selectPlatforms
|
|
151
|
+
: defaultSelectPlatforms;
|
|
45
152
|
const [command, ...args] = argv;
|
|
46
153
|
|
|
47
154
|
try {
|
|
@@ -57,7 +164,7 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
|
|
57
164
|
}
|
|
58
165
|
|
|
59
166
|
if (command === 'init') {
|
|
60
|
-
return await runInit(args, { io, prompt });
|
|
167
|
+
return await runInit(args, { io, prompt, selectPlatforms });
|
|
61
168
|
}
|
|
62
169
|
|
|
63
170
|
if (command === 'sync') {
|
package/src/commands/init.js
CHANGED
|
@@ -7,15 +7,37 @@ import {
|
|
|
7
7
|
summarizePlan
|
|
8
8
|
} from '../lib/copy-template.js';
|
|
9
9
|
import { readPackageInfo } from '../lib/package-info.js';
|
|
10
|
+
import {
|
|
11
|
+
SUPPORTED_PLATFORMS,
|
|
12
|
+
formatPlatformList,
|
|
13
|
+
parsePlatformSelection
|
|
14
|
+
} from '../lib/platforms.js';
|
|
10
15
|
|
|
11
16
|
function parseInitArgs(args) {
|
|
12
|
-
const options = {
|
|
17
|
+
const options = {
|
|
18
|
+
developer: null,
|
|
19
|
+
dryRun: false,
|
|
20
|
+
force: false,
|
|
21
|
+
platforms: [],
|
|
22
|
+
target: process.cwd()
|
|
23
|
+
};
|
|
13
24
|
for (let index = 0; index < args.length; index += 1) {
|
|
14
25
|
const arg = args[index];
|
|
15
26
|
if (arg === '--dry-run') {
|
|
16
27
|
options.dryRun = true;
|
|
17
28
|
} else if (arg === '--force') {
|
|
18
29
|
options.force = true;
|
|
30
|
+
} else if (arg === '--platform' || arg === '--platforms') {
|
|
31
|
+
const value = args[index + 1];
|
|
32
|
+
if (!value || value.startsWith('--')) {
|
|
33
|
+
throw new Error('Missing value for --platform');
|
|
34
|
+
}
|
|
35
|
+
options.platforms.push(value);
|
|
36
|
+
index += 1;
|
|
37
|
+
} else if (arg.startsWith('--platform=')) {
|
|
38
|
+
options.platforms.push(arg.slice('--platform='.length));
|
|
39
|
+
} else if (arg.startsWith('--platforms=')) {
|
|
40
|
+
options.platforms.push(arg.slice('--platforms='.length));
|
|
19
41
|
} else if (arg === '--developer') {
|
|
20
42
|
const value = args[index + 1];
|
|
21
43
|
if (!value || value.startsWith('--')) {
|
|
@@ -85,6 +107,38 @@ async function resolveDeveloperName(options, prompt) {
|
|
|
85
107
|
return { existing: false, name: normalizeDeveloperName(name) };
|
|
86
108
|
}
|
|
87
109
|
|
|
110
|
+
function platformLabel(platform) {
|
|
111
|
+
if (platform === 'codex') {
|
|
112
|
+
return 'Codex';
|
|
113
|
+
}
|
|
114
|
+
if (platform === 'opencode') {
|
|
115
|
+
return 'OpenCode';
|
|
116
|
+
}
|
|
117
|
+
if (platform === 'claude-code') {
|
|
118
|
+
return 'Claude Code';
|
|
119
|
+
}
|
|
120
|
+
return platform;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function resolvePlatforms(options, selectPlatforms) {
|
|
124
|
+
if (options.platforms.length > 0) {
|
|
125
|
+
return parsePlatformSelection(options.platforms);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let selected = null;
|
|
129
|
+
if (typeof selectPlatforms === 'function') {
|
|
130
|
+
selected = await selectPlatforms({
|
|
131
|
+
message: 'Select platforms to set up',
|
|
132
|
+
choices: SUPPORTED_PLATFORMS.map((platform) => ({
|
|
133
|
+
label: platformLabel(platform),
|
|
134
|
+
value: platform
|
|
135
|
+
})),
|
|
136
|
+
defaultSelected: ['codex']
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return parsePlatformSelection(selected);
|
|
140
|
+
}
|
|
141
|
+
|
|
88
142
|
function todayIsoDate() {
|
|
89
143
|
return new Date().toISOString().slice(0, 10);
|
|
90
144
|
}
|
|
@@ -170,21 +224,24 @@ async function initializeDeveloper(target, developer) {
|
|
|
170
224
|
}
|
|
171
225
|
}
|
|
172
226
|
|
|
173
|
-
export async function runInit(args, { io, prompt }) {
|
|
174
|
-
return runInitWithOptions(args, { io, prompt });
|
|
227
|
+
export async function runInit(args, { io, prompt, selectPlatforms }) {
|
|
228
|
+
return runInitWithOptions(args, { io, prompt, selectPlatforms });
|
|
175
229
|
}
|
|
176
230
|
|
|
177
|
-
export async function runInitWithOptions(args, { io, prompt }) {
|
|
231
|
+
export async function runInitWithOptions(args, { io, prompt, selectPlatforms }) {
|
|
178
232
|
const options = parseInitArgs(args);
|
|
233
|
+
const platforms = await resolvePlatforms(options, selectPlatforms);
|
|
179
234
|
const developer = await resolveDeveloperName(options, prompt);
|
|
180
235
|
const packageInfo = await readPackageInfo();
|
|
181
236
|
const plan = await buildInitPlan(options.target, {
|
|
182
237
|
force: options.force,
|
|
238
|
+
platforms,
|
|
183
239
|
version: packageInfo.version
|
|
184
240
|
});
|
|
185
241
|
|
|
186
242
|
await applyPlan(plan, { dryRun: options.dryRun });
|
|
187
243
|
io.writeOut(summarizePlan(plan, options.dryRun));
|
|
244
|
+
io.writeOut(`Platforms: ${formatPlatformList(platforms)}\n`);
|
|
188
245
|
|
|
189
246
|
if (options.dryRun) {
|
|
190
247
|
if (developer.existing) {
|
package/src/commands/sync.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
applyPlan,
|
|
5
|
+
buildSyncPlan,
|
|
6
|
+
detectInstalledPlatforms,
|
|
7
|
+
summarizePlan
|
|
8
|
+
} from '../lib/copy-template.js';
|
|
4
9
|
import { readPackageInfo } from '../lib/package-info.js';
|
|
10
|
+
import { formatPlatformList } from '../lib/platforms.js';
|
|
5
11
|
|
|
6
12
|
function parseSyncArgs(args) {
|
|
7
13
|
const options = { dryRun: false, force: false, target: process.cwd() };
|
|
@@ -22,12 +28,15 @@ function parseSyncArgs(args) {
|
|
|
22
28
|
export async function runSync(args, { io }) {
|
|
23
29
|
const options = parseSyncArgs(args);
|
|
24
30
|
const packageInfo = await readPackageInfo();
|
|
31
|
+
const platforms = await detectInstalledPlatforms(options.target);
|
|
25
32
|
const plan = await buildSyncPlan(options.target, {
|
|
26
33
|
force: options.force,
|
|
34
|
+
platforms,
|
|
27
35
|
version: packageInfo.version
|
|
28
36
|
});
|
|
29
37
|
|
|
30
38
|
await applyPlan(plan, { dryRun: options.dryRun });
|
|
31
39
|
io.writeOut(summarizePlan(plan, options.dryRun));
|
|
40
|
+
io.writeOut(`Platforms: ${platforms.length > 0 ? formatPlatformList(platforms) : 'none'}\n`);
|
|
32
41
|
return 0;
|
|
33
42
|
}
|
package/src/lib/copy-template.js
CHANGED
|
@@ -3,6 +3,7 @@ import { access, chmod, copyFile, mkdir, readFile, readdir, stat, writeFile } fr
|
|
|
3
3
|
import { dirname, join, relative } from 'node:path';
|
|
4
4
|
|
|
5
5
|
import { templateRoot } from './paths.js';
|
|
6
|
+
import { shouldIncludeForPlatforms } from './platforms.js';
|
|
6
7
|
|
|
7
8
|
async function pathExists(path) {
|
|
8
9
|
try {
|
|
@@ -39,8 +40,13 @@ function toTemplatePath(relativePath) {
|
|
|
39
40
|
export async function buildInitPlan(targetDir, options = {}) {
|
|
40
41
|
const files = await listFiles(templateRoot);
|
|
41
42
|
const actions = [];
|
|
43
|
+
const platforms = options.platforms ?? [];
|
|
42
44
|
|
|
43
45
|
for (const file of files) {
|
|
46
|
+
if (!shouldIncludeForPlatforms(file, platforms)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
if (toTemplatePath(file) === '.cowork-flow/.version') {
|
|
45
51
|
continue;
|
|
46
52
|
}
|
|
@@ -79,6 +85,8 @@ const PROTECTED_SYNC_PREFIXES = [
|
|
|
79
85
|
|
|
80
86
|
const SAFE_SYNC_PREFIXES = [
|
|
81
87
|
'.codex/',
|
|
88
|
+
'.opencode/',
|
|
89
|
+
'.claude/',
|
|
82
90
|
'.agent/skills/',
|
|
83
91
|
'.cowork-flow/'
|
|
84
92
|
];
|
|
@@ -87,7 +95,8 @@ const SAFE_SYNC_FILES = new Set([
|
|
|
87
95
|
'.cowork-flow/.gitignore',
|
|
88
96
|
'.cowork-flow/.version',
|
|
89
97
|
'.cowork-flow/run',
|
|
90
|
-
'.cowork-flow/run.cmd'
|
|
98
|
+
'.cowork-flow/run.cmd',
|
|
99
|
+
'.cowork-flow/spec/workflow-state-templates.md'
|
|
91
100
|
]);
|
|
92
101
|
|
|
93
102
|
const COWORK_FLOW_START = '<!-- COWORK-FLOW:START -->';
|
|
@@ -95,6 +104,9 @@ const COWORK_FLOW_END = '<!-- COWORK-FLOW:END -->';
|
|
|
95
104
|
|
|
96
105
|
function isProtectedSyncFile(relativePath) {
|
|
97
106
|
const templatePath = toTemplatePath(relativePath);
|
|
107
|
+
if (SAFE_SYNC_FILES.has(templatePath)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
98
110
|
return PROTECTED_SYNC_FILES.has(templatePath)
|
|
99
111
|
|| PROTECTED_SYNC_PREFIXES.some((prefix) => templatePath.startsWith(prefix));
|
|
100
112
|
}
|
|
@@ -125,13 +137,13 @@ function replaceManagedBlock(targetContent, templateContent) {
|
|
|
125
137
|
].join('');
|
|
126
138
|
}
|
|
127
139
|
|
|
128
|
-
async function
|
|
140
|
+
async function buildManagedBlockSyncAction({ source, destination, exists, options, relativePath }) {
|
|
129
141
|
if (!exists || options.force) {
|
|
130
142
|
return {
|
|
131
143
|
action: exists ? 'update' : 'create',
|
|
132
144
|
source,
|
|
133
145
|
destination,
|
|
134
|
-
relativePath
|
|
146
|
+
relativePath
|
|
135
147
|
};
|
|
136
148
|
}
|
|
137
149
|
|
|
@@ -142,10 +154,10 @@ async function buildAgentsSyncAction({ source, destination, exists, options }) {
|
|
|
142
154
|
const content = replaceManagedBlock(targetContent, templateContent);
|
|
143
155
|
|
|
144
156
|
if (content === null) {
|
|
145
|
-
return { action: 'protected', source, destination, relativePath
|
|
157
|
+
return { action: 'protected', source, destination, relativePath };
|
|
146
158
|
}
|
|
147
159
|
|
|
148
|
-
return { action: 'update', source, destination, relativePath
|
|
160
|
+
return { action: 'update', source, destination, relativePath, content };
|
|
149
161
|
}
|
|
150
162
|
|
|
151
163
|
export async function buildSyncPlan(targetDir, options = {}) {
|
|
@@ -155,8 +167,13 @@ export async function buildSyncPlan(targetDir, options = {}) {
|
|
|
155
167
|
|
|
156
168
|
const files = await listFiles(templateRoot);
|
|
157
169
|
const actions = [];
|
|
170
|
+
const platforms = options.platforms ?? await detectInstalledPlatforms(targetDir);
|
|
158
171
|
|
|
159
172
|
for (const file of files) {
|
|
173
|
+
if (!shouldIncludeForPlatforms(file, platforms)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
160
177
|
if (toTemplatePath(file) === '.cowork-flow/.version') {
|
|
161
178
|
continue;
|
|
162
179
|
}
|
|
@@ -164,8 +181,8 @@ export async function buildSyncPlan(targetDir, options = {}) {
|
|
|
164
181
|
const source = join(templateRoot, file);
|
|
165
182
|
const destination = join(targetDir, file);
|
|
166
183
|
const exists = await pathExists(destination);
|
|
167
|
-
if (file === 'AGENTS.md') {
|
|
168
|
-
actions.push(await
|
|
184
|
+
if (file === 'AGENTS.md' || file === 'CLAUDE.md') {
|
|
185
|
+
actions.push(await buildManagedBlockSyncAction({ source, destination, exists, options, relativePath: file }));
|
|
169
186
|
continue;
|
|
170
187
|
}
|
|
171
188
|
|
|
@@ -194,6 +211,20 @@ export async function buildSyncPlan(targetDir, options = {}) {
|
|
|
194
211
|
return actions;
|
|
195
212
|
}
|
|
196
213
|
|
|
214
|
+
export async function detectInstalledPlatforms(targetDir) {
|
|
215
|
+
const platforms = [];
|
|
216
|
+
if (await pathExists(join(targetDir, '.codex'))) {
|
|
217
|
+
platforms.push('codex');
|
|
218
|
+
}
|
|
219
|
+
if (await pathExists(join(targetDir, '.opencode'))) {
|
|
220
|
+
platforms.push('opencode');
|
|
221
|
+
}
|
|
222
|
+
if (await pathExists(join(targetDir, '.claude')) || await pathExists(join(targetDir, 'CLAUDE.md'))) {
|
|
223
|
+
platforms.push('claude-code');
|
|
224
|
+
}
|
|
225
|
+
return platforms;
|
|
226
|
+
}
|
|
227
|
+
|
|
197
228
|
export function summarizePlan(actions, dryRun = false) {
|
|
198
229
|
const counts = { create: 0, update: 0, skip: 0, protected: 0 };
|
|
199
230
|
for (const action of actions) {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export const SUPPORTED_PLATFORMS = ['codex', 'opencode', 'claude-code'];
|
|
2
|
+
|
|
3
|
+
const LEGACY_BOTH_PLATFORMS = ['codex', 'opencode'];
|
|
4
|
+
|
|
5
|
+
const PLATFORM_ALIASES = new Map([
|
|
6
|
+
['all', SUPPORTED_PLATFORMS],
|
|
7
|
+
['both', LEGACY_BOTH_PLATFORMS],
|
|
8
|
+
['codex', ['codex']],
|
|
9
|
+
['opencode', ['opencode']],
|
|
10
|
+
['claude', ['claude-code']],
|
|
11
|
+
['claude-code', ['claude-code']],
|
|
12
|
+
['claudecode', ['claude-code']]
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export function supportedPlatformMessage() {
|
|
16
|
+
return SUPPORTED_PLATFORMS.join(', ');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parsePlatformSelection(values) {
|
|
20
|
+
const rawValues = Array.isArray(values) ? values : [values];
|
|
21
|
+
const selected = new Set();
|
|
22
|
+
|
|
23
|
+
for (const rawValue of rawValues) {
|
|
24
|
+
const tokens = String(rawValue ?? '')
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.split(/[\s,;|+/]+/)
|
|
27
|
+
.map((token) => token.trim())
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
|
|
30
|
+
for (const token of tokens) {
|
|
31
|
+
const platforms = PLATFORM_ALIASES.get(token);
|
|
32
|
+
if (!platforms) {
|
|
33
|
+
throw new Error(`Unsupported platform: ${token}. Supported platforms: ${supportedPlatformMessage()}`);
|
|
34
|
+
}
|
|
35
|
+
for (const platform of platforms) {
|
|
36
|
+
selected.add(platform);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (selected.size === 0) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'Platform selection required. Run: cowork-flow init <target> --platform codex|opencode|claude-code'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return SUPPORTED_PLATFORMS.filter((platform) => selected.has(platform));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatPlatformList(platforms) {
|
|
51
|
+
return platforms.join(', ');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function shouldIncludeForPlatforms(relativePath, platforms) {
|
|
55
|
+
const normalized = relativePath.replaceAll('\\', '/');
|
|
56
|
+
if (normalized.startsWith('.agent/skills/')) {
|
|
57
|
+
return platforms.includes('codex') || platforms.includes('opencode');
|
|
58
|
+
}
|
|
59
|
+
if (normalized.startsWith('.codex/') || normalized.startsWith('.cowork-flow/adapters/codex/')) {
|
|
60
|
+
return platforms.includes('codex');
|
|
61
|
+
}
|
|
62
|
+
if (normalized.startsWith('.opencode/') || normalized.startsWith('.cowork-flow/adapters/opencode/')) {
|
|
63
|
+
return platforms.includes('opencode');
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
normalized === 'CLAUDE.md'
|
|
67
|
+
|| normalized.startsWith('.claude/')
|
|
68
|
+
|| normalized.startsWith('.cowork-flow/adapters/claude-code/')
|
|
69
|
+
) {
|
|
70
|
+
return platforms.includes('claude-code');
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|