cowork-flow 1.6.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +213 -213
  2. package/CONTRIBUTING.md +137 -0
  3. package/LICENSE +1 -1
  4. package/README.md +108 -369
  5. package/docs/architecture.md +122 -0
  6. package/docs/hosts.md +290 -0
  7. package/docs/index.md +32 -0
  8. package/docs/release.md +104 -0
  9. package/package.json +37 -7
  10. package/presets/claude-code/.claude-plugin/plugin.json +28 -0
  11. package/presets/claude-code/skills/cowork-flow-bootstrap/SKILL.md +35 -0
  12. package/presets/codex/.codex-plugin/plugin.json +37 -0
  13. package/presets/codex/assets/logo.svg +4 -0
  14. package/presets/codex/skills/cowork-flow-bootstrap/SKILL.md +35 -0
  15. package/presets/dsh/agent.cordis.yml +35 -12
  16. package/presets/dsh/plugins/workflow-state.js +88 -72
  17. package/presets/dsh/preset.yml +1 -1
  18. package/presets/kimi-code/.kimi-plugin/plugin.json +35 -0
  19. package/presets/kimi-code/hooks/cowork-flow-inject.mjs +14 -2
  20. package/presets/kimi-code/skills/cowork-flow-bootstrap/SKILL.md +35 -0
  21. package/presets/opencode/cowork-flow/plugin-core.js +1077 -0
  22. package/presets/opencode/cowork-flow/skills/cowork-flow-bootstrap/SKILL.md +35 -0
  23. package/presets/opencode/plugins/cowork-flow.js +44 -0
  24. package/presets/plugin-meta.json +52 -0
  25. package/presets/qoder/.qoder-plugin/plugin.json +29 -0
  26. package/presets/qoder/agents/cowork-check.md +29 -0
  27. package/presets/qoder/agents/cowork-implement.md +30 -0
  28. package/presets/qoder/agents/cowork-research.md +23 -0
  29. package/presets/qoder/hooks/hooks.json +50 -0
  30. package/presets/qoder/hooks/inject-context.mjs +150 -0
  31. package/presets/qoder/skills/cowork-flow-bootstrap/SKILL.md +35 -0
  32. package/{template/.zcode → presets/zcode}/.zcode-plugin/plugin.json +4 -2
  33. package/{template/.zcode → presets/zcode}/agents/cowork-check.md +4 -4
  34. package/{template/.zcode → presets/zcode}/agents/cowork-implement.md +4 -4
  35. package/{template/.zcode → presets/zcode}/agents/cowork-research.md +1 -1
  36. package/{template/.zcode → presets/zcode}/hooks/inject-context.js +36 -19
  37. package/presets/zcode/skills/cowork-flow-bootstrap/SKILL.md +35 -0
  38. package/src/cli.js +130 -130
  39. package/src/commands/host.js +182 -0
  40. package/src/commands/init.js +28 -43
  41. package/src/commands/install-claude-code-plugin.js +129 -0
  42. package/src/commands/install-codex-plugin.js +312 -0
  43. package/src/commands/install-dsh-hook.js +36 -43
  44. package/src/commands/install-dsh-preset.js +228 -49
  45. package/src/commands/install-kimi-code-plugin.js +142 -0
  46. package/src/commands/install-kimi-hook.js +142 -122
  47. package/src/commands/install-opencode-plugin.js +158 -0
  48. package/src/commands/install-qoder-plugin.js +211 -0
  49. package/src/commands/install-zcode-plugin.js +131 -82
  50. package/src/commands/mcp-state.js +7 -6
  51. package/src/commands/registry.js +345 -0
  52. package/src/commands/source-refresh.js +10 -11
  53. package/src/commands/sync.js +10 -13
  54. package/src/commands/update.js +5 -10
  55. package/src/lib/cli-flags.js +102 -0
  56. package/src/lib/copy-template.js +89 -75
  57. package/src/lib/host-assets.js +192 -57
  58. package/src/lib/install-support.js +50 -0
  59. package/src/lib/platforms.js +0 -15
  60. package/src/lib/plugin-metadata.js +187 -0
  61. package/src/lib/plugin-payload.js +70 -0
  62. package/template/.cowork-flow/.version +1 -1
  63. package/template/.cowork-flow/adapters/claude-code/adapter.yaml +0 -1
  64. package/template/.cowork-flow/adapters/dsh/adapter.yaml +0 -1
  65. package/template/.cowork-flow/adapters/kimi-code/adapter.yaml +0 -1
  66. package/template/.cowork-flow/adapters/qoder/adapter.yaml +32 -0
  67. package/template/.cowork-flow/adapters/zcode/adapter.yaml +2 -2
  68. package/template/.cowork-flow/config.yaml +6 -4
  69. package/template/.cowork-flow/scripts/adapters/cli/batch_mode.py +10 -12
  70. package/template/.cowork-flow/scripts/adapters/cli/spec_check.py +67 -35
  71. package/template/.cowork-flow/scripts/adapters/cli/subagent.py +84 -38
  72. package/template/.cowork-flow/scripts/adapters/cli/task.py +42 -0
  73. package/template/.cowork-flow/scripts/adapters/cli/task_create_command.py +1 -9
  74. package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +6 -0
  75. package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +13 -10
  76. package/template/.cowork-flow/scripts/adapters/cli/task_next_runner.py +2 -1
  77. package/template/.cowork-flow/scripts/adapters/git/git_context.py +14 -82
  78. package/template/.cowork-flow/scripts/adapters/host/claude_code_policy.py +2 -3
  79. package/template/.cowork-flow/scripts/adapters/host/codex_policy.py +2 -5
  80. package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +122 -10
  81. package/template/.cowork-flow/scripts/adapters/host/inject.py +49 -19
  82. package/template/.cowork-flow/scripts/adapters/host/kimi_code_policy.py +1 -1
  83. package/template/.cowork-flow/scripts/adapters/host/policy_base.py +58 -0
  84. package/template/.cowork-flow/scripts/adapters/host/qoder_policy.py +66 -0
  85. package/template/.cowork-flow/scripts/adapters/host/workflow_state_hook.py +136 -132
  86. package/template/.cowork-flow/scripts/adapters/host/zcode_policy.py +23 -24
  87. package/template/.cowork-flow/scripts/adapters/mcp/state_server.py +41 -56
  88. package/template/.cowork-flow/scripts/adapters/review/test_intent.py +2 -2
  89. package/template/.cowork-flow/scripts/infra/config.py +26 -23
  90. package/template/.cowork-flow/scripts/infra/developer_profile.py +4 -41
  91. package/template/.cowork-flow/scripts/infra/files.py +3 -7
  92. package/template/.cowork-flow/scripts/infra/git_snapshot.py +8 -7
  93. package/template/.cowork-flow/scripts/infra/paths.py +9 -69
  94. package/template/.cowork-flow/scripts/infra/process.py +9 -0
  95. package/template/.cowork-flow/scripts/infra/skill_manifest.py +262 -169
  96. package/template/.cowork-flow/scripts/infra/storage/operation_log.py +2 -0
  97. package/template/.cowork-flow/scripts/infra/storage/state_store.py +0 -2
  98. package/template/.cowork-flow/scripts/infra/storage/unit_of_work.py +163 -18
  99. package/template/.cowork-flow/scripts/kernel/workflow_route.py +6 -16
  100. package/template/.cowork-flow/scripts/run.py +14 -4
  101. package/template/.cowork-flow/scripts/runtime/host_identity.py +33 -15
  102. package/template/.cowork-flow/scripts/runtime/session_state.py +83 -26
  103. package/template/.cowork-flow/scripts/services/context_discovery.py +15 -3
  104. package/template/.cowork-flow/scripts/services/context_paths.py +4 -4
  105. package/template/.cowork-flow/scripts/services/fact_view.py +13 -10
  106. package/template/.cowork-flow/scripts/services/lifecycle_checks.py +47 -13
  107. package/template/.cowork-flow/scripts/services/lifecycle_policy.py +5 -2
  108. package/template/.cowork-flow/scripts/services/readiness.py +8 -3
  109. package/template/.cowork-flow/scripts/services/spec_check.py +23 -33
  110. package/template/.cowork-flow/scripts/services/task_archive.py +76 -41
  111. package/template/.cowork-flow/scripts/services/task_creation.py +90 -34
  112. package/template/.cowork-flow/scripts/services/task_lifecycle.py +210 -50
  113. package/template/.cowork-flow/scripts/services/task_routing.py +0 -8
  114. package/template/.cowork-flow/scripts/services/task_tree.py +181 -29
  115. package/template/.cowork-flow/scripts/services/task_utils.py +3 -20
  116. package/template/.cowork-flow/scripts/services/workflow_runtime.py +39 -22
  117. package/template/.cowork-flow/spec/contracts/context-injection.md +27 -2
  118. package/template/.cowork-flow/spec/contracts/fact-layer-access.md +7 -4
  119. package/template/.cowork-flow/spec/contracts/index.md +4 -0
  120. package/template/.cowork-flow/spec/contracts/spec-checks.md +13 -2
  121. package/template/.cowork-flow/spec/runtime/host-assets.json +155 -18
  122. package/template/.cowork-flow/spec/runtime/index.md +1 -1
  123. package/template/.cowork-flow/spec/schemas/host-assets.schema.json +69 -7
  124. package/template/.dsh/README.md +27 -7
  125. package/template/.opencode/cowork-flow/plugin-core.js +1077 -0
  126. package/template/.opencode/plugins/cowork-flow.js +23 -800
  127. package/template/skills/python-runtime-design/SKILL.md +6 -0
  128. package/template/skills/runtime-health/scripts/doctor.py +725 -14
  129. /package/{template/.zcode → presets/zcode}/hooks/hooks.json +0 -0
