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.
- package/CHANGELOG.md +213 -213
- package/CONTRIBUTING.md +137 -0
- package/LICENSE +1 -1
- package/README.md +108 -369
- package/docs/architecture.md +122 -0
- package/docs/hosts.md +290 -0
- package/docs/index.md +32 -0
- package/docs/release.md +104 -0
- package/package.json +37 -7
- package/presets/claude-code/.claude-plugin/plugin.json +28 -0
- package/presets/claude-code/skills/cowork-flow-bootstrap/SKILL.md +35 -0
- package/presets/codex/.codex-plugin/plugin.json +37 -0
- package/presets/codex/assets/logo.svg +4 -0
- package/presets/codex/skills/cowork-flow-bootstrap/SKILL.md +35 -0
- package/presets/dsh/agent.cordis.yml +35 -12
- package/presets/dsh/plugins/workflow-state.js +88 -72
- package/presets/dsh/preset.yml +1 -1
- package/presets/kimi-code/.kimi-plugin/plugin.json +35 -0
- package/presets/kimi-code/hooks/cowork-flow-inject.mjs +14 -2
- package/presets/kimi-code/skills/cowork-flow-bootstrap/SKILL.md +35 -0
- package/presets/opencode/cowork-flow/plugin-core.js +1077 -0
- package/presets/opencode/cowork-flow/skills/cowork-flow-bootstrap/SKILL.md +35 -0
- package/presets/opencode/plugins/cowork-flow.js +44 -0
- package/presets/plugin-meta.json +52 -0
- package/presets/qoder/.qoder-plugin/plugin.json +29 -0
- package/presets/qoder/agents/cowork-check.md +29 -0
- package/presets/qoder/agents/cowork-implement.md +30 -0
- package/presets/qoder/agents/cowork-research.md +23 -0
- package/presets/qoder/hooks/hooks.json +50 -0
- package/presets/qoder/hooks/inject-context.mjs +150 -0
- package/presets/qoder/skills/cowork-flow-bootstrap/SKILL.md +35 -0
- package/{template/.zcode → presets/zcode}/.zcode-plugin/plugin.json +4 -2
- package/{template/.zcode → presets/zcode}/agents/cowork-check.md +4 -4
- package/{template/.zcode → presets/zcode}/agents/cowork-implement.md +4 -4
- package/{template/.zcode → presets/zcode}/agents/cowork-research.md +1 -1
- package/{template/.zcode → presets/zcode}/hooks/inject-context.js +36 -19
- package/presets/zcode/skills/cowork-flow-bootstrap/SKILL.md +35 -0
- package/src/cli.js +130 -130
- package/src/commands/host.js +182 -0
- package/src/commands/init.js +28 -43
- package/src/commands/install-claude-code-plugin.js +129 -0
- package/src/commands/install-codex-plugin.js +312 -0
- package/src/commands/install-dsh-hook.js +36 -43
- package/src/commands/install-dsh-preset.js +228 -49
- package/src/commands/install-kimi-code-plugin.js +142 -0
- package/src/commands/install-kimi-hook.js +142 -122
- package/src/commands/install-opencode-plugin.js +158 -0
- package/src/commands/install-qoder-plugin.js +211 -0
- package/src/commands/install-zcode-plugin.js +131 -82
- package/src/commands/mcp-state.js +7 -6
- package/src/commands/registry.js +345 -0
- package/src/commands/source-refresh.js +10 -11
- package/src/commands/sync.js +10 -13
- package/src/commands/update.js +5 -10
- package/src/lib/cli-flags.js +102 -0
- package/src/lib/copy-template.js +89 -75
- package/src/lib/host-assets.js +192 -57
- package/src/lib/install-support.js +50 -0
- package/src/lib/platforms.js +0 -15
- package/src/lib/plugin-metadata.js +187 -0
- package/src/lib/plugin-payload.js +70 -0
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/adapters/claude-code/adapter.yaml +0 -1
- package/template/.cowork-flow/adapters/dsh/adapter.yaml +0 -1
- package/template/.cowork-flow/adapters/kimi-code/adapter.yaml +0 -1
- package/template/.cowork-flow/adapters/qoder/adapter.yaml +32 -0
- package/template/.cowork-flow/adapters/zcode/adapter.yaml +2 -2
- package/template/.cowork-flow/config.yaml +6 -4
- package/template/.cowork-flow/scripts/adapters/cli/batch_mode.py +10 -12
- package/template/.cowork-flow/scripts/adapters/cli/spec_check.py +67 -35
- package/template/.cowork-flow/scripts/adapters/cli/subagent.py +84 -38
- package/template/.cowork-flow/scripts/adapters/cli/task.py +42 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_create_command.py +1 -9
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +6 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +13 -10
- package/template/.cowork-flow/scripts/adapters/cli/task_next_runner.py +2 -1
- package/template/.cowork-flow/scripts/adapters/git/git_context.py +14 -82
- package/template/.cowork-flow/scripts/adapters/host/claude_code_policy.py +2 -3
- package/template/.cowork-flow/scripts/adapters/host/codex_policy.py +2 -5
- package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +122 -10
- package/template/.cowork-flow/scripts/adapters/host/inject.py +49 -19
- package/template/.cowork-flow/scripts/adapters/host/kimi_code_policy.py +1 -1
- package/template/.cowork-flow/scripts/adapters/host/policy_base.py +58 -0
- package/template/.cowork-flow/scripts/adapters/host/qoder_policy.py +66 -0
- package/template/.cowork-flow/scripts/adapters/host/workflow_state_hook.py +136 -132
- package/template/.cowork-flow/scripts/adapters/host/zcode_policy.py +23 -24
- package/template/.cowork-flow/scripts/adapters/mcp/state_server.py +41 -56
- package/template/.cowork-flow/scripts/adapters/review/test_intent.py +2 -2
- package/template/.cowork-flow/scripts/infra/config.py +26 -23
- package/template/.cowork-flow/scripts/infra/developer_profile.py +4 -41
- package/template/.cowork-flow/scripts/infra/files.py +3 -7
- package/template/.cowork-flow/scripts/infra/git_snapshot.py +8 -7
- package/template/.cowork-flow/scripts/infra/paths.py +9 -69
- package/template/.cowork-flow/scripts/infra/process.py +9 -0
- package/template/.cowork-flow/scripts/infra/skill_manifest.py +262 -169
- package/template/.cowork-flow/scripts/infra/storage/operation_log.py +2 -0
- package/template/.cowork-flow/scripts/infra/storage/state_store.py +0 -2
- package/template/.cowork-flow/scripts/infra/storage/unit_of_work.py +163 -18
- package/template/.cowork-flow/scripts/kernel/workflow_route.py +6 -16
- package/template/.cowork-flow/scripts/run.py +14 -4
- package/template/.cowork-flow/scripts/runtime/host_identity.py +33 -15
- package/template/.cowork-flow/scripts/runtime/session_state.py +83 -26
- package/template/.cowork-flow/scripts/services/context_discovery.py +15 -3
- package/template/.cowork-flow/scripts/services/context_paths.py +4 -4
- package/template/.cowork-flow/scripts/services/fact_view.py +13 -10
- package/template/.cowork-flow/scripts/services/lifecycle_checks.py +47 -13
- package/template/.cowork-flow/scripts/services/lifecycle_policy.py +5 -2
- package/template/.cowork-flow/scripts/services/readiness.py +8 -3
- package/template/.cowork-flow/scripts/services/spec_check.py +23 -33
- package/template/.cowork-flow/scripts/services/task_archive.py +76 -41
- package/template/.cowork-flow/scripts/services/task_creation.py +90 -34
- package/template/.cowork-flow/scripts/services/task_lifecycle.py +210 -50
- package/template/.cowork-flow/scripts/services/task_routing.py +0 -8
- package/template/.cowork-flow/scripts/services/task_tree.py +181 -29
- package/template/.cowork-flow/scripts/services/task_utils.py +3 -20
- package/template/.cowork-flow/scripts/services/workflow_runtime.py +39 -22
- package/template/.cowork-flow/spec/contracts/context-injection.md +27 -2
- package/template/.cowork-flow/spec/contracts/fact-layer-access.md +7 -4
- package/template/.cowork-flow/spec/contracts/index.md +4 -0
- package/template/.cowork-flow/spec/contracts/spec-checks.md +13 -2
- package/template/.cowork-flow/spec/runtime/host-assets.json +155 -18
- package/template/.cowork-flow/spec/runtime/index.md +1 -1
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +69 -7
- package/template/.dsh/README.md +27 -7
- package/template/.opencode/cowork-flow/plugin-core.js +1077 -0
- package/template/.opencode/plugins/cowork-flow.js +23 -800
- package/template/skills/python-runtime-design/SKILL.md +6 -0
- package/template/skills/runtime-health/scripts/doctor.py +725 -14
- /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 {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
|
119
|
-
if (
|
|
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 (
|
|
125
|
-
if (selected.size === 0 && visibleChoices[activeIndex]) {
|
|
126
|
-
selected.add(visibleChoices[activeIndex].value);
|
|
127
|
-
}
|
|
155
|
+
if (action?.outcome === 'confirm') {
|
|
128
156
|
cleanup();
|
|
129
|
-
resolve(
|
|
157
|
+
resolve(action.value);
|
|
130
158
|
return;
|
|
131
159
|
}
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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 (
|
|
173
|
-
|
|
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 (
|
|
179
|
-
|
|
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 (
|
|
203
|
-
|
|
196
|
+
if (argv[0] === 'help') {
|
|
197
|
+
io.writeOut(renderHelp(helpPath(argv)));
|
|
198
|
+
return 0;
|
|
204
199
|
}
|
|
205
200
|
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
211
|
-
|
|
209
|
+
const { command, args, alias } = resolve(argv);
|
|
210
|
+
if (alias !== null && !alias.permanent) {
|
|
211
|
+
io.writeErr(aliasNotice(alias));
|
|
212
212
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return
|
|
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
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
60
|
+
throw new UsageError('Developer name required. Run: cwf project init <target> --developer <name>');
|
|
76
61
|
}
|
|
77
62
|
if (/[\\/]/.test(name)) {
|
|
78
|
-
throw new
|
|
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
|
|
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
|
|
112
|
+
return parsePlatformSelectionOrUsage(selected);
|
|
128
113
|
}
|
|
129
114
|
|
|
130
115
|
|