cowork-flow 0.0.37 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -14
- package/package.json +1 -1
- package/src/commands/init.js +39 -28
- package/src/commands/sync.js +2 -2
- package/src/lib/asset-plan.js +127 -0
- package/src/lib/copy-template.js +101 -141
- package/src/lib/host-assets.js +231 -0
- package/src/lib/plan-applier.js +329 -0
- package/src/lib/platforms.js +15 -76
- package/src/lib/skill-registry.js +503 -0
- package/template/.claude/agents/cowork-check.md +10 -1
- package/template/.claude/agents/cowork-implement.md +11 -2
- package/template/.claude/hooks/inject-workflow-state.py +51 -254
- package/template/.codex/agents/cowork-check.toml +12 -3
- package/template/.codex/agents/cowork-implement.toml +11 -2
- package/template/.codex/agents/default.toml +3 -2
- package/template/.codex/agents/explorer.toml +3 -2
- package/template/.codex/agents/worker.toml +2 -1
- package/template/.codex/hooks/inject-workflow-state.py +46 -265
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/application/__init__.py +1 -0
- package/template/.cowork-flow/scripts/application/batch_execution.py +743 -0
- package/template/.cowork-flow/scripts/application/runtime_context_service.py +365 -0
- package/template/.cowork-flow/scripts/application/task_archive.py +220 -0
- package/template/.cowork-flow/scripts/application/task_context.py +765 -0
- package/template/.cowork-flow/scripts/application/task_creation.py +201 -0
- package/template/.cowork-flow/scripts/application/task_lifecycle.py +532 -0
- package/template/.cowork-flow/scripts/application/task_tree.py +179 -0
- package/template/.cowork-flow/scripts/commands/doctor.py +78 -57
- package/template/.cowork-flow/scripts/commands/party_mode_v2.py +671 -425
- package/template/.cowork-flow/scripts/commands/subagent.py +38 -26
- package/template/.cowork-flow/scripts/commands/task.py +535 -1953
- package/template/.cowork-flow/scripts/commands/task_archive_commands.py +195 -0
- package/template/.cowork-flow/scripts/commands/task_context_commands.py +240 -0
- package/template/.cowork-flow/scripts/commands/task_create_command.py +164 -0
- package/template/.cowork-flow/scripts/commands/task_navigation.py +407 -0
- package/template/.cowork-flow/scripts/commands/task_parser.py +364 -0
- package/template/.cowork-flow/scripts/commands/task_support.py +76 -0
- package/template/.cowork-flow/scripts/commands/task_tree_commands.py +160 -0
- package/template/.cowork-flow/scripts/common/core/execution_context.py +161 -85
- package/template/.cowork-flow/scripts/common/core/host_manifest.py +393 -0
- package/template/.cowork-flow/scripts/common/core/skill_registry.py +568 -0
- package/template/.cowork-flow/scripts/common/gates/coding_standards.py +58 -7
- package/template/.cowork-flow/scripts/common/gates/gates.py +323 -46
- package/template/.cowork-flow/scripts/common/gates/models.py +196 -0
- package/template/.cowork-flow/scripts/common/gates/registry.py +79 -0
- package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +13 -13
- package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +104 -62
- package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +39 -22
- package/template/.cowork-flow/scripts/common/gates/validate_rules.py +223 -40
- package/template/.cowork-flow/scripts/common/git/git_context.py +216 -182
- package/template/.cowork-flow/scripts/common/host/__init__.py +1 -0
- package/template/.cowork-flow/scripts/common/host/workflow_state_hook.py +303 -0
- package/template/.cowork-flow/scripts/common/storage/__init__.py +1 -0
- package/template/.cowork-flow/scripts/common/storage/operation_log.py +72 -0
- package/template/.cowork-flow/scripts/common/storage/state_store.py +279 -0
- package/template/.cowork-flow/scripts/common/storage/unit_of_work.py +214 -0
- package/template/.cowork-flow/scripts/common/task/active_task.py +93 -135
- package/template/.cowork-flow/scripts/common/task/batch_mode.py +33 -120
- package/template/.cowork-flow/scripts/common/task/decision_review.py +85 -0
- package/template/.cowork-flow/scripts/common/task/readiness.py +9 -15
- package/template/.cowork-flow/scripts/common/task/task_repository.py +156 -0
- package/template/.cowork-flow/scripts/run.py +24 -0
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
- package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
- package/template/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
- package/template/.cowork-flow/spec/protocols/decision-review.md +28 -0
- package/template/.cowork-flow/spec/protocols/review.md +34 -0
- package/template/.cowork-flow/spec/protocols/spec-maintenance.md +16 -0
- package/template/.cowork-flow/spec/protocols/tdd.md +30 -0
- package/template/.cowork-flow/spec/references/definition-of-done.md +4 -4
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
- package/template/.cowork-flow/spec/runtime/contract-registry.json +25 -0
- package/template/.cowork-flow/spec/runtime/host-assets.json +214 -0
- package/template/.cowork-flow/spec/runtime/index.md +8 -0
- package/template/.cowork-flow/spec/runtime/rules.json +61 -10
- package/template/.cowork-flow/spec/runtime/skill-registry.json +397 -0
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +173 -0
- package/template/.cowork-flow/spec/schemas/rules.schema.json +10 -1
- package/template/.cowork-flow/spec/schemas/skill-registry.schema.json +202 -0
- package/template/.cowork-flow/workflow.md +18 -13
- package/template/.opencode/agents/cowork-check.md +11 -0
- package/template/.opencode/agents/cowork-implement.md +16 -1
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/hooks/inject-context.js +3 -85
- package/template/.zcode/hooks/inject-context.selfcheck.mjs +6 -6
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
- package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +61 -10
- package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +10 -1
- package/template/.zcode/scaffold/.cowork-flow/workflow.md +18 -13
- package/template/.zcode/scaffold/AGENTS.md +18 -8
- package/template/AGENTS.md +1 -1
- package/template/skills/batch-mode/SKILL.md +140 -76
- package/template/skills/brainstorming/SKILL.md +2 -2
- package/template/skills/cowork-flow/SKILL.md +42 -0
- package/template/skills/party-mode/SHARED-BOUNDARIES.md +1 -1
- package/template/skills/party-mode/SKILL.md +48 -119
- package/template/.cowork-flow/scripts/common/gates/spec_validator.py +0 -53
- package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +0 -32
- package/template/skills/before-dev/SKILL.md +0 -41
- package/template/skills/check/SKILL.md +0 -53
- package/template/skills/check/simplification-guide.md +0 -47
- package/template/skills/continue/SKILL.md +0 -29
- package/template/skills/doubt-review/SKILL.md +0 -118
- package/template/skills/finish-work/SKILL.md +0 -32
- package/template/skills/party-mode-v2/SKILL.md +0 -75
- package/template/skills/start/SKILL.md +0 -78
- package/template/skills/tdd/SKILL.md +0 -74
- package/template/skills/update-spec/SKILL.md +0 -39
- package/template/skills/using-cowork-flow/SKILL.md +0 -80
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { templateRoot } from './paths.js';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const hostAssetManifestPath = join(
|
|
8
|
+
templateRoot,
|
|
9
|
+
'.cowork-flow',
|
|
10
|
+
'spec',
|
|
11
|
+
'runtime',
|
|
12
|
+
'host-assets.json'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export function loadHostAssetManifest(path = hostAssetManifestPath) {
|
|
17
|
+
const manifest = JSON.parse(readFileSync(path, 'utf8'));
|
|
18
|
+
validateManifest(manifest);
|
|
19
|
+
return manifest;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export function createHostRegistry(manifest) {
|
|
24
|
+
validateManifest(manifest);
|
|
25
|
+
const platforms = manifest.platforms.map((platform) => ({
|
|
26
|
+
...platform,
|
|
27
|
+
aliases: [...platform.aliases],
|
|
28
|
+
detectAny: [...platform.detectAny],
|
|
29
|
+
assetPrefixes: platform.assetPrefixes.map(normalizePath),
|
|
30
|
+
assetFiles: platform.assetFiles.map(normalizePath)
|
|
31
|
+
}));
|
|
32
|
+
const byId = new Map(platforms.map((platform) => [platform.id, platform]));
|
|
33
|
+
const aliases = new Map();
|
|
34
|
+
for (const platform of platforms) {
|
|
35
|
+
for (const alias of platform.aliases) {
|
|
36
|
+
const normalized = alias.toLowerCase();
|
|
37
|
+
if (aliases.has(normalized)) {
|
|
38
|
+
throw new Error(`Duplicate platform alias: ${alias}`);
|
|
39
|
+
}
|
|
40
|
+
aliases.set(normalized, platform.id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const platformIds = platforms.map((platform) => platform.id);
|
|
44
|
+
const syncPolicy = {
|
|
45
|
+
protectedFiles: manifest.syncPolicy.protectedFiles.map(normalizePath),
|
|
46
|
+
protectedPrefixes: manifest.syncPolicy.protectedPrefixes.map(normalizePath),
|
|
47
|
+
safeFiles: manifest.syncPolicy.safeFiles.map(normalizePath),
|
|
48
|
+
safePrefixes: manifest.syncPolicy.safePrefixes.map(normalizePath),
|
|
49
|
+
managedBlockFiles: manifest.syncPolicy.managedBlockFiles.map(normalizePath),
|
|
50
|
+
obsoleteFiles: manifest.syncPolicy.obsoleteFiles.map(normalizePath)
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function parsePlatformSelection(values) {
|
|
54
|
+
const rawValues = Array.isArray(values) ? values : [values];
|
|
55
|
+
const selected = new Set();
|
|
56
|
+
for (const rawValue of rawValues) {
|
|
57
|
+
const tokens = String(rawValue ?? '')
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.split(/[\s,;|+/]+/)
|
|
60
|
+
.map((token) => token.trim())
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
for (const token of tokens) {
|
|
63
|
+
if (token === 'all') {
|
|
64
|
+
for (const platformId of platformIds) {
|
|
65
|
+
selected.add(platformId);
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const platformId = aliases.get(token);
|
|
70
|
+
if (!platformId) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Unsupported platform: ${token}. Supported platforms: ${platformIds.join(', ')}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
selected.add(platformId);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (selected.size === 0) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Platform selection required. Run: cowork-flow init <target> --platform ${platformIds.join('|')}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return platformIds.filter((platformId) => selected.has(platformId));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function shouldInclude(relativePath, selectedPlatforms) {
|
|
87
|
+
const normalized = normalizePath(relativePath);
|
|
88
|
+
if (
|
|
89
|
+
manifest.excludedPrefixes.some(
|
|
90
|
+
(prefix) => normalized.startsWith(normalizePath(prefix))
|
|
91
|
+
)
|
|
92
|
+
) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const owners = platforms.filter((platform) => (
|
|
96
|
+
platform.assetFiles.includes(normalized)
|
|
97
|
+
|| platform.assetPrefixes.some(
|
|
98
|
+
(prefix) => normalized.startsWith(prefix)
|
|
99
|
+
)
|
|
100
|
+
));
|
|
101
|
+
if (owners.length === 0) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return owners.some(
|
|
105
|
+
(platform) => selectedPlatforms.includes(platform.id)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function detectInstalledPlatforms(targetDir, pathExists) {
|
|
110
|
+
const installed = [];
|
|
111
|
+
for (const platform of platforms) {
|
|
112
|
+
for (const marker of platform.detectAny) {
|
|
113
|
+
if (await pathExists(join(targetDir, marker))) {
|
|
114
|
+
installed.push(platform.id);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return installed;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
manifest,
|
|
124
|
+
platforms,
|
|
125
|
+
platformIds,
|
|
126
|
+
syncPolicy,
|
|
127
|
+
assetPrefixes: unique(
|
|
128
|
+
platforms.flatMap((platform) => platform.assetPrefixes)
|
|
129
|
+
),
|
|
130
|
+
skillTargets: unique(
|
|
131
|
+
platforms
|
|
132
|
+
.map((platform) => platform.skillTarget)
|
|
133
|
+
.filter(
|
|
134
|
+
(value) => typeof value === 'string' && value.length > 0
|
|
135
|
+
)
|
|
136
|
+
.map(normalizePath)
|
|
137
|
+
),
|
|
138
|
+
parsePlatformSelection,
|
|
139
|
+
shouldInclude,
|
|
140
|
+
detectInstalledPlatforms,
|
|
141
|
+
platform(platformId) {
|
|
142
|
+
return byId.get(platformId) ?? null;
|
|
143
|
+
},
|
|
144
|
+
platformLabel(platformId) {
|
|
145
|
+
return byId.get(platformId)?.displayName ?? platformId;
|
|
146
|
+
},
|
|
147
|
+
skillDestination(platformId) {
|
|
148
|
+
return byId.get(platformId)?.skillTarget ?? null;
|
|
149
|
+
},
|
|
150
|
+
isKnownPlatformAsset(relativePath) {
|
|
151
|
+
const normalized = normalizePath(relativePath);
|
|
152
|
+
return platforms.some((platform) => (
|
|
153
|
+
platform.assetFiles.includes(normalized)
|
|
154
|
+
|| platform.assetPrefixes.some(
|
|
155
|
+
(prefix) => normalized.startsWith(prefix)
|
|
156
|
+
)
|
|
157
|
+
));
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
function validateManifest(manifest) {
|
|
164
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
165
|
+
throw new Error('Host Asset Manifest must be an object');
|
|
166
|
+
}
|
|
167
|
+
if (manifest.schemaVersion !== 1) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Unsupported Host Asset Manifest schemaVersion: ${manifest.schemaVersion}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (!Array.isArray(manifest.platforms) || manifest.platforms.length === 0) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
'Host Asset Manifest platforms must be a non-empty array'
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (!Array.isArray(manifest.excludedPrefixes)) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
'Host Asset Manifest excludedPrefixes must be an array'
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (!manifest.syncPolicy || typeof manifest.syncPolicy !== 'object') {
|
|
183
|
+
throw new Error('Host Asset Manifest syncPolicy must be an object');
|
|
184
|
+
}
|
|
185
|
+
for (const platform of manifest.platforms) {
|
|
186
|
+
if (!platform || typeof platform !== 'object' || !platform.id) {
|
|
187
|
+
throw new Error('Every host platform must define an id');
|
|
188
|
+
}
|
|
189
|
+
for (const key of [
|
|
190
|
+
'aliases',
|
|
191
|
+
'detectAny',
|
|
192
|
+
'assetPrefixes',
|
|
193
|
+
'assetFiles',
|
|
194
|
+
'commandTargets'
|
|
195
|
+
]) {
|
|
196
|
+
if (!Array.isArray(platform[key])) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Host platform ${platform.id} ${key} must be an array`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const key of [
|
|
204
|
+
'protectedFiles',
|
|
205
|
+
'protectedPrefixes',
|
|
206
|
+
'safeFiles',
|
|
207
|
+
'safePrefixes',
|
|
208
|
+
'managedBlockFiles',
|
|
209
|
+
'obsoleteFiles'
|
|
210
|
+
]) {
|
|
211
|
+
if (!Array.isArray(manifest.syncPolicy[key])) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Host Asset Manifest syncPolicy.${key} must be an array`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
function normalizePath(value) {
|
|
221
|
+
return String(value).replaceAll('\\', '/');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
function unique(values) {
|
|
226
|
+
return [...new Set(values)];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
export const hostAssetManifest = loadHostAssetManifest();
|
|
231
|
+
export const hostRegistry = createHostRegistry(hostAssetManifest);
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import * as defaultFileSystem from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { planActions } from './asset-plan.js';
|
|
6
|
+
|
|
7
|
+
const MATERIALIZED_ACTIONS = new Set(['create', 'update']);
|
|
8
|
+
const VERSION_PATH = '.cowork-flow/.version';
|
|
9
|
+
const TRANSACTION_STATUSES = new Set([
|
|
10
|
+
'staging',
|
|
11
|
+
'staged',
|
|
12
|
+
'committing',
|
|
13
|
+
'rolling-back',
|
|
14
|
+
'rollback-failed',
|
|
15
|
+
'committed'
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function sha256(content) {
|
|
19
|
+
return createHash('sha256').update(content).digest('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function pathExists(fileSystem, path) {
|
|
23
|
+
try {
|
|
24
|
+
await fileSystem.access(path);
|
|
25
|
+
return true;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error && error.code === 'ENOENT') {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function transactionPrefix(targetDir) {
|
|
35
|
+
return `.${basename(targetDir)}.cowork-flow-txn-`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function transactionPath(root, section, relativePath) {
|
|
39
|
+
return join(root, section, ...relativePath.split('/'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function writeMetadata(fileSystem, transactionRoot, metadata) {
|
|
43
|
+
await fileSystem.writeFile(
|
|
44
|
+
join(transactionRoot, 'transaction.json'),
|
|
45
|
+
`${JSON.stringify(metadata, null, 2)}\n`,
|
|
46
|
+
'utf8'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function stagePlan(plan, transactionRoot, metadata, fileSystem) {
|
|
51
|
+
for (const [index, action] of plan.actions.entries()) {
|
|
52
|
+
if (!MATERIALIZED_ACTIONS.has(action.action)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const stagedPath = transactionPath(transactionRoot, 'staging', action.relativePath);
|
|
56
|
+
await fileSystem.mkdir(dirname(stagedPath), { recursive: true });
|
|
57
|
+
if (action.content !== undefined) {
|
|
58
|
+
await fileSystem.writeFile(stagedPath, action.content, 'utf8');
|
|
59
|
+
} else {
|
|
60
|
+
await fileSystem.copyFile(action.source, stagedPath);
|
|
61
|
+
}
|
|
62
|
+
if (action.mode !== null) {
|
|
63
|
+
await fileSystem.chmod(stagedPath, action.mode);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const stagedContent = await fileSystem.readFile(stagedPath, 'utf8');
|
|
67
|
+
const stagedHash = sha256(stagedContent);
|
|
68
|
+
if (stagedHash !== action.validation.sha256) {
|
|
69
|
+
throw new Error(`Staged asset hash mismatch: ${action.relativePath}`);
|
|
70
|
+
}
|
|
71
|
+
if (action.validation.executable) {
|
|
72
|
+
const stagedStats = await fileSystem.stat(stagedPath);
|
|
73
|
+
if ((stagedStats.mode & 0o111) === 0) {
|
|
74
|
+
throw new Error(`Staged executable lost permissions: ${action.relativePath}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
metadata.actions[index].staged = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function validateTargetState(action, fileSystem) {
|
|
82
|
+
const exists = await pathExists(fileSystem, action.destination);
|
|
83
|
+
if (action.targetPolicy === 'create' && exists) {
|
|
84
|
+
throw new Error(`Target changed after planning: ${action.relativePath}`);
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
(action.targetPolicy === 'replace' || action.targetPolicy === 'delete')
|
|
88
|
+
&& action.targetExists
|
|
89
|
+
&& !exists
|
|
90
|
+
) {
|
|
91
|
+
throw new Error(`Target disappeared after planning: ${action.relativePath}`);
|
|
92
|
+
}
|
|
93
|
+
return exists;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function commitPlan(plan, transactionRoot, metadata, fileSystem) {
|
|
97
|
+
metadata.status = 'committing';
|
|
98
|
+
await writeMetadata(fileSystem, transactionRoot, metadata);
|
|
99
|
+
|
|
100
|
+
for (const [index, action] of plan.actions.entries()) {
|
|
101
|
+
if (action.action === 'skip' || action.action === 'protected') {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const record = metadata.actions[index];
|
|
106
|
+
const targetExists = await validateTargetState(action, fileSystem);
|
|
107
|
+
const backupPath = transactionPath(transactionRoot, 'backup', action.relativePath);
|
|
108
|
+
if (targetExists) {
|
|
109
|
+
await fileSystem.mkdir(dirname(backupPath), { recursive: true });
|
|
110
|
+
await fileSystem.rename(action.destination, backupPath);
|
|
111
|
+
record.backedUp = true;
|
|
112
|
+
await writeMetadata(fileSystem, transactionRoot, metadata);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (action.action !== 'delete') {
|
|
116
|
+
const stagedPath = transactionPath(transactionRoot, 'staging', action.relativePath);
|
|
117
|
+
await fileSystem.mkdir(dirname(action.destination), { recursive: true });
|
|
118
|
+
await fileSystem.rename(stagedPath, action.destination);
|
|
119
|
+
}
|
|
120
|
+
record.committed = true;
|
|
121
|
+
await writeMetadata(fileSystem, transactionRoot, metadata);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function rollbackTransaction(transactionRoot, metadata, fileSystem) {
|
|
126
|
+
let touched = false;
|
|
127
|
+
for (let index = metadata.actions.length - 1; index >= 0; index -= 1) {
|
|
128
|
+
const record = metadata.actions[index];
|
|
129
|
+
const backupPath = transactionPath(transactionRoot, 'backup', record.relativePath);
|
|
130
|
+
const stagedPath = transactionPath(transactionRoot, 'staging', record.relativePath);
|
|
131
|
+
const backupExists = await pathExists(fileSystem, backupPath);
|
|
132
|
+
const stagedExists = await pathExists(fileSystem, stagedPath);
|
|
133
|
+
const destinationExists = await pathExists(fileSystem, record.destination);
|
|
134
|
+
|
|
135
|
+
if (backupExists) {
|
|
136
|
+
touched = true;
|
|
137
|
+
if (destinationExists) {
|
|
138
|
+
await fileSystem.rm(record.destination, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
await fileSystem.mkdir(dirname(record.destination), { recursive: true });
|
|
141
|
+
await fileSystem.rename(backupPath, record.destination);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const createdWasCommitted = record.committed
|
|
146
|
+
|| (record.targetPolicy === 'create' && !stagedExists && destinationExists);
|
|
147
|
+
if (createdWasCommitted && destinationExists) {
|
|
148
|
+
touched = true;
|
|
149
|
+
await fileSystem.rm(record.destination, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!metadata.targetDirExisted && touched) {
|
|
154
|
+
await fileSystem.rm(metadata.targetDir, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function readMetadata(fileSystem, transactionRoot) {
|
|
159
|
+
const metadataPath = join(transactionRoot, 'transaction.json');
|
|
160
|
+
let content;
|
|
161
|
+
try {
|
|
162
|
+
content = await fileSystem.readFile(metadataPath, 'utf8');
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error && error.code === 'ENOENT') {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Asset transaction metadata is missing: ${metadataPath}`,
|
|
167
|
+
{ cause: error }
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
return JSON.parse(content);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error && error.name === 'SyntaxError') {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Asset transaction metadata is invalid: ${metadataPath}`,
|
|
179
|
+
{ cause: error }
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validateMetadata(metadata, transactionRoot, resolvedTarget) {
|
|
187
|
+
const metadataPath = join(transactionRoot, 'transaction.json');
|
|
188
|
+
if (
|
|
189
|
+
!metadata
|
|
190
|
+
|| typeof metadata !== 'object'
|
|
191
|
+
|| Array.isArray(metadata)
|
|
192
|
+
|| metadata.schemaVersion !== 1
|
|
193
|
+
|| typeof metadata.targetDir !== 'string'
|
|
194
|
+
|| typeof metadata.targetDirExisted !== 'boolean'
|
|
195
|
+
|| !TRANSACTION_STATUSES.has(metadata.status)
|
|
196
|
+
|| !Array.isArray(metadata.actions)
|
|
197
|
+
) {
|
|
198
|
+
throw new Error(`Asset transaction metadata is invalid: ${metadataPath}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (resolve(metadata.targetDir) !== resolvedTarget) {
|
|
202
|
+
throw new Error(`Asset transaction target does not match: ${metadataPath}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const targetPrefix = `${resolvedTarget}${sep}`;
|
|
206
|
+
for (const action of metadata.actions) {
|
|
207
|
+
if (
|
|
208
|
+
!action
|
|
209
|
+
|| typeof action !== 'object'
|
|
210
|
+
|| typeof action.relativePath !== 'string'
|
|
211
|
+
|| typeof action.destination !== 'string'
|
|
212
|
+
) {
|
|
213
|
+
throw new Error(`Asset transaction metadata is invalid: ${metadataPath}`);
|
|
214
|
+
}
|
|
215
|
+
const destination = resolve(action.destination);
|
|
216
|
+
const expectedDestination = resolve(
|
|
217
|
+
resolvedTarget,
|
|
218
|
+
...action.relativePath.split('/')
|
|
219
|
+
);
|
|
220
|
+
if (
|
|
221
|
+
destination !== expectedDestination
|
|
222
|
+
|| !destination.startsWith(targetPrefix)
|
|
223
|
+
) {
|
|
224
|
+
throw new Error(`Asset transaction action escapes target: ${metadataPath}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function recoverAssetTransactions(
|
|
230
|
+
targetDir,
|
|
231
|
+
{ fileSystem = defaultFileSystem } = {}
|
|
232
|
+
) {
|
|
233
|
+
const resolvedTarget = resolve(targetDir);
|
|
234
|
+
const parentDir = dirname(resolvedTarget);
|
|
235
|
+
let entries;
|
|
236
|
+
try {
|
|
237
|
+
entries = await fileSystem.readdir(parentDir, { withFileTypes: true });
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (error && error.code === 'ENOENT') {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const prefix = transactionPrefix(resolvedTarget);
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
if (!entry.isDirectory() || !entry.name.startsWith(prefix)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const transactionRoot = join(parentDir, entry.name);
|
|
251
|
+
const metadata = await readMetadata(fileSystem, transactionRoot);
|
|
252
|
+
validateMetadata(metadata, transactionRoot, resolvedTarget);
|
|
253
|
+
if (metadata.status !== 'committed') {
|
|
254
|
+
await rollbackTransaction(transactionRoot, metadata, fileSystem);
|
|
255
|
+
}
|
|
256
|
+
await fileSystem.rm(transactionRoot, { recursive: true, force: true });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function applyAssetPlan(
|
|
261
|
+
plan,
|
|
262
|
+
{ dryRun = false, fileSystem = defaultFileSystem } = {}
|
|
263
|
+
) {
|
|
264
|
+
if (dryRun) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const actions = planActions(plan);
|
|
269
|
+
if (!Array.isArray(actions) || !plan.targetDir) {
|
|
270
|
+
throw new Error('Transactional asset apply requires a structured asset plan');
|
|
271
|
+
}
|
|
272
|
+
if (actions.at(-1)?.relativePath !== VERSION_PATH) {
|
|
273
|
+
throw new Error('Asset plan must commit .cowork-flow/.version last');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await recoverAssetTransactions(plan.targetDir, { fileSystem });
|
|
277
|
+
const parentDir = dirname(plan.targetDir);
|
|
278
|
+
await fileSystem.mkdir(parentDir, { recursive: true });
|
|
279
|
+
const transactionRoot = await fileSystem.mkdtemp(
|
|
280
|
+
join(parentDir, transactionPrefix(plan.targetDir))
|
|
281
|
+
);
|
|
282
|
+
const metadata = {
|
|
283
|
+
schemaVersion: 1,
|
|
284
|
+
kind: plan.kind,
|
|
285
|
+
status: 'staging',
|
|
286
|
+
targetDir: plan.targetDir,
|
|
287
|
+
targetDirExisted: await pathExists(fileSystem, plan.targetDir),
|
|
288
|
+
actions: actions.map((action) => ({
|
|
289
|
+
action: action.action,
|
|
290
|
+
relativePath: action.relativePath,
|
|
291
|
+
destination: action.destination,
|
|
292
|
+
targetPolicy: action.targetPolicy,
|
|
293
|
+
rollback: action.rollback,
|
|
294
|
+
staged: false,
|
|
295
|
+
backedUp: false,
|
|
296
|
+
committed: false
|
|
297
|
+
}))
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
await writeMetadata(fileSystem, transactionRoot, metadata);
|
|
302
|
+
await stagePlan(plan, transactionRoot, metadata, fileSystem);
|
|
303
|
+
metadata.status = 'staged';
|
|
304
|
+
await writeMetadata(fileSystem, transactionRoot, metadata);
|
|
305
|
+
await commitPlan(plan, transactionRoot, metadata, fileSystem);
|
|
306
|
+
metadata.status = 'committed';
|
|
307
|
+
await writeMetadata(fileSystem, transactionRoot, metadata);
|
|
308
|
+
await fileSystem.rm(transactionRoot, { recursive: true, force: true });
|
|
309
|
+
} catch (error) {
|
|
310
|
+
try {
|
|
311
|
+
metadata.status = 'rolling-back';
|
|
312
|
+
await writeMetadata(fileSystem, transactionRoot, metadata);
|
|
313
|
+
await rollbackTransaction(transactionRoot, metadata, fileSystem);
|
|
314
|
+
await fileSystem.rm(transactionRoot, { recursive: true, force: true });
|
|
315
|
+
} catch (rollbackError) {
|
|
316
|
+
metadata.status = 'rollback-failed';
|
|
317
|
+
try {
|
|
318
|
+
await writeMetadata(fileSystem, transactionRoot, metadata);
|
|
319
|
+
} catch {
|
|
320
|
+
// Preserve the original and rollback failures below.
|
|
321
|
+
}
|
|
322
|
+
throw new AggregateError(
|
|
323
|
+
[error, rollbackError],
|
|
324
|
+
`Asset transaction rollback failed: ${plan.targetDir}`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
}
|
package/src/lib/platforms.js
CHANGED
|
@@ -1,95 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
import { hostRegistry } from './host-assets.js';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const SUPPORTED_PLATFORMS = [...hostRegistry.platformIds];
|
|
2
5
|
|
|
3
|
-
const PLATFORM_ALIASES = new Map([
|
|
4
|
-
['all', SUPPORTED_PLATFORMS],
|
|
5
|
-
['codex', ['codex']],
|
|
6
|
-
['opencode', ['opencode']],
|
|
7
|
-
['claude', ['claude-code']],
|
|
8
|
-
['claude-code', ['claude-code']],
|
|
9
|
-
['claudecode', ['claude-code']]
|
|
10
|
-
]);
|
|
11
6
|
|
|
12
7
|
export function supportedPlatformMessage() {
|
|
13
8
|
return SUPPORTED_PLATFORMS.join(', ');
|
|
14
9
|
}
|
|
15
10
|
|
|
16
|
-
export function parsePlatformSelection(values) {
|
|
17
|
-
const rawValues = Array.isArray(values) ? values : [values];
|
|
18
|
-
const selected = new Set();
|
|
19
|
-
|
|
20
|
-
for (const rawValue of rawValues) {
|
|
21
|
-
const tokens = String(rawValue ?? '')
|
|
22
|
-
.toLowerCase()
|
|
23
|
-
.split(/[\s,;|+/]+/)
|
|
24
|
-
.map((token) => token.trim())
|
|
25
|
-
.filter(Boolean);
|
|
26
11
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (!platforms) {
|
|
30
|
-
throw new Error(`Unsupported platform: ${token}. Supported platforms: ${supportedPlatformMessage()}`);
|
|
31
|
-
}
|
|
32
|
-
for (const platform of platforms) {
|
|
33
|
-
selected.add(platform);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (selected.size === 0) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
'Platform selection required. Run: cowork-flow init <target> --platform codex|opencode|claude-code'
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return SUPPORTED_PLATFORMS.filter((platform) => selected.has(platform));
|
|
12
|
+
export function parsePlatformSelection(values) {
|
|
13
|
+
return hostRegistry.parsePlatformSelection(values);
|
|
45
14
|
}
|
|
46
15
|
|
|
16
|
+
|
|
47
17
|
export function formatPlatformList(platforms) {
|
|
48
18
|
return platforms.join(', ');
|
|
49
19
|
}
|
|
50
20
|
|
|
51
|
-
export function shouldIncludeForPlatforms(relativePath, platforms) {
|
|
52
|
-
const normalized = relativePath.replaceAll('\\', '/');
|
|
53
21
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
22
|
+
export function shouldIncludeForPlatforms(relativePath, platforms) {
|
|
23
|
+
return hostRegistry.shouldInclude(relativePath, platforms);
|
|
24
|
+
}
|
|
59
25
|
|
|
60
|
-
// ZCode plugin bundle (.zcode/) is installed via a dedicated
|
|
61
|
-
// command, not by init/sync; keep it out of project directories.
|
|
62
|
-
if (normalized.startsWith(".zcode")) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
26
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
if (normalized.startsWith('.opencode/') || normalized.startsWith('.cowork-flow/adapters/opencode/')) {
|
|
70
|
-
return platforms.includes('opencode');
|
|
71
|
-
}
|
|
72
|
-
if (
|
|
73
|
-
normalized === 'CLAUDE.md'
|
|
74
|
-
|| normalized.startsWith('.claude/')
|
|
75
|
-
|| normalized.startsWith('.cowork-flow/adapters/claude-code/')
|
|
76
|
-
) {
|
|
77
|
-
return platforms.includes('claude-code');
|
|
78
|
-
}
|
|
79
|
-
return true;
|
|
27
|
+
export function skillDestinationForPlatform(platform) {
|
|
28
|
+
return hostRegistry.skillDestination(platform);
|
|
80
29
|
}
|
|
81
30
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
export function skillDestinationForPlatform(platform) {
|
|
86
|
-
switch (platform) {
|
|
87
|
-
case 'codex':
|
|
88
|
-
case 'opencode':
|
|
89
|
-
return '.agents/skills';
|
|
90
|
-
case 'claude-code':
|
|
91
|
-
return '.claude/skills';
|
|
92
|
-
default:
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
31
|
+
|
|
32
|
+
export function platformLabel(platform) {
|
|
33
|
+
return hostRegistry.platformLabel(platform);
|
|
95
34
|
}
|