package/src/cli.js CHANGED
@@ -2,32 +2,19 @@ import { stdin as input, stdout as output } from 'node:process';
2
2
  import { emitKeypressEvents } from 'node:readline';
3
3
  import { createInterface } from 'node:readline/promises';
4
4
 
5
- import { runInit } from './commands/init.js';
6
- import { runInstallDshHook } from './commands/install-dsh-hook.js';
7
- import { runInstallDshPreset } from './commands/install-dsh-preset.js';
8
- import { runInstallKimiHook } from './commands/install-kimi-hook.js';
9
- import { runInstallZCodePlugin } from './commands/install-zcode-plugin.js';
10
- import { runMcpState } from './commands/mcp-state.js';
11
- import { runSourceRefresh } from './commands/source-refresh.js';
12
- import { runSync } from './commands/sync.js';
13
- import { runUpdate } from './commands/update.js';
5
+ import { UsageError } from './lib/cli-flags.js';
14
6
  import { readPackageInfo } from './lib/package-info.js';
15
-
16
- const HELP = `cowork-flow
17
-
18
- Usage:
19
- cowork-flow init [target] --platform <codex|opencode|claude-code|all> [--developer <name>] [--dry-run] [--force]
20
- cowork-flow install-zcode-plugin [--dry-run] [--force] [--prune-old]
21
- cowork-flow install-dsh-preset [--dry-run] [--force]
22
- cowork-flow install-dsh-hook [--dry-run] [--force] [--uninstall]
23
- cowork-flow install-kimi-hook [--dry-run] [--force] [--uninstall]
24
- cowork-flow update
25
- cowork-flow sync [target] [--dry-run] [--force]
26
- cowork-flow source-refresh [target] [--dry-run]
27
- cowork-flow mcp-state
28
- cowork-flow --version
29
- cowork-flow --help
30
- `;
7
+ import {
8
+ aliasNotice,
9
+ groupFor,
10
+ helpPath,
11
+ renderHelp,
12
+ resolve
13
+ } from './commands/registry.js';
14
+
15
+ // A usage error is the caller's mistake and gets its own
16
+ // exit code; a failed operation stays 1. Success is 0.
17
+ export const EXIT_USAGE = 2;
31
18
 
