takomi 2.1.26 → 2.1.28
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/.pi/README.md +10 -0
- package/.pi/agents/architect.md +0 -1
- package/.pi/agents/coder.md +0 -1
- package/.pi/agents/designer.md +0 -1
- package/.pi/agents/orchestrator.md +0 -1
- package/.pi/agents/reviewer.md +0 -1
- package/.pi/extensions/takomi-context-manager/diagnostics.ts +1 -1
- package/.pi/extensions/takomi-context-manager/extension-conflicts.ts +14 -3
- package/.pi/extensions/takomi-context-manager/index.ts +6 -3
- package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +28 -13
- package/.pi/extensions/takomi-runtime/commands.ts +24 -7
- package/.pi/extensions/takomi-runtime/context-panel.ts +583 -282
- package/.pi/extensions/takomi-runtime/index.ts +101 -6
- package/.pi/extensions/takomi-runtime/model-routing-defaults.ts +296 -0
- package/.pi/extensions/takomi-runtime/routing-policy.ts +67 -17
- package/.pi/extensions/takomi-runtime/takomi-stats.js +44 -16
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +12 -2
- package/.pi/extensions/takomi-subagents/tool-runner.ts +4 -2
- package/.pi/settings.json +18 -20
- package/README.md +34 -4
- package/package.json +4 -2
- package/src/cli.js +326 -33
- package/src/harness.js +132 -16
- package/src/pi-harness.js +27 -6
- package/src/pi-optional-features.js +195 -0
- package/src/postinstall.js +27 -0
- package/src/skills-catalog.js +245 -0
- package/src/skills-installer.js +244 -101
- package/src/skills-selection-tui.js +200 -0
- package/src/store.js +418 -240
- package/src/takomi-stats.js +44 -16
package/src/store.js
CHANGED
|
@@ -1,240 +1,418 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
await fs.
|
|
21
|
-
await fs.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import { PATHS } from './utils.js';
|
|
7
|
+
import { CORE_SKILLS } from './skills-catalog.js';
|
|
8
|
+
|
|
9
|
+
// ─── Global Store Path ───────────────────────────────────────────────────────
|
|
10
|
+
const HOME = os.homedir();
|
|
11
|
+
export const STORE_PATH = process.env.TAKOMI_STORE_PATH || process.env.TAKOMI_HOME_DIR || path.join(HOME, '.takomi');
|
|
12
|
+
export const MANIFEST_PATH = path.join(STORE_PATH, 'manifest.json');
|
|
13
|
+
|
|
14
|
+
function sha256(value) {
|
|
15
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function hashDirectory(dir) {
|
|
19
|
+
if (!await fs.pathExists(dir)) return null;
|
|
20
|
+
const rootStat = await fs.stat(dir);
|
|
21
|
+
if (rootStat.isFile()) return sha256(await fs.readFile(dir));
|
|
22
|
+
const entries = [];
|
|
23
|
+
async function walk(current, prefix = '') {
|
|
24
|
+
const names = (await fs.readdir(current)).sort();
|
|
25
|
+
for (const name of names) {
|
|
26
|
+
const full = path.join(current, name);
|
|
27
|
+
const rel = path.join(prefix, name).replace(/\\/g, '/');
|
|
28
|
+
const stat = await fs.stat(full);
|
|
29
|
+
if (stat.isDirectory()) {
|
|
30
|
+
entries.push(`dir:${rel}`);
|
|
31
|
+
await walk(full, rel);
|
|
32
|
+
} else {
|
|
33
|
+
entries.push(`file:${rel}:${sha256(await fs.readFile(full))}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
await walk(dir);
|
|
38
|
+
return sha256(entries.join('\n'));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeOwnedMap(value) {
|
|
42
|
+
if (!value || typeof value !== 'object') return {};
|
|
43
|
+
const normalized = {};
|
|
44
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
45
|
+
if (typeof entry === 'string') normalized[name] = { hash: entry };
|
|
46
|
+
else if (entry?.hash) normalized[name] = entry;
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createDefaultManifest() {
|
|
52
|
+
return {
|
|
53
|
+
version: '2.0.0',
|
|
54
|
+
createdAt: new Date().toISOString(),
|
|
55
|
+
updatedAt: new Date().toISOString(),
|
|
56
|
+
linkedHarnesses: [],
|
|
57
|
+
installed: {
|
|
58
|
+
skills: [],
|
|
59
|
+
workflows: [],
|
|
60
|
+
agents: [],
|
|
61
|
+
},
|
|
62
|
+
bundledOwned: {
|
|
63
|
+
skills: {},
|
|
64
|
+
workflows: {},
|
|
65
|
+
},
|
|
66
|
+
harnessOwned: {},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeManifest(manifest) {
|
|
71
|
+
const normalized = { ...createDefaultManifest(), ...(manifest || {}) };
|
|
72
|
+
normalized.installed = { ...createDefaultManifest().installed, ...(manifest?.installed || {}) };
|
|
73
|
+
normalized.bundledOwned = {
|
|
74
|
+
skills: normalizeOwnedMap(manifest?.bundledOwned?.skills),
|
|
75
|
+
workflows: normalizeOwnedMap(manifest?.bundledOwned?.workflows),
|
|
76
|
+
};
|
|
77
|
+
normalized.harnessOwned = manifest?.harnessOwned || {};
|
|
78
|
+
return normalized;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Store Structure ─────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Creates the global store directory structure at ~/.takomi/
|
|
85
|
+
* Idempotent — safe to call multiple times.
|
|
86
|
+
* @returns {Promise<void>}
|
|
87
|
+
*/
|
|
88
|
+
export async function initGlobalStore() {
|
|
89
|
+
await fs.ensureDir(path.join(STORE_PATH, 'skills'));
|
|
90
|
+
await fs.ensureDir(path.join(STORE_PATH, 'workflows'));
|
|
91
|
+
await fs.ensureDir(path.join(STORE_PATH, 'agents'));
|
|
92
|
+
|
|
93
|
+
// Create manifest if it doesn't exist
|
|
94
|
+
if (!await fs.pathExists(MANIFEST_PATH)) {
|
|
95
|
+
const manifest = createDefaultManifest();
|
|
96
|
+
await writeManifest(manifest);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Manifest ────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reads the manifest from disk.
|
|
104
|
+
* @returns {Promise<object>}
|
|
105
|
+
*/
|
|
106
|
+
export async function getManifest() {
|
|
107
|
+
try {
|
|
108
|
+
if (await fs.pathExists(MANIFEST_PATH)) {
|
|
109
|
+
return normalizeManifest(await fs.readJson(MANIFEST_PATH));
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Corrupted manifest — recreate
|
|
113
|
+
}
|
|
114
|
+
return createDefaultManifest();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Writes the manifest to disk.
|
|
119
|
+
* @param {object} manifest
|
|
120
|
+
* @returns {Promise<void>}
|
|
121
|
+
*/
|
|
122
|
+
export async function writeManifest(manifest) {
|
|
123
|
+
const nextManifest = normalizeManifest(manifest);
|
|
124
|
+
nextManifest.updatedAt = new Date().toISOString();
|
|
125
|
+
await fs.ensureDir(STORE_PATH);
|
|
126
|
+
await fs.writeJson(MANIFEST_PATH, nextManifest, { spaces: 2 });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Populate Store from Package Assets ──────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
async function listAllBundledSkillNames() {
|
|
132
|
+
const entries = await fs.readdir(PATHS.skills);
|
|
133
|
+
const skills = [];
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
const stat = await fs.stat(path.join(PATHS.skills, entry));
|
|
136
|
+
if (stat.isDirectory()) skills.push(entry);
|
|
137
|
+
}
|
|
138
|
+
return skills.sort();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const CORE_WORKFLOWS = [
|
|
142
|
+
'vibe-genesis.md', 'vibe-design.md', 'vibe-build.md',
|
|
143
|
+
'vibe-continueBuild.md', 'vibe-finalize.md', 'vibe-spawnTask.md',
|
|
144
|
+
'vibe-primeAgent.md', 'vibe-syncDocs.md', 'stitch.md',
|
|
145
|
+
'mode-orchestrator.md', 'mode-architect.md', 'mode-code.md',
|
|
146
|
+
'mode-debug.md', 'mode-ask.md', 'mode-review.md', 'mode-visionary.md',
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
function resolveSkillSelection(mode) {
|
|
150
|
+
if (mode === 'core') return [...CORE_SKILLS];
|
|
151
|
+
if (Array.isArray(mode)) return [...mode];
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function resolveWorkflowSelection(mode) {
|
|
156
|
+
if (mode === 'core') return [...CORE_WORKFLOWS];
|
|
157
|
+
if (mode === 'all' || mode === 'no-legacy') {
|
|
158
|
+
const entries = await fs.readdir(PATHS.workflows);
|
|
159
|
+
return entries.filter(f => {
|
|
160
|
+
if (!f.endsWith('.md')) return false;
|
|
161
|
+
if (mode === 'no-legacy' && f.startsWith('LEGACY')) return false;
|
|
162
|
+
return true;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(mode)) return [...mode];
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function resolveStoreSkillSelection(mode) {
|
|
170
|
+
if (mode === 'all') return await listAllBundledSkillNames();
|
|
171
|
+
return resolveSkillSelection(mode) || [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function buildStoreSkillsReconcilePlan(mode) {
|
|
175
|
+
const manifest = await getManifest();
|
|
176
|
+
const selected = new Set(await resolveStoreSkillSelection(mode));
|
|
177
|
+
const owned = normalizeOwnedMap(manifest.bundledOwned?.skills);
|
|
178
|
+
return {
|
|
179
|
+
owned: Object.keys(owned).sort(),
|
|
180
|
+
selected: [...selected].sort(),
|
|
181
|
+
toRemove: Object.keys(owned).filter((skill) => !selected.has(skill)).sort(),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function buildStoreWorkflowsReconcilePlan(mode) {
|
|
186
|
+
const manifest = await getManifest();
|
|
187
|
+
const selected = new Set(await resolveWorkflowSelection(mode));
|
|
188
|
+
const owned = normalizeOwnedMap(manifest.bundledOwned?.workflows);
|
|
189
|
+
return {
|
|
190
|
+
owned: Object.keys(owned).sort(),
|
|
191
|
+
selected: [...selected].sort(),
|
|
192
|
+
toRemove: Object.keys(owned).filter((workflow) => !selected.has(workflow)).sort(),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Copies skills from the bundled package assets into the global store.
|
|
198
|
+
* Prunes only skills previously recorded as Takomi-bundled owned.
|
|
199
|
+
* @param {'core'|'all'|string[]} mode - Which skills to install
|
|
200
|
+
* @returns {Promise<string[]>} - List of skill names copied
|
|
201
|
+
*/
|
|
202
|
+
export async function populateSkills(mode) {
|
|
203
|
+
const storeSkills = path.join(STORE_PATH, 'skills');
|
|
204
|
+
await fs.ensureDir(storeSkills);
|
|
205
|
+
|
|
206
|
+
let skillsToCopy = await resolveStoreSkillSelection(mode);
|
|
207
|
+
skillsToCopy = [...new Set(skillsToCopy)].sort();
|
|
208
|
+
const selected = new Set(skillsToCopy);
|
|
209
|
+
|
|
210
|
+
const manifest = await getManifest();
|
|
211
|
+
const previousOwned = normalizeOwnedMap(manifest.bundledOwned?.skills);
|
|
212
|
+
const nextOwned = {};
|
|
213
|
+
const copied = [];
|
|
214
|
+
const pruned = [];
|
|
215
|
+
const preservedManual = [];
|
|
216
|
+
const preservedModified = [];
|
|
217
|
+
|
|
218
|
+
for (const [skill, ownedEntry] of Object.entries(previousOwned)) {
|
|
219
|
+
if (selected.has(skill)) continue;
|
|
220
|
+
const dest = path.join(storeSkills, skill);
|
|
221
|
+
if (!await fs.pathExists(dest)) continue;
|
|
222
|
+
const currentHash = await hashDirectory(dest);
|
|
223
|
+
if (currentHash && currentHash === ownedEntry.hash) {
|
|
224
|
+
await fs.remove(dest);
|
|
225
|
+
pruned.push(skill);
|
|
226
|
+
} else {
|
|
227
|
+
preservedModified.push(skill);
|
|
228
|
+
nextOwned[skill] = ownedEntry;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const skill of skillsToCopy) {
|
|
233
|
+
const src = path.join(PATHS.skills, skill);
|
|
234
|
+
const dest = path.join(storeSkills, skill);
|
|
235
|
+
try {
|
|
236
|
+
if (!await fs.pathExists(src)) continue;
|
|
237
|
+
|
|
238
|
+
if (await fs.pathExists(dest)) {
|
|
239
|
+
const currentHash = await hashDirectory(dest);
|
|
240
|
+
const previous = previousOwned[skill];
|
|
241
|
+
if (!previous) {
|
|
242
|
+
preservedManual.push(skill);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (previous.hash && currentHash && currentHash !== previous.hash) {
|
|
246
|
+
preservedModified.push(skill);
|
|
247
|
+
nextOwned[skill] = previous;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
await fs.remove(dest);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await fs.copy(src, dest, { overwrite: true });
|
|
254
|
+
nextOwned[skill] = {
|
|
255
|
+
hash: await hashDirectory(dest),
|
|
256
|
+
targetPath: dest,
|
|
257
|
+
installedAt: new Date().toISOString(),
|
|
258
|
+
};
|
|
259
|
+
copied.push(skill);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.log(pc.yellow(` ⚠ Could not copy skill "${skill}": ${error.message}`));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
manifest.bundledOwned.skills = Object.fromEntries(Object.entries(nextOwned).sort(([a], [b]) => a.localeCompare(b)));
|
|
266
|
+
manifest.lastSkillPopulate = { mode: Array.isArray(mode) ? 'custom' : mode, copied, pruned, preservedManual, preservedModified };
|
|
267
|
+
await writeManifest(manifest);
|
|
268
|
+
|
|
269
|
+
if (pruned.length) console.log(pc.dim(` Removed ${pruned.length} deselected Takomi-owned store skill${pruned.length === 1 ? '' : 's'}`));
|
|
270
|
+
if (preservedManual.length) console.log(pc.yellow(` Preserved manual store skill name collisions: ${preservedManual.join(', ')}`));
|
|
271
|
+
if (preservedModified.length) console.log(pc.yellow(` Preserved modified Takomi-owned store skills: ${preservedModified.join(', ')}`));
|
|
272
|
+
|
|
273
|
+
return copied;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Copies workflows from the bundled package assets into the global store.
|
|
278
|
+
* @param {'core'|'all'|'no-legacy'|string[]} mode - Which workflows to install
|
|
279
|
+
* @returns {Promise<string[]>} - List of workflow filenames copied
|
|
280
|
+
*/
|
|
281
|
+
export async function populateWorkflows(mode) {
|
|
282
|
+
const storeWorkflows = path.join(STORE_PATH, 'workflows');
|
|
283
|
+
await fs.ensureDir(storeWorkflows);
|
|
284
|
+
|
|
285
|
+
const workflowsToCopy = [...new Set(await resolveWorkflowSelection(mode))].sort();
|
|
286
|
+
const selected = new Set(workflowsToCopy);
|
|
287
|
+
const manifest = await getManifest();
|
|
288
|
+
const previousOwned = normalizeOwnedMap(manifest.bundledOwned?.workflows);
|
|
289
|
+
const nextOwned = {};
|
|
290
|
+
const copied = [];
|
|
291
|
+
const pruned = [];
|
|
292
|
+
const preservedManual = [];
|
|
293
|
+
const preservedModified = [];
|
|
294
|
+
|
|
295
|
+
for (const [workflow, ownedEntry] of Object.entries(previousOwned)) {
|
|
296
|
+
if (selected.has(workflow)) continue;
|
|
297
|
+
const dest = path.join(storeWorkflows, workflow);
|
|
298
|
+
if (!await fs.pathExists(dest)) continue;
|
|
299
|
+
const currentHash = await hashDirectory(dest);
|
|
300
|
+
if (currentHash && currentHash === ownedEntry.hash) {
|
|
301
|
+
await fs.remove(dest);
|
|
302
|
+
pruned.push(workflow);
|
|
303
|
+
} else {
|
|
304
|
+
preservedModified.push(workflow);
|
|
305
|
+
nextOwned[workflow] = ownedEntry;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const workflow of workflowsToCopy) {
|
|
310
|
+
const src = path.join(PATHS.workflows, workflow);
|
|
311
|
+
const dest = path.join(storeWorkflows, workflow);
|
|
312
|
+
try {
|
|
313
|
+
if (!await fs.pathExists(src)) continue;
|
|
314
|
+
|
|
315
|
+
if (await fs.pathExists(dest)) {
|
|
316
|
+
const currentHash = await hashDirectory(dest);
|
|
317
|
+
const previous = previousOwned[workflow];
|
|
318
|
+
if (!previous) {
|
|
319
|
+
preservedManual.push(workflow);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (previous.hash && currentHash && currentHash !== previous.hash) {
|
|
323
|
+
preservedModified.push(workflow);
|
|
324
|
+
nextOwned[workflow] = previous;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
await fs.remove(dest);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await fs.copy(src, dest, { overwrite: true });
|
|
331
|
+
nextOwned[workflow] = {
|
|
332
|
+
hash: await hashDirectory(dest),
|
|
333
|
+
targetPath: dest,
|
|
334
|
+
installedAt: new Date().toISOString(),
|
|
335
|
+
};
|
|
336
|
+
copied.push(workflow);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.log(pc.yellow(` ⚠ Could not copy workflow "${workflow}": ${error.message}`));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
manifest.bundledOwned.workflows = Object.fromEntries(Object.entries(nextOwned).sort(([a], [b]) => a.localeCompare(b)));
|
|
343
|
+
manifest.lastWorkflowPopulate = { mode: Array.isArray(mode) ? 'custom' : mode, copied, pruned, preservedManual, preservedModified };
|
|
344
|
+
await writeManifest(manifest);
|
|
345
|
+
|
|
346
|
+
if (pruned.length) console.log(pc.dim(` Removed ${pruned.length} deselected Takomi-owned store workflow${pruned.length === 1 ? '' : 's'}`));
|
|
347
|
+
if (preservedManual.length) console.log(pc.yellow(` Preserved manual store workflow name collisions: ${preservedManual.join(', ')}`));
|
|
348
|
+
if (preservedModified.length) console.log(pc.yellow(` Preserved modified Takomi-owned store workflows: ${preservedModified.join(', ')}`));
|
|
349
|
+
|
|
350
|
+
return copied;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Copies agent YAMLs (custom_modes.yaml, etc.) into the global store.
|
|
355
|
+
* @returns {Promise<string[]>} - List of YAML filenames copied
|
|
356
|
+
*/
|
|
357
|
+
export async function populateAgentYamls() {
|
|
358
|
+
const storeAgents = path.join(STORE_PATH, 'agents');
|
|
359
|
+
await fs.ensureDir(storeAgents);
|
|
360
|
+
|
|
361
|
+
const copied = [];
|
|
362
|
+
try {
|
|
363
|
+
const entries = await fs.readdir(PATHS.agentsYaml);
|
|
364
|
+
for (const entry of entries) {
|
|
365
|
+
const src = path.join(PATHS.agentsYaml, entry);
|
|
366
|
+
const dest = path.join(storeAgents, entry);
|
|
367
|
+
await fs.copy(src, dest, { overwrite: true });
|
|
368
|
+
copied.push(entry);
|
|
369
|
+
}
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.log(pc.yellow(` ⚠ Could not copy agent YAMLs: ${error.message}`));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return copied;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ─── Store Queries ───────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Lists all skills currently in the global store.
|
|
381
|
+
* @returns {Promise<string[]>}
|
|
382
|
+
*/
|
|
383
|
+
export async function getStoreSkills() {
|
|
384
|
+
const skillsDir = path.join(STORE_PATH, 'skills');
|
|
385
|
+
try {
|
|
386
|
+
const entries = await fs.readdir(skillsDir);
|
|
387
|
+
const skills = [];
|
|
388
|
+
for (const entry of entries) {
|
|
389
|
+
const stat = await fs.stat(path.join(skillsDir, entry));
|
|
390
|
+
if (stat.isDirectory()) skills.push(entry);
|
|
391
|
+
}
|
|
392
|
+
return skills;
|
|
393
|
+
} catch {
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Lists all workflows currently in the global store.
|
|
400
|
+
* @returns {Promise<string[]>}
|
|
401
|
+
*/
|
|
402
|
+
export async function getStoreWorkflows() {
|
|
403
|
+
const workflowsDir = path.join(STORE_PATH, 'workflows');
|
|
404
|
+
try {
|
|
405
|
+
const entries = await fs.readdir(workflowsDir);
|
|
406
|
+
return entries.filter(f => f.endsWith('.md'));
|
|
407
|
+
} catch {
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Checks if the global store has been initialized.
|
|
414
|
+
* @returns {Promise<boolean>}
|
|
415
|
+
*/
|
|
416
|
+
export async function isStoreInitialized() {
|
|
417
|
+
return fs.pathExists(MANIFEST_PATH);
|
|
418
|
+
}
|