cowork-flow 2.3.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 +18 -1
- package/README.en.md +34 -18
- package/README.md +8 -5
- 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 +3 -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 +4 -0
- package/presets/codex/.codex-plugin/plugin.json +1 -1
- package/presets/codex/skills/cowork-flow-bootstrap/SKILL.md +4 -0
- package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
- package/presets/kimi-code/skills/cowork-flow-bootstrap/SKILL.md +4 -0
- package/presets/opencode/cowork-flow/skills/cowork-flow-bootstrap/SKILL.md +4 -0
- package/presets/qoder/.qoder-plugin/plugin.json +1 -1
- package/presets/qoder/skills/cowork-flow-bootstrap/SKILL.md +4 -0
- package/presets/zcode/.zcode-plugin/plugin.json +1 -1
- package/presets/zcode/skills/cowork-flow-bootstrap/SKILL.md +4 -0
- package/src/commands/init.js +129 -2
- package/src/commands/registry.js +3 -1
- package/src/commands/source-refresh.js +5 -1
- package/src/commands/sync.js +3 -0
- package/src/lib/copy-template.js +23 -5
- package/src/lib/host-assets.js +117 -2
- 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
package/src/commands/init.js
CHANGED
|
@@ -17,6 +17,14 @@ import {
|
|
|
17
17
|
platformLabel
|
|
18
18
|
} from '../lib/platforms.js';
|
|
19
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';
|
|
20
28
|
|
|
21
29
|
const execFileAsync = promisify(execFile);
|
|
22
30
|
|
|
@@ -34,7 +42,7 @@ function parsePlatformSelectionOrUsage(values) {
|
|
|
34
42
|
function parseInitArgs(args) {
|
|
35
43
|
const { flags, positionals } = parseFlags(args, {
|
|
36
44
|
boolean: ['--dry-run', '--force'],
|
|
37
|
-
value: ['--host', '--platform', '--platforms', '--developer'],
|
|
45
|
+
value: ['--host', '--platform', '--platforms', '--developer', '--language'],
|
|
38
46
|
repeatable: ['--host', '--platform', '--platforms'],
|
|
39
47
|
positional: { min: 0, max: 1, name: '[target]' }
|
|
40
48
|
});
|
|
@@ -42,6 +50,7 @@ function parseInitArgs(args) {
|
|
|
42
50
|
developer: flags['--developer'] ?? null,
|
|
43
51
|
dryRun: Boolean(flags['--dry-run']),
|
|
44
52
|
force: Boolean(flags['--force']),
|
|
53
|
+
language: flags['--language'] ?? null,
|
|
45
54
|
platforms: [
|
|
46
55
|
...(flags['--host'] ?? []),
|
|
47
56
|
...(flags['--platform'] ?? []),
|
|
@@ -165,6 +174,104 @@ async function resolvePlatforms(options, selectPlatforms) {
|
|
|
165
174
|
return parsePlatformSelectionOrUsage(selected);
|
|
166
175
|
}
|
|
167
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
|
+
|
|
168
275
|
|
|
169
276
|
async function buildDeveloperActions(target, developer) {
|
|
170
277
|
const workflowDir = join(target, '.cowork-flow');
|
|
@@ -195,14 +302,22 @@ export async function runInitWithOptions(
|
|
|
195
302
|
) {
|
|
196
303
|
const options = parseInitArgs(args);
|
|
197
304
|
const platforms = await resolvePlatforms(options, selectPlatforms);
|
|
305
|
+
const language = await resolveLanguage(options, prompt);
|
|
306
|
+
emitLanguageWarnings(io, language);
|
|
198
307
|
const developer = await resolveDeveloperName(options, prompt);
|
|
199
308
|
const packageInfo = await readPackageInfo();
|
|
200
309
|
const developerActions = developer.name
|
|
201
310
|
? await buildDeveloperActions(options.target, developer)
|
|
202
311
|
: [];
|
|
312
|
+
const configActions = await buildConfigActions(
|
|
313
|
+
options.target,
|
|
314
|
+
language.language,
|
|
315
|
+
language.existing
|
|
316
|
+
);
|
|
203
317
|
const plan = await buildInitPlan(options.target, {
|
|
204
|
-
additionalActions: developerActions,
|
|
318
|
+
additionalActions: [...developerActions, ...configActions],
|
|
205
319
|
force: options.force,
|
|
320
|
+
language: language.language,
|
|
206
321
|
platforms,
|
|
207
322
|
version: packageInfo.version
|
|
208
323
|
});
|
|
@@ -220,6 +335,11 @@ export async function runInitWithOptions(
|
|
|
220
335
|
} else {
|
|
221
336
|
io.writeOut('dry-run would-skip=.cowork-flow/.developer (no developer identity; pass --developer)\n');
|
|
222
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`);
|
|
342
|
+
}
|
|
223
343
|
io.writeOut('Next: update AGENTS.md and .cowork-flow/config.yaml for this project.\n');
|
|
224
344
|
return 0;
|
|
225
345
|
}
|
|
@@ -230,6 +350,13 @@ export async function runInitWithOptions(
|
|
|
230
350
|
const source = developer.source === 'git' ? ' (from git user.name)' : '';
|
|
231
351
|
io.writeOut(`Developer initialized: ${developer.name}${source}\n`);
|
|
232
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`);
|
|
359
|
+
}
|
|
233
360
|
io.writeOut('Next: update AGENTS.md and .cowork-flow/config.yaml for this project.\n');
|
|
234
361
|
return 0;
|
|
235
362
|
}
|
package/src/commands/registry.js
CHANGED
|
@@ -51,18 +51,20 @@ 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] --host <${HOST_TOKENS}> [--developer <name>] [--dry-run] [--force]`,
|
|
54
|
+
usage: `project init [target] --host <${HOST_TOKENS}> [--developer <name>] [--language <zh-CN|en-US>] [--dry-run] [--force]`,
|
|
55
55
|
flags: [
|
|
56
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
57
|
{ name: '--platform', kind: 'value', valueName: 'host', repeatable: true, description: 'Alias of --host' },
|
|
58
58
|
{ name: '--platforms', kind: 'value', valueName: 'host', repeatable: true, description: 'Alias of --host' },
|
|
59
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)' },
|
|
60
61
|
flag('--dry-run'),
|
|
61
62
|
flag('--force')
|
|
62
63
|
],
|
|
63
64
|
positional: { min: 0, max: 1, name: '[target]' },
|
|
64
65
|
examples: [
|
|
65
66
|
'cwf project init --host codex --developer alice',
|
|
67
|
+
'cwf project init --host codex --developer alice --language en-US',
|
|
66
68
|
'cwf project init ./my-project --host all --dry-run'
|
|
67
69
|
],
|
|
68
70
|
run: (args, context) => runInit(args, context)
|
|
@@ -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
|
});
|
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
|
|
package/src/lib/host-assets.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { packageRoot, templateRoot } from './paths.js';
|
|
@@ -37,6 +37,7 @@ const MANIFEST_KEYS = [
|
|
|
37
37
|
'capabilityMatrix',
|
|
38
38
|
'platforms',
|
|
39
39
|
'excludedPrefixes',
|
|
40
|
+
'localization',
|
|
40
41
|
'syncPolicy'
|
|
41
42
|
];
|
|
42
43
|
const PLATFORM_KEYS = [
|
|
@@ -57,6 +58,7 @@ const SKILL_DISCOVERY_KEYS = ['scope', 'evidence', 'path', 'gates'];
|
|
|
57
58
|
const SKILL_DISCOVERY_SCOPES = ['project'];
|
|
58
59
|
const SYNC_POLICY_KEYS = [
|
|
59
60
|
'protectedFiles',
|
|
61
|
+
'forceProtectedFiles',
|
|
60
62
|
'protectedPrefixes',
|
|
61
63
|
'safeFiles',
|
|
62
64
|
'safePrefixes',
|
|
@@ -101,6 +103,7 @@ function aliasIndex(platforms) {
|
|
|
101
103
|
function normalizedSyncPolicy(policy) {
|
|
102
104
|
return {
|
|
103
105
|
protectedFiles: policy.protectedFiles.map(normalizePath),
|
|
106
|
+
forceProtectedFiles: (policy.forceProtectedFiles ?? []).map(normalizePath),
|
|
104
107
|
protectedPrefixes: policy.protectedPrefixes.map(normalizePath),
|
|
105
108
|
safeFiles: policy.safeFiles.map(normalizePath),
|
|
106
109
|
safePrefixes: policy.safePrefixes.map(normalizePath),
|
|
@@ -110,6 +113,27 @@ function normalizedSyncPolicy(policy) {
|
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
|
|
116
|
+
// A manifest without localization keeps every asset in the base template, which
|
|
117
|
+
// is exactly the pre-localization behaviour.
|
|
118
|
+
function normalizedLocalization(localization) {
|
|
119
|
+
const raw = localization ?? {
|
|
120
|
+
default: 'zh-CN',
|
|
121
|
+
supported: ['zh-CN', 'en-US'],
|
|
122
|
+
assets: {}
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
default: raw.default,
|
|
126
|
+
supported: [...raw.supported],
|
|
127
|
+
assets: Object.fromEntries(
|
|
128
|
+
Object.entries(raw.assets).map(([language, assets]) => [
|
|
129
|
+
language,
|
|
130
|
+
assets.map(normalizePath)
|
|
131
|
+
])
|
|
132
|
+
)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
113
137
|
// Every lookup a registry closes over, built after the
|
|
114
138
|
// manifest passes validation. The order here is the order
|
|
115
139
|
// init/sync observe.
|
|
@@ -120,6 +144,7 @@ function buildHostIndex(manifest) {
|
|
|
120
144
|
const aliases = aliasIndex(platforms);
|
|
121
145
|
const platformIds = platforms.map((platform) => platform.id);
|
|
122
146
|
const capabilityMatrix = normalizeCapabilityMatrix(manifest.capabilityMatrix);
|
|
147
|
+
const localization = normalizedLocalization(manifest.localization);
|
|
123
148
|
const syncPolicy = normalizedSyncPolicy(manifest.syncPolicy);
|
|
124
149
|
const assetPrefixes = unique(
|
|
125
150
|
platforms.flatMap((platform) => platform.assetPrefixes)
|
|
@@ -140,10 +165,12 @@ function buildHostIndex(manifest) {
|
|
|
140
165
|
aliases,
|
|
141
166
|
platformIds,
|
|
142
167
|
capabilityMatrix,
|
|
168
|
+
localization,
|
|
143
169
|
syncPolicy,
|
|
144
170
|
assetPrefixes,
|
|
145
171
|
skillReadRoots,
|
|
146
172
|
protectedFiles: new Set(syncPolicy.protectedFiles),
|
|
173
|
+
forceProtectedFiles: new Set(syncPolicy.forceProtectedFiles),
|
|
147
174
|
safeFiles: new Set(syncPolicy.safeFiles),
|
|
148
175
|
managedBlockFiles: new Set(syncPolicy.managedBlockFiles)
|
|
149
176
|
};
|
|
@@ -226,6 +253,7 @@ function syncRules(index) {
|
|
|
226
253
|
const {
|
|
227
254
|
syncPolicy,
|
|
228
255
|
protectedFiles,
|
|
256
|
+
forceProtectedFiles,
|
|
229
257
|
safeFiles,
|
|
230
258
|
managedBlockFiles,
|
|
231
259
|
assetPrefixes,
|
|
@@ -250,6 +278,12 @@ function syncRules(index) {
|
|
|
250
278
|
|| syncPolicy.protectedPrefixes.some((prefix) => normalized.startsWith(prefix));
|
|
251
279
|
}
|
|
252
280
|
|
|
281
|
+
// A force-protected file is created once and never replaced, even by
|
|
282
|
+
// `init --force` or `sync --force`.
|
|
283
|
+
function isForceProtectedSyncFile(relativePath) {
|
|
284
|
+
return forceProtectedFiles.has(normalizePath(relativePath));
|
|
285
|
+
}
|
|
286
|
+
|
|
253
287
|
function isManagedBlockFile(relativePath) {
|
|
254
288
|
return managedBlockFiles.has(normalizePath(relativePath));
|
|
255
289
|
}
|
|
@@ -267,7 +301,13 @@ function syncRules(index) {
|
|
|
267
301
|
});
|
|
268
302
|
}
|
|
269
303
|
|
|
270
|
-
return {
|
|
304
|
+
return {
|
|
305
|
+
isSafeSyncFile,
|
|
306
|
+
isProtectedSyncFile,
|
|
307
|
+
isForceProtectedSyncFile,
|
|
308
|
+
isManagedBlockFile,
|
|
309
|
+
obsoleteSyncFiles
|
|
310
|
+
};
|
|
271
311
|
}
|
|
272
312
|
|
|
273
313
|
|
|
@@ -298,6 +338,7 @@ export function createHostRegistry(manifest) {
|
|
|
298
338
|
platforms: index.platforms,
|
|
299
339
|
platformIds: index.platformIds,
|
|
300
340
|
capabilityMatrix: index.capabilityMatrix,
|
|
341
|
+
localization: index.localization,
|
|
301
342
|
syncPolicy: index.syncPolicy,
|
|
302
343
|
assetPrefixes: index.assetPrefixes,
|
|
303
344
|
skillReadRoots: index.skillReadRoots,
|
|
@@ -323,6 +364,27 @@ export function createHostRegistry(manifest) {
|
|
|
323
364
|
skillDestination(platformId) {
|
|
324
365
|
return index.byId.get(platformId)?.skillReadRoot ?? null;
|
|
325
366
|
},
|
|
367
|
+
// Canonical project-relative path for the asset's source. Non-default
|
|
368
|
+
// languages resolve only declared assets; everything else stays on the
|
|
369
|
+
// base template.
|
|
370
|
+
localizedSource(relativePath, language) {
|
|
371
|
+
const normalized = normalizePath(relativePath);
|
|
372
|
+
const selected = language ?? index.localization.default;
|
|
373
|
+
if (!index.localization.supported.includes(selected)) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`Unsupported language: ${selected}. Supported: ${index.localization.supported.join(', ')}`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const assets = index.localization.assets[selected] ?? [];
|
|
379
|
+
if (!assets.includes(normalized)) {
|
|
380
|
+
return normalized;
|
|
381
|
+
}
|
|
382
|
+
const localized = `locales/${selected}/${normalized}`;
|
|
383
|
+
if (!existsSync(join(templateRoot, ...localized.split('/')))) {
|
|
384
|
+
throw new Error(`Missing localized asset: ${localized}`);
|
|
385
|
+
}
|
|
386
|
+
return localized;
|
|
387
|
+
},
|
|
326
388
|
// A declaration names a directory inside the package;
|
|
327
389
|
// the absolute source dir is resolved once, here.
|
|
328
390
|
platformPayload(platformId) {
|
|
@@ -337,6 +399,7 @@ export function createHostRegistry(manifest) {
|
|
|
337
399
|
},
|
|
338
400
|
isSafeSyncFile: sync.isSafeSyncFile,
|
|
339
401
|
isProtectedSyncFile: sync.isProtectedSyncFile,
|
|
402
|
+
isForceProtectedSyncFile: sync.isForceProtectedSyncFile,
|
|
340
403
|
isManagedBlockFile: sync.isManagedBlockFile,
|
|
341
404
|
obsoleteSyncFiles: sync.obsoleteSyncFiles
|
|
342
405
|
};
|
|
@@ -378,10 +441,59 @@ function validateManifest(manifest) {
|
|
|
378
441
|
manifest.excludedPrefixes,
|
|
379
442
|
'Host Asset Manifest excludedPrefixes'
|
|
380
443
|
);
|
|
444
|
+
validateLocalization(manifest.localization);
|
|
381
445
|
validateCapabilityMatrix(manifest.capabilityMatrix, allowed, platformIds);
|
|
382
446
|
validateSyncPolicy(manifest.syncPolicy);
|
|
383
447
|
}
|
|
384
448
|
|
|
449
|
+
function validateLocalization(localization) {
|
|
450
|
+
if (localization === undefined) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (!localization || typeof localization !== 'object' || Array.isArray(localization)) {
|
|
454
|
+
throw new Error('Host Asset Manifest localization must be an object');
|
|
455
|
+
}
|
|
456
|
+
assertKnownKeys(
|
|
457
|
+
localization,
|
|
458
|
+
['default', 'supported', 'assets'],
|
|
459
|
+
'Host Asset Manifest localization'
|
|
460
|
+
);
|
|
461
|
+
validateRequiredString(
|
|
462
|
+
localization.default,
|
|
463
|
+
'Host Asset Manifest localization.default'
|
|
464
|
+
);
|
|
465
|
+
validateStringArray(
|
|
466
|
+
localization.supported,
|
|
467
|
+
'Host Asset Manifest localization.supported',
|
|
468
|
+
{ unique: true }
|
|
469
|
+
);
|
|
470
|
+
if (!localization.supported.includes(localization.default)) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
'Host Asset Manifest localization.default must be listed in localization.supported'
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
if (!localization.assets || typeof localization.assets !== 'object' || Array.isArray(localization.assets)) {
|
|
476
|
+
throw new Error('Host Asset Manifest localization.assets must be an object');
|
|
477
|
+
}
|
|
478
|
+
for (const [language, assets] of Object.entries(localization.assets)) {
|
|
479
|
+
if (!localization.supported.includes(language)) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
`Host Asset Manifest localization.assets language is not supported: ${language}`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (language === localization.default) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
'Host Asset Manifest localization.assets must not override the default language'
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
validateStringArray(
|
|
490
|
+
assets,
|
|
491
|
+
`Host Asset Manifest localization.assets.${language}`,
|
|
492
|
+
{ unique: true }
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
385
497
|
function validateCapabilityValues(values) {
|
|
386
498
|
validateStringArray(
|
|
387
499
|
values,
|
|
@@ -515,6 +627,9 @@ function validateSyncPolicy(syncPolicy) {
|
|
|
515
627
|
}
|
|
516
628
|
assertKnownKeys(syncPolicy, SYNC_POLICY_KEYS, 'Host Asset Manifest syncPolicy');
|
|
517
629
|
for (const key of SYNC_POLICY_KEYS) {
|
|
630
|
+
if (key === 'forceProtectedFiles' && syncPolicy[key] === undefined) {
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
518
633
|
validateStringArray(
|
|
519
634
|
syncPolicy[key],
|
|
520
635
|
`Host Asset Manifest syncPolicy.${key}`
|