32
19
  function defaultIo() {
33
20
  return {
@@ -52,55 +39,99 @@ async function defaultPrompt(message) {
52
39
  }
53
40
  }
54
41
 
55
- async function defaultSelectPlatforms({ message, choices, defaultSelected = [] }) {
56
- if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== 'function') {
57
- return null;
58
- }
59
42
 
60
- const selected = new Set(defaultSelected);
61
- let activeIndex = 0;
62
- let search = '';
63
- let renderedLines = 0;
43
+ function matchingChoices(choices, search) {
44
+ const term = search.toLowerCase();
45
+ return choices.filter((choice) => choice.label.toLowerCase().includes(term));
46
+ }
47
+
64
48
 
65
- const filteredChoices = () => {
66
- const term = search.toLowerCase();
67
- return choices.filter((choice) => choice.label.toLowerCase().includes(term));
68
- };
49
+ function pickerLines(state, { message, choices }) {
50
+ const visibleChoices = matchingChoices(choices, state.search);
51
+ if (state.activeIndex >= visibleChoices.length) {
52
+ state.activeIndex = Math.max(0, visibleChoices.length - 1);
53
+ }
69
54
 
70
- const render = () => {
71
- const visibleChoices = filteredChoices();
72
- if (activeIndex >= visibleChoices.length) {
73
- activeIndex = Math.max(0, visibleChoices.length - 1);
55
+ const selectedLabels = choices
56
+ .filter((choice) => state.selected.has(choice.value))
57
+ .map((choice) => choice.label);
58
+ const lines = [
59
+ `? ${message} (${choices.length} available)`,
60
+ `Selected: ${selectedLabels.length > 0 ? selectedLabels.join(', ') : '(none)'}`,
61
+ `Search: ${state.search || '[type to filter]'}`,
62
+ '↑↓ navigate • Space toggle • Backspace remove • Enter confirm'
63
+ ];
64
+
65
+ if (visibleChoices.length === 0) {
66
+ lines.push(' (no matches)');
67
+ } else {
68
+ for (let index = 0; index < visibleChoices.length; index += 1) {
69
+ const choice = visibleChoices[index];
70
+ const cursor = index === state.activeIndex ? '›' : ' ';
71
+ const marker = state.selected.has(choice.value) ? '◉' : '○';
72
+ const suffix = state.selected.has(choice.value) ? ' (selected)' : '';
73
+ lines.push(`${cursor} ${marker} ${choice.label}${suffix}`);
74
74
  }
75
+ }
76
+ return lines;
77
+ }
75
78
 
76
- if (renderedLines > 0) {
77
- output.write(`\x1b[${renderedLines}A\x1b[0J`);
78
- }
79
79
 
80
- const selectedLabels = choices
81
- .filter((choice) => selected.has(choice.value))
82
- .map((choice) => choice.label);
83
- const lines = [
84
- `? ${message} (${choices.length} available)`,
85
- `Selected: ${selectedLabels.length > 0 ? selectedLabels.join(', ') : '(none)'}`,
86
- `Search: ${search || '[type to filter]'}`,
87
- '↑↓ navigate • Space toggle • Backspace remove • Enter confirm'
88
- ];
89
-
90
- if (visibleChoices.length === 0) {
91
- lines.push(' (no matches)');
80
+ function drawPicker(state, { message, choices }) {
81
+ const lines = pickerLines(state, { message, choices });
82
+ if (state.renderedLines > 0) {
83
+ output.write(`\x1b[${state.renderedLines}A\x1b[0J`);
84
+ }
85
+ output.write(`${lines.join('\n')}\n`);
86
+ state.renderedLines = lines.length;
87
+ }
88
+
89
+
90
+ // Returns a cancel/confirm outcome, or null for "mutated,
91
+ // redraw".
92
+ function applyPickerKey(state, text, key, choices) {
93
+ const visibleChoices = matchingChoices(choices, state.search);
94
+ if (key.ctrl && key.name === 'c') {
95
+ return { outcome: 'cancel' };
96
+ }
97
+ if (key.name === 'return' || key.name === 'enter') {
98
+ if (state.selected.size === 0 && visibleChoices[state.activeIndex]) {
99
+ state.selected.add(visibleChoices[state.activeIndex].value);
100
+ }
101
+ return { outcome: 'confirm', value: [...state.selected] };
102
+ }
103
+ if (key.name === 'up') {
104
+ state.activeIndex = Math.max(0, state.activeIndex - 1);
105
+ } else if (key.name === 'down') {
106
+ state.activeIndex = Math.min(Math.max(visibleChoices.length - 1, 0), state.activeIndex + 1);
107
+ } else if (key.name === 'space' && visibleChoices[state.activeIndex]) {
108
+ const value = visibleChoices[state.activeIndex].value;
109
+ if (state.selected.has(value)) {
110
+ state.selected.delete(value);
92
111
  } else {
93
- for (let index = 0; index < visibleChoices.length; index += 1) {
94
- const choice = visibleChoices[index];
95
- const cursor = index === activeIndex ? '›' : ' ';
96
- const marker = selected.has(choice.value) ? '◉' : '○';
97
- const suffix = selected.has(choice.value) ? ' (selected)' : '';
98
- lines.push(`${cursor} ${marker} ${choice.label}${suffix}`);
99
- }
112
+ state.selected.add(value);
100
113
  }
114
+ } else if (key.name === 'backspace') {
115
+ state.search = state.search.slice(0, -1);
116
+ state.activeIndex = 0;
117
+ } else if (text && text.trim() && text.length === 1) {
118
+ state.search += text;
119
+ state.activeIndex = 0;
120
+ }
121
+ return null;
122
+ }
123
+
124
+
125
+ async function defaultSelectPlatforms({ message, choices, defaultSelected = [] }) {
126
+ if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== 'function') {
127
+ return null;
128
+ }
101
129
 
102
- output.write(`${lines.join('\n')}\n`);
103
- renderedLines = lines.length;
130
+ const state = {
131
+ selected: new Set(defaultSelected),
132
+ activeIndex: 0,
133
+ search: '',
134
+ renderedLines: 0
104
135
  };
105
136
 
106
137
  return await new Promise((resolve, reject) => {
@@ -115,106 +146,75 @@ async function defaultSelectPlatforms({ message, choices, defaultSelected = [] }
115
146
  };
116
147
 
117
148
  const onKeypress = (text, key = {}) => {
118
- const visibleChoices = filteredChoices();
119
- if (key.ctrl && key.name === 'c') {
149
+ const action = applyPickerKey(state, text, key, choices);
150
+ if (action?.outcome === 'cancel') {
120
151
  cleanup();
121
152
  reject(new Error('Platform selection cancelled'));
122
153
  return;
123
154
  }
124
- if (key.name === 'return' || key.name === 'enter') {
125
- if (selected.size === 0 && visibleChoices[activeIndex]) {
126
- selected.add(visibleChoices[activeIndex].value);
127
- }
155
+ if (action?.outcome === 'confirm') {
128
156
  cleanup();
129
- resolve([...selected]);
157
+ resolve(action.value);
130
158
  return;
131
159
  }
132
- if (key.name === 'up') {
133
- activeIndex = Math.max(0, activeIndex - 1);
134
- } else if (key.name === 'down') {
135
- activeIndex = Math.min(Math.max(visibleChoices.length - 1, 0), activeIndex + 1);
136
- } else if (key.name === 'space' && visibleChoices[activeIndex]) {
137
- const value = visibleChoices[activeIndex].value;
138
- if (selected.has(value)) {
139
- selected.delete(value);
140
- } else {
141
- selected.add(value);
142
- }
143
- } else if (key.name === 'backspace') {
144
- search = search.slice(0, -1);
145
- activeIndex = 0;
146
- } else if (text && text.trim() && text.length === 1) {
147
- search += text;
148
- activeIndex = 0;
149
- }
150
- render();
160
+ drawPicker(state, { message, choices });
151
161
  };
152
162
 
153
163
  input.on('keypress', onKeypress);
154
- render();
164
+ drawPicker(state, { message, choices });
155
165
  });
156
166
  }
157
167
 
168
+
158
169
  export async function main(argv = process.argv.slice(2), options = {}) {
159
170
  const io = options.io ?? defaultIo();
160
171
  const prompt = Object.hasOwn(options, 'prompt') ? options.prompt : defaultPrompt;
161
172
  const selectPlatforms = Object.hasOwn(options, 'selectPlatforms')
162
173
  ? options.selectPlatforms
163
174
  : defaultSelectPlatforms;
164
- const [command, ...args] = argv;
165
175
 
166
176
  try {
167
- if (!command || command === '--help' || command === '-h') {
168
- io.writeOut(HELP);
177
+ // Asking for help is an explicit intent that outranks
178
+ // everything else in argv, so it is answered before
179
+ // any command is resolved.
180
+ if (argv.includes('--help') || argv.includes('-h')) {
181
+ io.writeOut(renderHelp(helpPath(argv)));
169
182
  return 0;
170
183
  }
171
184
 
172
- if (command === '--version' || command === '-v') {
173
- const packageInfo = await readPackageInfo();
174
- io.writeOut(`${packageInfo.version}\n`);
185
+ if (argv.length === 0) {
186
+ io.writeOut(renderHelp([]));
175
187
  return 0;
176
188
  }
177
189
 
178
- if (command === 'init') {
179
- return await runInit(args, { io, prompt, selectPlatforms });
180
- }
181
-
182
- if (command === 'sync') {
183
- return await runSync(args, { io });
184
- }
185
-
186
- if (command === 'source-refresh') {
187
- return await runSourceRefresh(args, { io });
188
- }
189
-
190
- if (command === 'update') {
191
- return await runUpdate(args, { io });
192
- }
193
-
194
- if (command === 'install-zcode-plugin') {
195
- return await runInstallZCodePlugin(args);
196
- }
197
-
198
- if (command === 'install-dsh-preset') {
199
- return await runInstallDshPreset(args);
190
+ if (argv[0] === '--version' || argv[0] === '-v') {
191
+ const packageInfo = await readPackageInfo();
192
+ io.writeOut(`${packageInfo.version}\n`);
193
+ return 0;
200
194
  }
201
195
 
202
- if (command === 'install-dsh-hook') {
203
- return await runInstallDshHook(args);
196
+ if (argv[0] === 'help') {
197
+ io.writeOut(renderHelp(helpPath(argv)));
198
+ return 0;
204
199
  }
205
200
 
206
- if (command === 'install-kimi-hook') {
207
- return await runInstallKimiHook(args);
201
+ // A bare group names no action: answer with that
202
+ // group's commands instead of an error, which is what
203
+ // the caller wants anyway.
204
+ if (argv.length === 1 && groupFor(argv[0])) {
205
+ io.writeOut(renderHelp([argv[0]]));
206
+ return 0;
208
207
  }
209
208
 
210
- if (command === 'mcp-state') {
211
- return await runMcpState(args);
209
+ const { command, args, alias } = resolve(argv);
210
+ if (alias !== null && !alias.permanent) {
211
+ io.writeErr(aliasNotice(alias));
212
212
  }
213
-
214
- io.writeErr(`Unknown command: ${command}\n`);
215
- return 1;
213
+ // Installers that finish quietly return nothing;
214
+ // `main` always answers with an exit code.
215
+ return (await command.run(args, { io, prompt, selectPlatforms })) ?? 0;
216
216
  } catch (error) {
217
217
  io.writeErr(`${error instanceof Error ? error.message : String(error)}\n`);
218
- return 1;
218
+ return error instanceof UsageError ? EXIT_USAGE : 1;
219
219
  }
220
220
  }
@@ -0,0 +1,182 @@
1
+ import { join, resolve } from 'node:path';
2
+
3
+ import { UsageError, extractValueFlag, parseFlags } from '../lib/cli-flags.js';
4
+ import { hostRegistry } from '../lib/host-assets.js';
5
+ import { pathExists } from '../lib/install-support.js';
6
+ import { runInstallCodexPlugin, FLAGS as CODEX_PLUGIN_FLAGS } from './install-codex-plugin.js';
7
+ import { runInstallClaudeCodePlugin, FLAGS as CLAUDE_CODE_PLUGIN_FLAGS } from './install-claude-code-plugin.js';
8
+ import { runInstallDshHook, FLAGS as DSH_HOOK_FLAGS } from './install-dsh-hook.js';
9
+ import { runInstallDshPreset, FLAGS as DSH_PRESET_FLAGS } from './install-dsh-preset.js';
10
+ import { runInstallKimiPlugin, FLAGS as KIMI_PLUGIN_FLAGS } from './install-kimi-code-plugin.js';
11
+ import { runInstallKimiHook, FLAGS as KIMI_HOOK_FLAGS } from './install-kimi-hook.js';
12
+ import { runInstallOpenCodePlugin, FLAGS as OPENCODE_PLUGIN_FLAGS } from './install-opencode-plugin.js';
13
+ import { runInstallQoderPlugin, FLAGS as QODER_PLUGIN_FLAGS } from './install-qoder-plugin.js';
14
+ import { runInstallZCodePlugin, FLAGS as ZCODE_PLUGIN_FLAGS } from './install-zcode-plugin.js';
15
+
16
+
17
+ // Which machine-level integrations a host has.
18
+ // Deliberately not part of host-assets.json: that manifest
19
+ // ships into user projects and declares asset facts, while
20
+ // this is a property of the command surface. A gate keeps
21
+ // the two in step.
22
+ export const HOST_COMPONENTS = {
23
+ codex: {
24
+ default: 'plugin',
25
+ components: {
26
+ plugin: { summary: 'Codex plugin: marketplace source plus the bootstrap skill', flags: CODEX_PLUGIN_FLAGS, run: runInstallCodexPlugin }
27
+ }
28
+ },
29
+ 'claude-code': {
30
+ default: 'plugin',
31
+ components: {
32
+ plugin: { summary: 'Claude Code skills-directory plugin: the bootstrap skill', flags: CLAUDE_CODE_PLUGIN_FLAGS, run: runInstallClaudeCodePlugin }
33
+ }
34
+ },
35
+ opencode: {
36
+ default: 'plugin',
37
+ components: {
38
+ plugin: { summary: 'OpenCode plugin: the plugin file plus the bootstrap skill it registers', flags: OPENCODE_PLUGIN_FLAGS, run: runInstallOpenCodePlugin }
39
+ }
40
+ },
41
+ zcode: {
42
+ default: 'plugin',
43
+ components: {
44
+ plugin: { summary: 'ZCode plugin: hooks, agents and the bootstrap skill', flags: ZCODE_PLUGIN_FLAGS, run: runInstallZCodePlugin }
45
+ }
46
+ },
47
+ qoder: {
48
+ default: 'plugin',
49
+ components: {
50
+ plugin: { summary: 'Qoder plugin: payload, registry entry and enable flag', flags: QODER_PLUGIN_FLAGS, run: runInstallQoderPlugin }
51
+ }
52
+ },
53
+ dsh: {
54
+ default: 'preset',
55
+ components: {
56
+ preset: { summary: 'DSH agent preset (bundles the hook, the primary path)', flags: DSH_PRESET_FLAGS, run: runInstallDshPreset },
57
+ hook: { summary: 'DSH workflow-state hook as a machine-level patch row', flags: DSH_HOOK_FLAGS, run: runInstallDshHook }
58
+ }
59
+ },
60
+ 'kimi-code': {
61
+ default: 'plugin',
62
+ components: {
63
+ plugin: { summary: 'Kimi Code plugin: the bootstrap Skill it loads at session start', flags: KIMI_PLUGIN_FLAGS, run: runInstallKimiPlugin },
64
+ hook: { summary: 'Kimi Code context-injection hook (fallback when no plugin is installed)', flags: KIMI_HOOK_FLAGS, run: runInstallKimiHook }
65
+ }
66
+ }
67
+ };
68
+
69
+
70
+ // Every flag any installer accepts, in a stable order.
71
+ // `host add` forwards argv unchanged, so installers stay
72
+ // the only validators; this list is what `host add --help`
73
+ // can name.
74
+ export const HOST_FLAGS = [...new Set(
75
+ Object.values(HOST_COMPONENTS).flatMap(
76
+ (host) => Object.values(host.components).flatMap((component) => component.flags)
77
+ )
78
+ )].sort();
79
+
80
+
81
+ function resolveHost(token) {
82
+ const id = hostRegistry.platformIdFor(token);
83
+ if (id === null) {
84
+ throw new UsageError(
85
+ `Unknown host: ${token}. Declared hosts: ${hostRegistry.platformIds.join(', ')}`
86
+ );
87
+ }
88
+ const host = HOST_COMPONENTS[id];
89
+ return { id, host };
90
+ }
91
+
92
+
93
+ function resolveComponent(id, host, name) {
94
+ const componentName = name ?? host.default;
95
+ const component = host.components[componentName];
96
+ if (!component) {
97
+ throw new UsageError(
98
+ `Host ${id} has no component ${componentName}. `
99
+ + `Components: ${Object.keys(host.components).join(', ')}`
100
+ );
101
+ }
102
+ return component;
103
+ }
104
+
105
+
106
+ // `--component` is the only flag these two commands own;
107
+ // everything else belongs to the installer and is
108
+ // forwarded verbatim, so a flag this module never heard of
109
+ // still reaches it and is validated there.
110
+ function splitHostArgs(args) {
111
+ const { value: componentName, rest } = extractValueFlag(args, '--component');
112
+ const [token, ...forwarded] = rest;
113
+ if (token === undefined) {
114
+ throw new UsageError('Missing required argument: <host>');
115
+ }
116
+ const { id, host } = resolveHost(token);
117
+ return { id, component: resolveComponent(id, host, componentName), forwarded };
118
+ }
119
+
120
+
121
+ export async function runHostAdd(args = []) {
122
+ const { component, forwarded } = splitHostArgs(args);
123
+ return await component.run(forwarded);
124
+ }
125
+
126
+
127
+ export async function runHostRemove(args = []) {
128
+ const { component, forwarded } = splitHostArgs(args);
129
+ return await component.run(['--uninstall', ...forwarded]);
130
+ }
131
+
132
+
133
+ export async function runHostList(args = [], { io } = {}) {
134
+ const { flags, positionals } = parseFlags(args, {
135
+ boolean: ['--json'],
136
+ positional: { max: 1, name: '[target]' }
137
+ });
138
+ const target = resolve(positionals[0] ?? process.cwd());
139
+
140
+ const rows = [];
141
+ for (const platform of hostRegistry.platforms) {
142
+ // Every declared host has components;
143
+ // cli-registry.test.js asserts it.
144
+ const host = HOST_COMPONENTS[platform.id];
145
+ rows.push({
146
+ id: platform.id,
147
+ displayName: platform.displayName,
148
+ aliases: [...platform.aliases],
149
+ components: Object.keys(host.components),
150
+ defaultComponent: host.default,
151
+ // The per-project adapter record is the selection
152
+ // signal.
153
+ selected: await pathExists(join(target, ...platform.adapterPath.split('/')))
154
+ });
155
+ }
156
+
157
+ if (flags['--json']) {
158
+ io.writeOut(`${JSON.stringify({ target, hosts: rows }, null, 2)}\n`);
159
+ return 0;
160
+ }
161
+
162
+ const header = ['host', 'name', 'components', 'selected'];
163
+ const table = rows.map((row) => [
164
+ row.id,
165
+ row.displayName,
166
+ row.components.join(', '),
167
+ row.selected ? 'yes' : 'no'
168
+ ]);
169
+ const widths = header.map((cell, index) => Math.max(
170
+ cell.length,
171
+ ...table.map((line) => line[index].length)
172
+ ));
173
+ const format = (line) => line.map((cell, index) => cell.padEnd(widths[index])).join(' ').trimEnd();
174
+
175
+ io.writeOut(`Hosts declared in host-assets.json (project: ${target})\n`);
176
+ io.writeOut(`${format(header)}\n`);
177
+ for (const line of table) {
178
+ io.writeOut(`${format(line)}\n`);
179
+ }
180
+ io.writeOut('`selected` means this project has the host\'s adapter.\n');
181
+ return 0;
182
+ }
@@ -13,48 +13,33 @@ import {
13
13
  parsePlatformSelection,
14
14
  platformLabel
15
15
  } from '../lib/platforms.js';
16
+ import { parseFlags, UsageError } from '../lib/cli-flags.js';
17
+
18
+ // The manifest layer reports a plain Error; re-thrown as a
19
+ // usage error so a bad host name exits with the
20
+ // argv-mistake code.
21
+ function parsePlatformSelectionOrUsage(values) {
22
+ try {
23
+ return parsePlatformSelection(values);
24
+ } catch (error) {
25
+ throw new UsageError(error instanceof Error ? error.message : String(error));
26
+ }
27
+ }
16
28
 
17
29
  function parseInitArgs(args) {
18
- const options = {
19
- developer: null,
20
- dryRun: false,
21
- force: false,
22
- platforms: [],
23
- target: process.cwd()
30
+ const { flags, positionals } = parseFlags(args, {
31
+ boolean: ['--dry-run', '--force'],
32
+ value: ['--platform', '--platforms', '--developer'],
33
+ repeatable: ['--platform', '--platforms'],
34
+ positional: { min: 0, max: 1, name: '[target]' }
35
+ });
36
+ return {
37
+ developer: flags['--developer'] ?? null,
38
+ dryRun: Boolean(flags['--dry-run']),
39
+ force: Boolean(flags['--force']),
40
+ platforms: [...(flags['--platform'] ?? []), ...(flags['--platforms'] ?? [])],
41
+ target: positionals[0] === undefined ? process.cwd() : resolve(positionals[0])
24
42
  };
25
- for (let index = 0; index < args.length; index += 1) {
26
- const arg = args[index];
27
- if (arg === '--dry-run') {
28
- options.dryRun = true;
29
- } else if (arg === '--force') {
30
- options.force = true;
31
- } else if (arg === '--platform' || arg === '--platforms') {
32
- const value = args[index + 1];
33
- if (!value || value.startsWith('--')) {
34
- throw new Error('Missing value for --platform');
35
- }
36
- options.platforms.push(value);
37
- index += 1;
38
- } else if (arg.startsWith('--platform=')) {
39
- options.platforms.push(arg.slice('--platform='.length));
40
- } else if (arg.startsWith('--platforms=')) {
41
- options.platforms.push(arg.slice('--platforms='.length));
42
- } else if (arg === '--developer') {
43
- const value = args[index + 1];
44
- if (!value || value.startsWith('--')) {
45
- throw new Error('Missing value for --developer');
46
- }
47
- options.developer = value;
48
- index += 1;
49
- } else if (arg.startsWith('--developer=')) {
50
- options.developer = arg.slice('--developer='.length);
51
- } else if (arg.startsWith('--')) {
52
- throw new Error(`Unknown init option: ${arg}`);
53
- } else {
54
- options.target = resolve(arg);
55
- }
56
- }
57
- return options;
58
43
  }
59
44
 
60
45
  async function pathExists(path) {
@@ -72,10 +57,10 @@ async function pathExists(path) {
72
57
  function normalizeDeveloperName(value) {
73
58
  const name = String(value ?? '').trim();
74
59
  if (!name) {
75
- throw new Error('Developer name required. Run: cowork-flow init <target> --developer <name>');
60
+ throw new UsageError('Developer name required. Run: cwf project init <target> --developer <name>');
76
61
  }
77
62
  if (/[\\/]/.test(name)) {
78
- throw new Error('Developer name must not contain path separators');
63
+ throw new UsageError('Developer name must not contain path separators');
79
64
  }
80
65
  return name;
81
66
  }
@@ -110,7 +95,7 @@ async function resolveDeveloperName(options, prompt) {
110
95
 
111
96
  async function resolvePlatforms(options, selectPlatforms) {
112
97
  if (options.platforms.length > 0) {
113
- return parsePlatformSelection(options.platforms);
98
+ return parsePlatformSelectionOrUsage(options.platforms);
114
99
  }
115
100
 
116
101
  let selected = null;
@@ -124,7 +109,7 @@ async function resolvePlatforms(options, selectPlatforms) {
124
109
  defaultSelected: ['codex']
125
110
  });
126
111
  }
127
- return parsePlatformSelection(selected);
112
+ return parsePlatformSelectionOrUsage(selected);
128
113
  }
129
114
 
130
115