cowork-flow 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -1
- package/README.en.md +46 -22
- package/README.md +10 -7
- package/docs/architecture.en.md +95 -0
- package/docs/architecture.md +7 -3
- package/docs/direction.en.md +71 -0
- package/docs/direction.md +6 -1
- package/docs/hosts.en.md +299 -0
- package/docs/hosts.md +5 -1
- package/docs/index.en.md +30 -0
- package/docs/index.md +2 -0
- package/docs/release.en.md +108 -0
- package/docs/release.md +2 -0
- package/package.json +2 -2
- package/presets/claude-code/.claude-plugin/plugin.json +1 -1
- package/presets/claude-code/skills/cowork-flow-bootstrap/SKILL.md +8 -2
- package/presets/codex/.codex-plugin/plugin.json +1 -1
- package/presets/codex/skills/cowork-flow-bootstrap/SKILL.md +8 -2
- package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
- package/presets/kimi-code/skills/cowork-flow-bootstrap/SKILL.md +8 -2
- package/presets/opencode/cowork-flow/skills/cowork-flow-bootstrap/SKILL.md +8 -2
- package/presets/qoder/.qoder-plugin/plugin.json +1 -1
- package/presets/qoder/skills/cowork-flow-bootstrap/SKILL.md +8 -2
- package/presets/zcode/.zcode-plugin/plugin.json +1 -1
- package/presets/zcode/skills/cowork-flow-bootstrap/SKILL.md +8 -2
- package/src/cli.js +1 -1
- package/src/commands/init.js +200 -16
- package/src/commands/registry.js +10 -7
- package/src/commands/source-refresh.js +5 -1
- package/src/commands/sync.js +4 -1
- package/src/lib/copy-template.js +23 -5
- package/src/lib/host-assets.js +119 -4
- package/src/lib/language.js +162 -0
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/config.yaml +8 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_create_command.py +5 -3
- package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +71 -2
- package/template/.cowork-flow/scripts/infra/config.py +85 -0
- package/template/.cowork-flow/scripts/services/decision_anchor_schema.py +104 -0
- package/template/.cowork-flow/scripts/services/fact_view.py +21 -15
- package/template/.cowork-flow/scripts/services/task_context.py +13 -7
- package/template/.cowork-flow/scripts/services/task_creation.py +72 -30
- package/template/.cowork-flow/scripts/services/task_gate_profiles.py +7 -24
- package/template/.cowork-flow/spec/contracts/context-injection.md +5 -4
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +21 -14
- package/template/.cowork-flow/spec/contracts/plan-binding.md +2 -1
- package/template/.cowork-flow/spec/index.md +26 -26
- package/template/.cowork-flow/spec/references/definition-of-done.md +26 -26
- package/template/.cowork-flow/spec/references/index.md +16 -16
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +71 -71
- package/template/.cowork-flow/spec/references/security-checklist.md +37 -37
- package/template/.cowork-flow/spec/references/testing-checklist.md +32 -32
- package/template/.cowork-flow/spec/runtime/host-assets.json +23 -4
- package/template/.cowork-flow/spec/runtime/index.md +10 -12
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +31 -0
- package/template/.cowork-flow/spec/schemas/index.md +5 -5
- package/template/.cowork-flow/templates/plan-template.md +91 -0
- package/template/locales/en-US/.cowork-flow/spec/contracts/decision-anchor.md +160 -0
- package/template/locales/en-US/.cowork-flow/templates/plan-template.md +91 -0
- package/template/skills/runtime-health/scripts/doctor.py +110 -0
- package/template/skills/task-planning/SKILL.md +19 -92
|
@@ -17,12 +17,18 @@ workflow entry `./.cowork-flow/run`).
|
|
|
17
17
|
|
|
18
18
|
## No runtime: offer the install
|
|
19
19
|
|
|
20
|
-
Tell the user this repository has no cowork-flow runtime, then offer
|
|
20
|
+
Tell the user this repository has no cowork-flow runtime, then offer (replace
|
|
21
|
+
`<host>` with the host you are running in: `codex`, `claude-code`, `opencode`,
|
|
22
|
+
`zcode`, `qoder`, `kimi-code`, or `dsh`):
|
|
21
23
|
|
|
22
24
|
```bash
|
|
23
|
-
npx cowork-flow project init
|
|
25
|
+
npx cowork-flow project init --host <host>
|
|
24
26
|
```
|
|
25
27
|
|
|
28
|
+
The default artifact language is `zh-CN`. When the user asks for English task or
|
|
29
|
+
plan artifacts, append `--language en-US`. An existing
|
|
30
|
+
`.cowork-flow/config.yaml` remains authoritative.
|
|
31
|
+
|
|
26
32
|
After init the project carries its own Skills (`.agents/skills/`), the task
|
|
27
33
|
runtime (`.cowork-flow/`), and the workflow entry `./.cowork-flow/run task next`.
|
|
28
34
|
Resume the work through that entry.
|
package/src/cli.js
CHANGED
|
@@ -149,7 +149,7 @@ async function defaultSelectPlatforms({ message, choices, defaultSelected = [] }
|
|
|
149
149
|
const action = applyPickerKey(state, text, key, choices);
|
|
150
150
|
if (action?.outcome === 'cancel') {
|
|
151
151
|
cleanup();
|
|
152
|
-
reject(new Error('
|
|
152
|
+
reject(new Error('Host selection cancelled'));
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
155
|
if (action?.outcome === 'confirm') {
|
package/src/commands/init.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
1
2
|
import { access, readFile } from 'node:fs/promises';
|
|
2
3
|
import { join, resolve } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
3
5
|
|
|
4
6
|
import {
|
|
5
7
|
applyPlan,
|
|
6
8
|
buildInitPlan,
|
|
9
|
+
detectInstalledPlatforms,
|
|
7
10
|
summarizePlan
|
|
8
11
|
} from '../lib/copy-template.js';
|
|
9
12
|
import { readPackageInfo } from '../lib/package-info.js';
|
|
@@ -14,6 +17,16 @@ import {
|
|
|
14
17
|
platformLabel
|
|
15
18
|
} from '../lib/platforms.js';
|
|
16
19
|
import { parseFlags, UsageError } from '../lib/cli-flags.js';
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_LANGUAGE,
|
|
22
|
+
SUPPORTED_LANGUAGES,
|
|
23
|
+
normalizeLanguage,
|
|
24
|
+
readProjectLanguage,
|
|
25
|
+
withLanguage
|
|
26
|
+
} from '../lib/language.js';
|
|
27
|
+
import { templateRoot } from '../lib/paths.js';
|
|
28
|
+
|
|
29
|
+
const execFileAsync = promisify(execFile);
|
|
17
30
|
|
|
18
31
|
// The manifest layer reports a plain Error; re-thrown as a
|
|
19
32
|
// usage error so a bad host name exits with the
|
|
@@ -29,15 +42,20 @@ function parsePlatformSelectionOrUsage(values) {
|
|
|
29
42
|
function parseInitArgs(args) {
|
|
30
43
|
const { flags, positionals } = parseFlags(args, {
|
|
31
44
|
boolean: ['--dry-run', '--force'],
|
|
32
|
-
value: ['--platform', '--platforms', '--developer'],
|
|
33
|
-
repeatable: ['--platform', '--platforms'],
|
|
45
|
+
value: ['--host', '--platform', '--platforms', '--developer', '--language'],
|
|
46
|
+
repeatable: ['--host', '--platform', '--platforms'],
|
|
34
47
|
positional: { min: 0, max: 1, name: '[target]' }
|
|
35
48
|
});
|
|
36
49
|
return {
|
|
37
50
|
developer: flags['--developer'] ?? null,
|
|
38
51
|
dryRun: Boolean(flags['--dry-run']),
|
|
39
52
|
force: Boolean(flags['--force']),
|
|
40
|
-
|
|
53
|
+
language: flags['--language'] ?? null,
|
|
54
|
+
platforms: [
|
|
55
|
+
...(flags['--host'] ?? []),
|
|
56
|
+
...(flags['--platform'] ?? []),
|
|
57
|
+
...(flags['--platforms'] ?? [])
|
|
58
|
+
],
|
|
41
59
|
target: positionals[0] === undefined ? process.cwd() : resolve(positionals[0])
|
|
42
60
|
};
|
|
43
61
|
}
|
|
@@ -57,7 +75,7 @@ async function pathExists(path) {
|
|
|
57
75
|
function normalizeDeveloperName(value) {
|
|
58
76
|
const name = String(value ?? '').trim();
|
|
59
77
|
if (!name) {
|
|
60
|
-
throw new UsageError('Developer name required. Run: cwf project init
|
|
78
|
+
throw new UsageError('Developer name required. Run: cwf project init [target] --developer <name>');
|
|
61
79
|
}
|
|
62
80
|
if (/[\\/]/.test(name)) {
|
|
63
81
|
throw new UsageError('Developer name must not contain path separators');
|
|
@@ -65,6 +83,32 @@ function normalizeDeveloperName(value) {
|
|
|
65
83
|
return name;
|
|
66
84
|
}
|
|
67
85
|
|
|
86
|
+
function normalizeGitDeveloper(value) {
|
|
87
|
+
const name = String(value ?? '').trim();
|
|
88
|
+
if (!name || /[\\/]/.test(name) || (name.startsWith('<') && name.endsWith('>'))) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return name;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// `git config user.name` reads the local repository first and falls back to the
|
|
95
|
+
// global config, which is the "nearest identity" this command wants. A missing
|
|
96
|
+
// binary, an unset value, or a directory that vanishes under us all mean "no
|
|
97
|
+
// git identity", never a failed init.
|
|
98
|
+
async function readGitDeveloper(target) {
|
|
99
|
+
const cwd = await pathExists(target) ? target : process.cwd();
|
|
100
|
+
try {
|
|
101
|
+
const { stdout } = await execFileAsync('git', ['config', 'user.name'], {
|
|
102
|
+
cwd,
|
|
103
|
+
timeout: 5000,
|
|
104
|
+
windowsHide: true
|
|
105
|
+
});
|
|
106
|
+
return normalizeGitDeveloper(stdout);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
68
112
|
async function readExistingDeveloper(target) {
|
|
69
113
|
const developerFile = join(target, '.cowork-flow', '.developer');
|
|
70
114
|
if (!await pathExists(developerFile)) {
|
|
@@ -78,19 +122,30 @@ async function readExistingDeveloper(target) {
|
|
|
78
122
|
async function resolveDeveloperName(options, prompt) {
|
|
79
123
|
const existing = await readExistingDeveloper(options.target);
|
|
80
124
|
if (existing) {
|
|
81
|
-
return { existing: true, name: existing };
|
|
125
|
+
return { existing: true, name: existing, source: 'existing' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (options.developer) {
|
|
129
|
+
return { existing: false, name: normalizeDeveloperName(options.developer), source: 'flag' };
|
|
82
130
|
}
|
|
83
131
|
|
|
84
|
-
|
|
85
|
-
|
|
132
|
+
const gitName = await readGitDeveloper(options.target);
|
|
133
|
+
|
|
134
|
+
if (options.dryRun) {
|
|
135
|
+
return { existing: false, name: gitName, source: gitName ? 'git' : null };
|
|
86
136
|
}
|
|
87
137
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
138
|
+
if (typeof prompt === 'function') {
|
|
139
|
+
const answer = await prompt(`Developer name${gitName ? ` [${gitName}]` : ''}: `);
|
|
140
|
+
const typed = String(answer ?? '').trim();
|
|
141
|
+
return {
|
|
142
|
+
existing: false,
|
|
143
|
+
name: normalizeDeveloperName(typed || gitName),
|
|
144
|
+
source: typed ? 'prompt' : 'git'
|
|
145
|
+
};
|
|
91
146
|
}
|
|
92
147
|
|
|
93
|
-
return { existing: false, name: normalizeDeveloperName(
|
|
148
|
+
return { existing: false, name: normalizeDeveloperName(gitName), source: 'git' };
|
|
94
149
|
}
|
|
95
150
|
|
|
96
151
|
async function resolvePlatforms(options, selectPlatforms) {
|
|
@@ -98,10 +153,17 @@ async function resolvePlatforms(options, selectPlatforms) {
|
|
|
98
153
|
return parsePlatformSelectionOrUsage(options.platforms);
|
|
99
154
|
}
|
|
100
155
|
|
|
156
|
+
// Reuse the same marker-based detection `project sync` uses, so init and sync
|
|
157
|
+
// agree on what "this project already uses" means.
|
|
158
|
+
const detected = await detectInstalledPlatforms(options.target);
|
|
159
|
+
if (detected.length > 0) {
|
|
160
|
+
return detected;
|
|
161
|
+
}
|
|
162
|
+
|
|
101
163
|
let selected = null;
|
|
102
164
|
if (typeof selectPlatforms === 'function') {
|
|
103
165
|
selected = await selectPlatforms({
|
|
104
|
-
message: 'Select
|
|
166
|
+
message: 'Select hosts to set up',
|
|
105
167
|
choices: SUPPORTED_PLATFORMS.map((platform) => ({
|
|
106
168
|
label: platformLabel(platform),
|
|
107
169
|
value: platform
|
|
@@ -112,6 +174,104 @@ async function resolvePlatforms(options, selectPlatforms) {
|
|
|
112
174
|
return parsePlatformSelectionOrUsage(selected);
|
|
113
175
|
}
|
|
114
176
|
|
|
177
|
+
function normalizeLanguageOrUsage(value) {
|
|
178
|
+
const language = normalizeLanguage(value);
|
|
179
|
+
if (!language) {
|
|
180
|
+
throw new UsageError(
|
|
181
|
+
`Unsupported language: ${value}. Supported: ${SUPPORTED_LANGUAGES.join(', ')}`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return language;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// An existing config is authoritative: the choice is never re-prompted or
|
|
188
|
+
// rewritten, even by --force. A missing config prompts once, with zh-CN as the
|
|
189
|
+
// default for both non-interactive and dry-run runs.
|
|
190
|
+
async function resolveLanguage(options, prompt) {
|
|
191
|
+
const requested = options.language
|
|
192
|
+
? normalizeLanguageOrUsage(options.language)
|
|
193
|
+
: null;
|
|
194
|
+
const existing = await readProjectLanguage(options.target);
|
|
195
|
+
if (existing.present) {
|
|
196
|
+
return {
|
|
197
|
+
existing: true,
|
|
198
|
+
configured: existing.configured,
|
|
199
|
+
language: existing.language,
|
|
200
|
+
valid: existing.valid,
|
|
201
|
+
raw: existing.raw,
|
|
202
|
+
requested,
|
|
203
|
+
source: 'existing'
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (requested) {
|
|
208
|
+
return {
|
|
209
|
+
existing: false,
|
|
210
|
+
language: requested,
|
|
211
|
+
requested,
|
|
212
|
+
source: 'flag'
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (options.dryRun || typeof prompt !== 'function') {
|
|
217
|
+
return {
|
|
218
|
+
existing: false,
|
|
219
|
+
language: DEFAULT_LANGUAGE,
|
|
220
|
+
requested,
|
|
221
|
+
source: 'default'
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const answer = String(
|
|
226
|
+
await prompt(`Project language (${SUPPORTED_LANGUAGES.join('/')}) [${DEFAULT_LANGUAGE}]: `) ?? ''
|
|
227
|
+
).trim();
|
|
228
|
+
return {
|
|
229
|
+
existing: false,
|
|
230
|
+
language: normalizeLanguageOrUsage(answer || DEFAULT_LANGUAGE),
|
|
231
|
+
requested,
|
|
232
|
+
source: answer ? 'prompt' : 'default'
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function emitLanguageWarnings(io, language) {
|
|
237
|
+
if (!language.existing) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!language.valid) {
|
|
241
|
+
io.writeErr(
|
|
242
|
+
`Warning: unsupported language ${JSON.stringify(language.raw)} in .cowork-flow/config.yaml; using ${language.language}. Edit the file to change it.\n`
|
|
243
|
+
);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (language.requested && !language.configured) {
|
|
247
|
+
io.writeErr(
|
|
248
|
+
`Warning: .cowork-flow/config.yaml has no language setting; using ${language.language}. Add language: ${language.requested} and run cwf project sync to switch.\n`
|
|
249
|
+
);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (language.requested && language.requested !== language.language) {
|
|
253
|
+
io.writeErr(
|
|
254
|
+
`Warning: existing .cowork-flow/config.yaml language ${language.language} wins over --language ${language.requested}.\n`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function buildConfigActions(target, language, existing) {
|
|
260
|
+
if (existing) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
const source = join(templateRoot, '.cowork-flow', 'config.yaml');
|
|
264
|
+
const content = withLanguage(await readFile(source, 'utf8'), language);
|
|
265
|
+
return [{
|
|
266
|
+
action: 'create',
|
|
267
|
+
source,
|
|
268
|
+
destination: join(target, '.cowork-flow', 'config.yaml'),
|
|
269
|
+
relativePath: '.cowork-flow/config.yaml',
|
|
270
|
+
content,
|
|
271
|
+
targetExists: false
|
|
272
|
+
}];
|
|
273
|
+
}
|
|
274
|
+
|
|
115
275
|
|
|
116
276
|
async function buildDeveloperActions(target, developer) {
|
|
117
277
|
const workflowDir = join(target, '.cowork-flow');
|
|
@@ -142,27 +302,43 @@ export async function runInitWithOptions(
|
|
|
142
302
|
) {
|
|
143
303
|
const options = parseInitArgs(args);
|
|
144
304
|
const platforms = await resolvePlatforms(options, selectPlatforms);
|
|
305
|
+
const language = await resolveLanguage(options, prompt);
|
|
306
|
+
emitLanguageWarnings(io, language);
|
|
145
307
|
const developer = await resolveDeveloperName(options, prompt);
|
|
146
308
|
const packageInfo = await readPackageInfo();
|
|
147
309
|
const developerActions = developer.name
|
|
148
310
|
? await buildDeveloperActions(options.target, developer)
|
|
149
311
|
: [];
|
|
312
|
+
const configActions = await buildConfigActions(
|
|
313
|
+
options.target,
|
|
314
|
+
language.language,
|
|
315
|
+
language.existing
|
|
316
|
+
);
|
|
150
317
|
const plan = await buildInitPlan(options.target, {
|
|
151
|
-
additionalActions: developerActions,
|
|
318
|
+
additionalActions: [...developerActions, ...configActions],
|
|
152
319
|
force: options.force,
|
|
320
|
+
language: language.language,
|
|
153
321
|
platforms,
|
|
154
322
|
version: packageInfo.version
|
|
155
323
|
});
|
|
156
324
|
|
|
157
325
|
await applyPlan(plan, { dryRun: options.dryRun, fileSystem });
|
|
158
326
|
io.writeOut(summarizePlan(plan, options.dryRun));
|
|
159
|
-
io.writeOut(`
|
|
327
|
+
io.writeOut(`Hosts: ${formatPlatformList(platforms)}\n`);
|
|
160
328
|
|
|
161
329
|
if (options.dryRun) {
|
|
162
330
|
if (developer.existing) {
|
|
163
331
|
io.writeOut(`dry-run preserve-existing=.cowork-flow/.developer for developer ${developer.name}\n`);
|
|
164
332
|
} else if (developer.name) {
|
|
165
|
-
|
|
333
|
+
const source = developer.source === 'git' ? ' (from git user.name)' : '';
|
|
334
|
+
io.writeOut(`dry-run would-create=.cowork-flow/.developer for developer ${developer.name}${source}\n`);
|
|
335
|
+
} else {
|
|
336
|
+
io.writeOut('dry-run would-skip=.cowork-flow/.developer (no developer identity; pass --developer)\n');
|
|
337
|
+
}
|
|
338
|
+
if (language.existing) {
|
|
339
|
+
io.writeOut(`dry-run preserve-existing=.cowork-flow/config.yaml for language ${language.language}\n`);
|
|
340
|
+
} else {
|
|
341
|
+
io.writeOut(`dry-run would-create=.cowork-flow/config.yaml for language ${language.language}\n`);
|
|
166
342
|
}
|
|
167
343
|
io.writeOut('Next: update AGENTS.md and .cowork-flow/config.yaml for this project.\n');
|
|
168
344
|
return 0;
|
|
@@ -171,7 +347,15 @@ export async function runInitWithOptions(
|
|
|
171
347
|
if (developer.existing) {
|
|
172
348
|
io.writeOut(`Developer already initialized: ${developer.name}\n`);
|
|
173
349
|
} else {
|
|
174
|
-
|
|
350
|
+
const source = developer.source === 'git' ? ' (from git user.name)' : '';
|
|
351
|
+
io.writeOut(`Developer initialized: ${developer.name}${source}\n`);
|
|
352
|
+
}
|
|
353
|
+
if (language.existing) {
|
|
354
|
+
const missing = language.configured ? '' : ' (default; no language key)';
|
|
355
|
+
io.writeOut(`Language already initialized: ${language.language}${missing}\n`);
|
|
356
|
+
} else {
|
|
357
|
+
const source = language.source === 'default' ? ' (default)' : '';
|
|
358
|
+
io.writeOut(`Language initialized: ${language.language}${source}\n`);
|
|
175
359
|
}
|
|
176
360
|
io.writeOut('Next: update AGENTS.md and .cowork-flow/config.yaml for this project.\n');
|
|
177
361
|
return 0;
|
package/src/commands/registry.js
CHANGED
|
@@ -35,7 +35,7 @@ const FLAG_HELP = {
|
|
|
35
35
|
const flag = (name) => ({ name, kind: 'boolean', description: FLAG_HELP[name] });
|
|
36
36
|
const valueFlag = (name, valueName) => ({ name, kind: 'value', valueName, description: FLAG_HELP[name] });
|
|
37
37
|
|
|
38
|
-
const
|
|
38
|
+
const HOST_TOKENS = `${hostRegistry.platformIds.join('|')}|all`;
|
|
39
39
|
const HOST_NOTE = `Hosts: ${Object.entries(HOST_COMPONENTS)
|
|
40
40
|
.map(([id, host]) => `${id} (${Object.keys(host.components).join(', ')})`)
|
|
41
41
|
.join('; ')}.`;
|
|
@@ -51,18 +51,21 @@ export const COMMANDS = [
|
|
|
51
51
|
{
|
|
52
52
|
path: ['project', 'init'],
|
|
53
53
|
summary: 'Copy the cowork-flow template into a project',
|
|
54
|
-
usage: `project init [target] --
|
|
54
|
+
usage: `project init [target] --host <${HOST_TOKENS}> [--developer <name>] [--language <zh-CN|en-US>] [--dry-run] [--force]`,
|
|
55
55
|
flags: [
|
|
56
|
-
{ name: '--
|
|
57
|
-
{ name: '--
|
|
58
|
-
{ name: '--
|
|
56
|
+
{ name: '--host', kind: 'value', valueName: 'host', repeatable: true, description: 'Host to set the project up for; repeatable and comma-separated, auto-detected when omitted' },
|
|
57
|
+
{ name: '--platform', kind: 'value', valueName: 'host', repeatable: true, description: 'Alias of --host' },
|
|
58
|
+
{ name: '--platforms', kind: 'value', valueName: 'host', repeatable: true, description: 'Alias of --host' },
|
|
59
|
+
{ name: '--developer', kind: 'value', valueName: 'name', description: 'Developer name recorded in .cowork-flow/.developer; falls back to git user.name' },
|
|
60
|
+
{ name: '--language', kind: 'value', valueName: 'language', description: 'Output language for task and plan artifacts; accepts zh-CN or en-US (default: zh-CN)' },
|
|
59
61
|
flag('--dry-run'),
|
|
60
62
|
flag('--force')
|
|
61
63
|
],
|
|
62
64
|
positional: { min: 0, max: 1, name: '[target]' },
|
|
63
65
|
examples: [
|
|
64
|
-
'cwf project init
|
|
65
|
-
'cwf project init
|
|
66
|
+
'cwf project init --host codex --developer alice',
|
|
67
|
+
'cwf project init --host codex --developer alice --language en-US',
|
|
68
|
+
'cwf project init ./my-project --host all --dry-run'
|
|
66
69
|
],
|
|
67
70
|
run: (args, context) => runInit(args, context)
|
|
68
71
|
},
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
formatReadinessReport,
|
|
10
10
|
summarizePlan
|
|
11
11
|
} from '../lib/copy-template.js';
|
|
12
|
+
import { readProjectLanguageStrict } from '../lib/language.js';
|
|
12
13
|
|
|
13
14
|
import { parseFlags } from '../lib/cli-flags.js';
|
|
14
15
|
|
|
@@ -25,7 +26,10 @@ function parseSourceRefreshArgs(args) {
|
|
|
25
26
|
|
|
26
27
|
export async function runSourceRefresh(args, { io, fileSystem } = {}) {
|
|
27
28
|
const options = parseSourceRefreshArgs(args);
|
|
28
|
-
const
|
|
29
|
+
const language = await readProjectLanguageStrict(options.target);
|
|
30
|
+
const plan = await buildSourceCheckoutRefreshPlan(options.target, {
|
|
31
|
+
language: language.language
|
|
32
|
+
});
|
|
29
33
|
|
|
30
34
|
await applyPlan(plan, { dryRun: options.dryRun, fileSystem });
|
|
31
35
|
io.writeOut(summarizePlan(plan, options.dryRun));
|
package/src/commands/sync.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '../lib/copy-template.js';
|
|
13
13
|
import { readPackageInfo } from '../lib/package-info.js';
|
|
14
14
|
import { formatPlatformList } from '../lib/platforms.js';
|
|
15
|
+
import { readProjectLanguageStrict } from '../lib/language.js';
|
|
15
16
|
import { parseFlags } from '../lib/cli-flags.js';
|
|
16
17
|
|
|
17
18
|
function parseSyncArgs(args) {
|
|
@@ -29,9 +30,11 @@ function parseSyncArgs(args) {
|
|
|
29
30
|
export async function runSync(args, { io, fileSystem }) {
|
|
30
31
|
const options = parseSyncArgs(args);
|
|
31
32
|
const packageInfo = await readPackageInfo();
|
|
33
|
+
const language = await readProjectLanguageStrict(options.target);
|
|
32
34
|
const platforms = await detectInstalledPlatforms(options.target);
|
|
33
35
|
const plan = await buildSyncPlan(options.target, {
|
|
34
36
|
force: options.force,
|
|
37
|
+
language: language.language,
|
|
35
38
|
platforms,
|
|
36
39
|
version: packageInfo.version
|
|
37
40
|
});
|
|
@@ -43,6 +46,6 @@ export async function runSync(args, { io, fileSystem }) {
|
|
|
43
46
|
const readiness = await buildReadinessReport(plan, { fileSystem });
|
|
44
47
|
io.writeOut(formatReadinessReport(readiness));
|
|
45
48
|
}
|
|
46
|
-
io.writeOut(`
|
|
49
|
+
io.writeOut(`Hosts: ${platforms.length > 0 ? formatPlatformList(platforms) : 'none'}\n`);
|
|
47
50
|
return 0;
|
|
48
51
|
}
|
package/src/lib/copy-template.js
CHANGED
|
@@ -153,13 +153,17 @@ async function appendSkillReadRootActions(actions, {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// The source checkout's `.cowork-flow/spec` tree is a mirror of the template,
|
|
157
|
+
// not a downstream project's spec tree: project-owned directories are framework
|
|
158
|
+
// content here too, so dev refresh rewrites every spec directory and keeps only
|
|
159
|
+
// task/plan state and the project config protected.
|
|
156
160
|
function isSourceCheckoutProtected(relativePath) {
|
|
157
161
|
const normalized = toTemplatePath(relativePath);
|
|
158
162
|
if (hostRegistry.syncPolicy.protectedFiles.includes(normalized)) {
|
|
159
163
|
return true;
|
|
160
164
|
}
|
|
161
165
|
return hostRegistry.syncPolicy.protectedPrefixes
|
|
162
|
-
.filter((prefix) => prefix
|
|
166
|
+
.filter((prefix) => !prefix.startsWith('.cowork-flow/spec/'))
|
|
163
167
|
.some((prefix) => normalized.startsWith(prefix))
|
|
164
168
|
|| normalized.startsWith('.cowork-flow/.runtime/');
|
|
165
169
|
}
|
|
@@ -196,12 +200,16 @@ export async function buildInitPlan(targetDir, options = {}) {
|
|
|
196
200
|
continue;
|
|
197
201
|
}
|
|
198
202
|
|
|
199
|
-
const source = join(
|
|
203
|
+
const source = join(
|
|
204
|
+
templateRoot,
|
|
205
|
+
...hostRegistry.localizedSource(templatePath, options.language).split('/')
|
|
206
|
+
);
|
|
200
207
|
const destination = join(targetDir, file);
|
|
201
208
|
seen.add(destination);
|
|
202
209
|
const exists = await pathExists(destination);
|
|
210
|
+
const forceProtected = hostRegistry.isForceProtectedSyncFile(templatePath);
|
|
203
211
|
const action = exists
|
|
204
|
-
? (options.force && templatePath !== '.cowork-flow/.developer' ? 'update' : 'skip')
|
|
212
|
+
? (forceProtected ? 'protected' : (options.force && templatePath !== '.cowork-flow/.developer' ? 'update' : 'skip'))
|
|
205
213
|
: 'create';
|
|
206
214
|
actions.push({ action, source, destination, relativePath: file });
|
|
207
215
|
}
|
|
@@ -448,7 +456,10 @@ export async function buildSourceCheckoutRefreshPlan(targetDir, options = {}) {
|
|
|
448
456
|
if (!shouldIncludeSourceCheckoutRuntimeFile(templatePath)) {
|
|
449
457
|
continue;
|
|
450
458
|
}
|
|
451
|
-
const source = join(
|
|
459
|
+
const source = join(
|
|
460
|
+
templateRoot,
|
|
461
|
+
...hostRegistry.localizedSource(templatePath, options.language).split('/')
|
|
462
|
+
);
|
|
452
463
|
const destination = join(targetDir, file);
|
|
453
464
|
seen.add(destination);
|
|
454
465
|
const exists = await pathExists(destination);
|
|
@@ -508,12 +519,19 @@ async function planSyncFile(file, { targetDir, options, platforms, seen }) {
|
|
|
508
519
|
return null;
|
|
509
520
|
}
|
|
510
521
|
|
|
511
|
-
const source = join(
|
|
522
|
+
const source = join(
|
|
523
|
+
templateRoot,
|
|
524
|
+
...hostRegistry.localizedSource(file, options.language).split('/')
|
|
525
|
+
);
|
|
512
526
|
const exists = await pathExists(destination);
|
|
513
527
|
if (hostRegistry.isManagedBlockFile(file)) {
|
|
514
528
|
return await buildManagedBlockSyncAction({ source, destination, exists, options, relativePath: file });
|
|
515
529
|
}
|
|
516
530
|
|
|
531
|
+
if (exists && hostRegistry.isForceProtectedSyncFile(file)) {
|
|
532
|
+
return { action: 'protected', source, destination, relativePath: file };
|
|
533
|
+
}
|
|
534
|
+
|
|
517
535
|
const protectedFile = hostRegistry.isProtectedSyncFile(file) && !options.force && exists;
|
|
518
536
|
const safeFile = hostRegistry.isSafeSyncFile(file);
|
|
519
537
|
|