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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { PATHS } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export const CORE_SKILLS = [
|
|
7
|
+
'takomi',
|
|
8
|
+
'sync-docs',
|
|
9
|
+
'code-review',
|
|
10
|
+
'security-audit',
|
|
11
|
+
'optimize-agent-context',
|
|
12
|
+
'agent-recovery',
|
|
13
|
+
'avoid-feature-creep',
|
|
14
|
+
'ai-sdk',
|
|
15
|
+
'git-commit-generation',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const SKILL_CATEGORIES = [
|
|
19
|
+
{
|
|
20
|
+
id: 'core',
|
|
21
|
+
title: 'Core / Recommended',
|
|
22
|
+
color: 'cyan',
|
|
23
|
+
description: 'Essential skills for efficient Takomi usage.',
|
|
24
|
+
skills: CORE_SKILLS,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'developer',
|
|
28
|
+
title: 'Developer / Frameworks',
|
|
29
|
+
color: 'blue',
|
|
30
|
+
description: 'Framework, repo, and developer workflow helpers.',
|
|
31
|
+
skills: [
|
|
32
|
+
'ai-sdk',
|
|
33
|
+
'nextjs-standards',
|
|
34
|
+
'context7',
|
|
35
|
+
'monorepo-management',
|
|
36
|
+
'upgrading-expo',
|
|
37
|
+
'github-ops',
|
|
38
|
+
'git-worktree',
|
|
39
|
+
'git-commit-generation',
|
|
40
|
+
'pr-comment-fix',
|
|
41
|
+
'jules',
|
|
42
|
+
'gemini',
|
|
43
|
+
'anti-gravity',
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'security',
|
|
48
|
+
title: 'Security / Review',
|
|
49
|
+
color: 'red',
|
|
50
|
+
description: 'Security, audit, and review workflows.',
|
|
51
|
+
skills: [
|
|
52
|
+
'security-audit',
|
|
53
|
+
'audit-website',
|
|
54
|
+
'code-review',
|
|
55
|
+
'jstar-reviewer',
|
|
56
|
+
'convex-security-audit',
|
|
57
|
+
'convex-security-check',
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'convex',
|
|
62
|
+
title: 'Convex',
|
|
63
|
+
color: 'green',
|
|
64
|
+
description: 'Convex framework skills and best practices.',
|
|
65
|
+
skills: [
|
|
66
|
+
'convex',
|
|
67
|
+
'convex-agents',
|
|
68
|
+
'convex-best-practices',
|
|
69
|
+
'convex-component-authoring',
|
|
70
|
+
'convex-cron-jobs',
|
|
71
|
+
'convex-file-storage',
|
|
72
|
+
'convex-functions',
|
|
73
|
+
'convex-http-actions',
|
|
74
|
+
'convex-migrations',
|
|
75
|
+
'convex-realtime',
|
|
76
|
+
'convex-schema-validator',
|
|
77
|
+
'convex-security-audit',
|
|
78
|
+
'convex-security-check',
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'frontend',
|
|
83
|
+
title: 'Frontend / UI',
|
|
84
|
+
color: 'magenta',
|
|
85
|
+
description: 'Frontend implementation, UI/UX, components, and testing.',
|
|
86
|
+
skills: [
|
|
87
|
+
'frontend-design',
|
|
88
|
+
'web-design-guidelines',
|
|
89
|
+
'building-native-ui',
|
|
90
|
+
'ui-ux-pro-max',
|
|
91
|
+
'component-analysis',
|
|
92
|
+
'21st-dev-components',
|
|
93
|
+
'stitch',
|
|
94
|
+
'webapp-testing',
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'docs-office',
|
|
99
|
+
title: 'Docs / Office / Extraction',
|
|
100
|
+
color: 'yellow',
|
|
101
|
+
description: 'Document formats, extraction, and README support.',
|
|
102
|
+
skills: [
|
|
103
|
+
'pdf',
|
|
104
|
+
'docx',
|
|
105
|
+
'pptx',
|
|
106
|
+
'xlsx',
|
|
107
|
+
'high-fidelity-extraction',
|
|
108
|
+
'crafting-effective-readmes',
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'marketing',
|
|
113
|
+
title: 'Marketing / SEO / Copy',
|
|
114
|
+
color: 'green',
|
|
115
|
+
description: 'Marketing, SEO, naming, pricing, and social strategy.',
|
|
116
|
+
skills: [
|
|
117
|
+
'copywriting',
|
|
118
|
+
'marketing-ideas',
|
|
119
|
+
'pricing-strategy',
|
|
120
|
+
'programmatic-seo',
|
|
121
|
+
'seo-ready',
|
|
122
|
+
'social-content',
|
|
123
|
+
'twitter-automation',
|
|
124
|
+
'google-trends',
|
|
125
|
+
'domain-name-brainstormer',
|
|
126
|
+
'global-brand-namer',
|
|
127
|
+
'youtube-pipeline',
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'ai-media',
|
|
132
|
+
title: 'AI Media / Content Creation',
|
|
133
|
+
color: 'magenta',
|
|
134
|
+
description: 'Optional AI media/content skills. Not installed by default.',
|
|
135
|
+
skills: [
|
|
136
|
+
'ai-avatar-video',
|
|
137
|
+
'ai-marketing-videos',
|
|
138
|
+
'ai-podcast-creation',
|
|
139
|
+
'ai-product-photography',
|
|
140
|
+
'ai-social-media-content',
|
|
141
|
+
'ai-voice-cloning',
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'creative-video',
|
|
146
|
+
title: 'Creative / Video / Art',
|
|
147
|
+
color: 'blue',
|
|
148
|
+
description: 'Creative visuals, video, animation, and art workflows.',
|
|
149
|
+
skills: [
|
|
150
|
+
'algorithmic-art',
|
|
151
|
+
'blender-mcp-scene-director',
|
|
152
|
+
'remotion',
|
|
153
|
+
'youtube-pipeline',
|
|
154
|
+
'ai-avatar-video',
|
|
155
|
+
'ai-marketing-videos',
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'skill-building',
|
|
160
|
+
title: 'Skill Building / Prompting / Orchestration',
|
|
161
|
+
color: 'cyan',
|
|
162
|
+
description: 'Skill authoring, prompt engineering, and optional orchestration helpers.',
|
|
163
|
+
skills: [
|
|
164
|
+
'skill-creator',
|
|
165
|
+
'prompt-engineering',
|
|
166
|
+
'subagent-driven-development',
|
|
167
|
+
'spawn-task',
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const colorFns = {
|
|
173
|
+
cyan: pc.cyan,
|
|
174
|
+
blue: pc.blue,
|
|
175
|
+
red: pc.red,
|
|
176
|
+
green: pc.green,
|
|
177
|
+
magenta: pc.magenta,
|
|
178
|
+
yellow: pc.yellow,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export function colorCategory(category) {
|
|
182
|
+
const color = colorFns[category.color] || ((value) => value);
|
|
183
|
+
return color(`[${category.title}]`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function listBundledSkillNames() {
|
|
187
|
+
const entries = await fs.readdir(PATHS.skills);
|
|
188
|
+
const skills = [];
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
const stat = await fs.stat(path.join(PATHS.skills, entry));
|
|
191
|
+
if (stat.isDirectory()) skills.push(entry);
|
|
192
|
+
}
|
|
193
|
+
return skills.sort();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function readSkillDescription(skillName) {
|
|
197
|
+
try {
|
|
198
|
+
const skillPath = path.join(PATHS.skills, skillName, 'SKILL.md');
|
|
199
|
+
const content = await fs.readFile(skillPath, 'utf8');
|
|
200
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
201
|
+
const frontmatter = match?.[1] || '';
|
|
202
|
+
const descriptionMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
203
|
+
if (!descriptionMatch) return '';
|
|
204
|
+
return descriptionMatch[1].trim().replace(/^['"]|['"]$/g, '');
|
|
205
|
+
} catch {
|
|
206
|
+
return '';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function getSkillChoices(skillNames, selected = []) {
|
|
211
|
+
const selectedSet = new Set(selected);
|
|
212
|
+
const choices = [];
|
|
213
|
+
for (const skillName of skillNames) {
|
|
214
|
+
choices.push({
|
|
215
|
+
title: skillName,
|
|
216
|
+
value: skillName,
|
|
217
|
+
selected: selectedSet.has(skillName),
|
|
218
|
+
description: await readSkillDescription(skillName),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return choices;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function getValidCoreSkills() {
|
|
225
|
+
const bundled = new Set(await listBundledSkillNames());
|
|
226
|
+
return CORE_SKILLS.filter((skill) => bundled.has(skill));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function getSkillsForCategories(categoryIds) {
|
|
230
|
+
const bundled = new Set(await listBundledSkillNames());
|
|
231
|
+
const selected = new Set();
|
|
232
|
+
for (const category of SKILL_CATEGORIES) {
|
|
233
|
+
if (!categoryIds.includes(category.id)) continue;
|
|
234
|
+
for (const skill of category.skills) {
|
|
235
|
+
if (bundled.has(skill)) selected.add(skill);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return [...selected].sort();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function getUncategorizedSkills() {
|
|
242
|
+
const bundled = await listBundledSkillNames();
|
|
243
|
+
const categorized = new Set(SKILL_CATEGORIES.flatMap((category) => category.skills));
|
|
244
|
+
return bundled.filter((skill) => !categorized.has(skill));
|
|
245
|
+
}
|
package/src/skills-installer.js
CHANGED
|
@@ -1,101 +1,244 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import crypto from 'crypto';
|
|
5
|
-
import pc from 'picocolors';
|
|
6
|
-
import { PATHS } from './utils.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
export const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
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
|
-
const
|
|
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
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import { PATHS } from './utils.js';
|
|
7
|
+
import { getValidCoreSkills, listBundledSkillNames } from './skills-catalog.js';
|
|
8
|
+
|
|
9
|
+
const HOME = os.homedir();
|
|
10
|
+
const TAKOMI_HOME = process.env.TAKOMI_HOME_DIR || path.join(HOME, '.takomi');
|
|
11
|
+
export const SKILLS_MANIFEST_PATH = path.join(TAKOMI_HOME, 'skills-manifest.json');
|
|
12
|
+
export const SKILLS_ROOT = process.env.TAKOMI_SKILLS_ROOT || path.join(HOME, '.agents', 'skills');
|
|
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 entries = [];
|
|
21
|
+
async function walk(current, prefix = '') {
|
|
22
|
+
const names = (await fs.readdir(current)).sort();
|
|
23
|
+
for (const name of names) {
|
|
24
|
+
const full = path.join(current, name);
|
|
25
|
+
const rel = path.join(prefix, name).replace(/\\/g, '/');
|
|
26
|
+
const stat = await fs.stat(full);
|
|
27
|
+
if (stat.isDirectory()) {
|
|
28
|
+
entries.push(`dir:${rel}`);
|
|
29
|
+
await walk(full, rel);
|
|
30
|
+
} else {
|
|
31
|
+
entries.push(`file:${rel}:${sha256(await fs.readFile(full))}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
await walk(dir);
|
|
36
|
+
return sha256(entries.join('\n'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeOwnedEntry(name, entry) {
|
|
40
|
+
if (!entry) return null;
|
|
41
|
+
if (typeof entry === 'string') {
|
|
42
|
+
return {
|
|
43
|
+
name,
|
|
44
|
+
hash: entry,
|
|
45
|
+
targetPath: path.join(SKILLS_ROOT, name),
|
|
46
|
+
installedAt: undefined,
|
|
47
|
+
takomiVersion: undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
name,
|
|
52
|
+
hash: entry.hash,
|
|
53
|
+
targetPath: entry.targetPath || path.join(SKILLS_ROOT, name),
|
|
54
|
+
installedAt: entry.installedAt,
|
|
55
|
+
takomiVersion: entry.takomiVersion,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getOwnedMap(manifest) {
|
|
60
|
+
const owned = {};
|
|
61
|
+
for (const [name, entry] of Object.entries(manifest?.owned || {})) {
|
|
62
|
+
const normalized = normalizeOwnedEntry(name, entry);
|
|
63
|
+
if (normalized?.hash) owned[name] = normalized;
|
|
64
|
+
}
|
|
65
|
+
return owned;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function readSkillsInstallManifest() {
|
|
69
|
+
try {
|
|
70
|
+
if (await fs.pathExists(SKILLS_MANIFEST_PATH)) return await fs.readJson(SKILLS_MANIFEST_PATH);
|
|
71
|
+
} catch {}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function writeSkillsInstallManifest(manifest) {
|
|
76
|
+
await fs.ensureDir(TAKOMI_HOME);
|
|
77
|
+
await fs.writeJson(SKILLS_MANIFEST_PATH, manifest, { spaces: 2 });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function getInstalledTakomiSkillNames() {
|
|
81
|
+
return Object.keys(getOwnedMap(await readSkillsInstallManifest())).sort();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function buildSkillsReconcilePlan(selectedSkills = []) {
|
|
85
|
+
const manifest = await readSkillsInstallManifest();
|
|
86
|
+
const owned = getOwnedMap(manifest);
|
|
87
|
+
const selected = new Set(selectedSkills);
|
|
88
|
+
return {
|
|
89
|
+
owned: Object.keys(owned).sort(),
|
|
90
|
+
selected: [...selected].sort(),
|
|
91
|
+
toInstall: [...selected].sort(),
|
|
92
|
+
toRemove: Object.keys(owned).filter((name) => !selected.has(name)).sort(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function installBundledSkills(version = 'unknown', options = {}) {
|
|
97
|
+
const bundled = new Set(await listBundledSkillNames());
|
|
98
|
+
const selectedSkills = Array.isArray(options.selectedSkills)
|
|
99
|
+
? options.selectedSkills
|
|
100
|
+
: await getValidCoreSkills();
|
|
101
|
+
const selected = [...new Set(selectedSkills)].filter((name) => bundled.has(name)).sort();
|
|
102
|
+
const selectedSet = new Set(selected);
|
|
103
|
+
const prune = options.prune !== false;
|
|
104
|
+
const mode = options.mode || 'core';
|
|
105
|
+
|
|
106
|
+
await fs.ensureDir(SKILLS_ROOT);
|
|
107
|
+
|
|
108
|
+
const previousManifest = await readSkillsInstallManifest();
|
|
109
|
+
const previousOwned = getOwnedMap(previousManifest);
|
|
110
|
+
const nextOwned = {};
|
|
111
|
+
const installed = [];
|
|
112
|
+
const pruned = [];
|
|
113
|
+
const missing = [];
|
|
114
|
+
const preservedManual = [];
|
|
115
|
+
const preservedModified = [];
|
|
116
|
+
const skippedUnknown = selectedSkills.filter((name) => !bundled.has(name)).sort();
|
|
117
|
+
|
|
118
|
+
if (prune) {
|
|
119
|
+
for (const [name, ownedEntry] of Object.entries(previousOwned)) {
|
|
120
|
+
if (selectedSet.has(name)) continue;
|
|
121
|
+
const dest = ownedEntry.targetPath || path.join(SKILLS_ROOT, name);
|
|
122
|
+
if (!await fs.pathExists(dest)) continue;
|
|
123
|
+
const currentHash = await hashDirectory(dest);
|
|
124
|
+
if (currentHash && currentHash === ownedEntry.hash) {
|
|
125
|
+
await fs.remove(dest);
|
|
126
|
+
pruned.push(name);
|
|
127
|
+
} else {
|
|
128
|
+
preservedModified.push(name);
|
|
129
|
+
nextOwned[name] = ownedEntry;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const name of selected) {
|
|
135
|
+
const src = path.join(PATHS.skills, name);
|
|
136
|
+
const dest = path.join(SKILLS_ROOT, name);
|
|
137
|
+
const previous = previousOwned[name];
|
|
138
|
+
|
|
139
|
+
if (!await fs.pathExists(src)) {
|
|
140
|
+
missing.push(name);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (await fs.pathExists(dest)) {
|
|
145
|
+
const currentHash = await hashDirectory(dest);
|
|
146
|
+
if (!previous) {
|
|
147
|
+
preservedManual.push(name);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (currentHash && previous.hash && currentHash !== previous.hash) {
|
|
151
|
+
preservedModified.push(name);
|
|
152
|
+
nextOwned[name] = previous;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
await fs.remove(dest);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await fs.copy(src, dest, { overwrite: true, errorOnExist: false });
|
|
159
|
+
const hash = await hashDirectory(dest);
|
|
160
|
+
nextOwned[name] = {
|
|
161
|
+
name,
|
|
162
|
+
hash,
|
|
163
|
+
targetPath: dest,
|
|
164
|
+
installedAt: new Date().toISOString(),
|
|
165
|
+
takomiVersion: version,
|
|
166
|
+
};
|
|
167
|
+
installed.push(name);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Keep selected, previously-owned modified resources tracked so future
|
|
171
|
+
// deselection still preserves them instead of treating them as manual files.
|
|
172
|
+
for (const name of selected) {
|
|
173
|
+
if (!nextOwned[name] && previousOwned[name]) nextOwned[name] = previousOwned[name];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const manifest = {
|
|
177
|
+
schemaVersion: 2,
|
|
178
|
+
takomiVersion: version,
|
|
179
|
+
installedAt: previousManifest?.installedAt || new Date().toISOString(),
|
|
180
|
+
updatedAt: new Date().toISOString(),
|
|
181
|
+
targetRoot: SKILLS_ROOT,
|
|
182
|
+
mode,
|
|
183
|
+
selectedSkills: selected,
|
|
184
|
+
owned: Object.fromEntries(Object.entries(nextOwned).sort(([a], [b]) => a.localeCompare(b))),
|
|
185
|
+
lastRun: {
|
|
186
|
+
installed,
|
|
187
|
+
pruned,
|
|
188
|
+
preservedManual,
|
|
189
|
+
preservedModified,
|
|
190
|
+
skippedUnknown,
|
|
191
|
+
missing,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
await writeSkillsInstallManifest(manifest);
|
|
196
|
+
return {
|
|
197
|
+
targetRoot: SKILLS_ROOT,
|
|
198
|
+
manifest,
|
|
199
|
+
count: installed.length,
|
|
200
|
+
selectedCount: selected.length,
|
|
201
|
+
installed,
|
|
202
|
+
pruned,
|
|
203
|
+
preservedManual,
|
|
204
|
+
preservedModified: [...new Set(preservedModified)].sort(),
|
|
205
|
+
skippedUnknown,
|
|
206
|
+
missing,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function validateSkillsInstall(selectedSkills = null) {
|
|
211
|
+
const expected = Array.isArray(selectedSkills) ? selectedSkills : (await readSkillsInstallManifest())?.selectedSkills || await getValidCoreSkills();
|
|
212
|
+
const missing = [];
|
|
213
|
+
for (const name of expected) {
|
|
214
|
+
if (!await fs.pathExists(path.join(SKILLS_ROOT, name))) missing.push(name);
|
|
215
|
+
}
|
|
216
|
+
return { root: SKILLS_ROOT, expected: expected.length, missing, ok: missing.length === 0 };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function printSkillsInstallSummary(result, validation) {
|
|
220
|
+
if (result?.leaveAsIs) {
|
|
221
|
+
console.log(pc.green('\n✔ Left Takomi skills unchanged'));
|
|
222
|
+
console.log(pc.white(` Root: ${result.targetRoot}`));
|
|
223
|
+
console.log(pc.white(` Manifest: ${SKILLS_MANIFEST_PATH}`));
|
|
224
|
+
console.log(pc.white(` Owned: ${result.ownedCount}`));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log(pc.green('\n✔ Reconciled Takomi skills'));
|
|
229
|
+
console.log(pc.white(` Root: ${result.targetRoot}`));
|
|
230
|
+
console.log(pc.white(` Manifest: ${SKILLS_MANIFEST_PATH}`));
|
|
231
|
+
console.log(pc.white(` Selected: ${result.selectedCount}`));
|
|
232
|
+
console.log(pc.white(` Installed: ${result.installed.length}`));
|
|
233
|
+
console.log(pc.white(` Removed: ${result.pruned.length}`));
|
|
234
|
+
if (result.preservedManual.length) {
|
|
235
|
+
console.log(pc.yellow(` Preserved manual name collisions: ${result.preservedManual.join(', ')}`));
|
|
236
|
+
}
|
|
237
|
+
if (result.preservedModified.length) {
|
|
238
|
+
console.log(pc.yellow(` Preserved modified Takomi skills: ${result.preservedModified.join(', ')}`));
|
|
239
|
+
}
|
|
240
|
+
if (result.skippedUnknown.length) {
|
|
241
|
+
console.log(pc.yellow(` Skipped unknown bundled skills: ${result.skippedUnknown.join(', ')}`));
|
|
242
|
+
}
|
|
243
|
+
console.log(pc.white(` Status: ${validation.ok ? 'ok' : `missing ${validation.missing.length}`}`));
|
|
244
|
+
}
|