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/harness.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import crypto from 'crypto';
|
|
4
5
|
import pc from 'picocolors';
|
|
5
6
|
|
|
6
7
|
// ─── Cross-Platform Home Directory ───────────────────────────────────────────
|
|
@@ -169,33 +170,122 @@ export function printHarnessStatus(detected) {
|
|
|
169
170
|
* @param {string} label - Human-readable label for logging
|
|
170
171
|
* @returns {Promise<number>} - Number of items copied
|
|
171
172
|
*/
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
function sha256(value) {
|
|
174
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function hashPath(targetPath) {
|
|
178
|
+
if (!await fs.pathExists(targetPath)) return null;
|
|
179
|
+
const stat = await fs.stat(targetPath);
|
|
180
|
+
if (stat.isFile()) return sha256(await fs.readFile(targetPath));
|
|
181
|
+
|
|
182
|
+
const entries = [];
|
|
183
|
+
async function walk(current, prefix = '') {
|
|
184
|
+
const names = (await fs.readdir(current)).sort();
|
|
185
|
+
for (const name of names) {
|
|
186
|
+
const full = path.join(current, name);
|
|
187
|
+
const rel = path.join(prefix, name).replace(/\\/g, '/');
|
|
188
|
+
const st = await fs.stat(full);
|
|
189
|
+
if (st.isDirectory()) {
|
|
190
|
+
entries.push(`dir:${rel}`);
|
|
191
|
+
await walk(full, rel);
|
|
192
|
+
} else {
|
|
193
|
+
entries.push(`file:${rel}:${sha256(await fs.readFile(full))}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
await walk(targetPath);
|
|
198
|
+
return sha256(entries.join('\n'));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeOwnedMap(value) {
|
|
202
|
+
if (!value || typeof value !== 'object') return {};
|
|
203
|
+
const normalized = {};
|
|
204
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
205
|
+
if (typeof entry === 'string') normalized[name] = { hash: entry };
|
|
206
|
+
else if (entry?.hash) normalized[name] = entry;
|
|
207
|
+
}
|
|
208
|
+
return normalized;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Syncs a directory to a target path. When ownership is supplied, it also
|
|
213
|
+
* prunes previous Takomi-owned items that are no longer in the source while
|
|
214
|
+
* preserving manual or modified files.
|
|
215
|
+
*/
|
|
216
|
+
export async function syncDirectory(sourcePath, targetPath, label = '', options = {}) {
|
|
217
|
+
if (!targetPath) return options.returnDetails ? { copied: 0, pruned: 0, preservedManual: [], preservedModified: [], owned: {} } : 0;
|
|
174
218
|
|
|
175
219
|
try {
|
|
176
220
|
await fs.ensureDir(targetPath);
|
|
177
221
|
|
|
178
|
-
const items = await fs.readdir(sourcePath);
|
|
179
|
-
|
|
222
|
+
const items = (await fs.readdir(sourcePath)).sort();
|
|
223
|
+
const itemSet = new Set(items);
|
|
224
|
+
const previousOwned = normalizeOwnedMap(options.owned);
|
|
225
|
+
const nextOwned = {};
|
|
226
|
+
const preservedManual = [];
|
|
227
|
+
const preservedModified = [];
|
|
228
|
+
let copied = 0;
|
|
229
|
+
let pruned = 0;
|
|
230
|
+
|
|
231
|
+
if (options.prune && Object.keys(previousOwned).length > 0) {
|
|
232
|
+
for (const [item, ownedEntry] of Object.entries(previousOwned)) {
|
|
233
|
+
if (itemSet.has(item)) continue;
|
|
234
|
+
const dest = path.join(targetPath, item);
|
|
235
|
+
if (!await fs.pathExists(dest)) continue;
|
|
236
|
+
const currentHash = await hashPath(dest);
|
|
237
|
+
if (currentHash && currentHash === ownedEntry.hash) {
|
|
238
|
+
await fs.remove(dest);
|
|
239
|
+
pruned++;
|
|
240
|
+
} else {
|
|
241
|
+
preservedModified.push(item);
|
|
242
|
+
nextOwned[item] = ownedEntry;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
180
246
|
|
|
181
247
|
for (const item of items) {
|
|
182
248
|
const src = path.join(sourcePath, item);
|
|
183
249
|
const dest = path.join(targetPath, item);
|
|
250
|
+
const previous = previousOwned[item];
|
|
251
|
+
|
|
252
|
+
if (await fs.pathExists(dest)) {
|
|
253
|
+
const currentHash = await hashPath(dest);
|
|
254
|
+
if (options.preserveManual && !previous) {
|
|
255
|
+
preservedManual.push(item);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (previous?.hash && currentHash && currentHash !== previous.hash) {
|
|
259
|
+
preservedModified.push(item);
|
|
260
|
+
nextOwned[item] = previous;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
await fs.remove(dest);
|
|
264
|
+
}
|
|
184
265
|
|
|
185
266
|
await fs.copy(src, dest, { overwrite: true });
|
|
186
|
-
|
|
267
|
+
nextOwned[item] = {
|
|
268
|
+
hash: await hashPath(dest),
|
|
269
|
+
targetPath: dest,
|
|
270
|
+
syncedAt: new Date().toISOString(),
|
|
271
|
+
};
|
|
272
|
+
copied++;
|
|
187
273
|
}
|
|
188
274
|
|
|
189
275
|
if (label) {
|
|
190
|
-
|
|
276
|
+
const suffix = pruned ? `, removed ${pruned}` : '';
|
|
277
|
+
console.log(pc.green(` ✔ ${label} (${copied} items${suffix})`));
|
|
278
|
+
if (preservedManual.length) console.log(pc.yellow(` Preserved manual items: ${preservedManual.join(', ')}`));
|
|
279
|
+
if (preservedModified.length) console.log(pc.yellow(` Preserved modified Takomi-owned items: ${preservedModified.join(', ')}`));
|
|
191
280
|
}
|
|
192
281
|
|
|
193
|
-
|
|
282
|
+
const details = { copied, pruned, preservedManual, preservedModified, owned: Object.fromEntries(Object.entries(nextOwned).sort(([a], [b]) => a.localeCompare(b))) };
|
|
283
|
+
return options.returnDetails ? details : copied;
|
|
194
284
|
} catch (error) {
|
|
195
285
|
if (label) {
|
|
196
286
|
console.log(pc.red(` ✗ ${label}: ${error.message}`));
|
|
197
287
|
}
|
|
198
|
-
return 0;
|
|
288
|
+
return options.returnDetails ? { copied: 0, pruned: 0, preservedManual: [], preservedModified: [], owned: {} } : 0;
|
|
199
289
|
}
|
|
200
290
|
}
|
|
201
291
|
|
|
@@ -238,29 +328,55 @@ export async function syncFile(sourceFile, targetPaths, label = '') {
|
|
|
238
328
|
* @param {string} storePath - Path to the global store (~/.takomi/)
|
|
239
329
|
* @returns {Promise<{skills: number, workflows: number, yamls: number}>}
|
|
240
330
|
*/
|
|
241
|
-
export async function syncToHarness(harness, storePath) {
|
|
242
|
-
const results = { skills: 0, workflows: 0, yamls: 0 };
|
|
331
|
+
export async function syncToHarness(harness, storePath, options = {}) {
|
|
332
|
+
const results = { skills: 0, workflows: 0, yamls: 0, owned: { skills: {}, workflows: {} } };
|
|
333
|
+
const harnessOwned = options.owned?.[harness.id] || {};
|
|
334
|
+
const useOwnership = Boolean(options.useOwnership);
|
|
243
335
|
|
|
244
336
|
console.log(pc.cyan(`\n 📡 Syncing to ${harness.name}...`));
|
|
245
337
|
|
|
246
338
|
// Skills
|
|
247
339
|
const skillsStore = path.join(storePath, 'skills');
|
|
248
340
|
if (harness.targets.skills && await fs.pathExists(skillsStore)) {
|
|
249
|
-
|
|
341
|
+
const skillResult = await syncDirectory(
|
|
250
342
|
skillsStore,
|
|
251
343
|
harness.targets.skills,
|
|
252
|
-
`Skills → ${harness.name}
|
|
344
|
+
`Skills → ${harness.name}`,
|
|
345
|
+
{
|
|
346
|
+
owned: harnessOwned.skills,
|
|
347
|
+
preserveManual: useOwnership,
|
|
348
|
+
prune: useOwnership,
|
|
349
|
+
returnDetails: useOwnership,
|
|
350
|
+
},
|
|
253
351
|
);
|
|
352
|
+
if (useOwnership) {
|
|
353
|
+
results.skills = skillResult.copied;
|
|
354
|
+
results.owned.skills = skillResult.owned;
|
|
355
|
+
} else {
|
|
356
|
+
results.skills = skillResult;
|
|
357
|
+
}
|
|
254
358
|
}
|
|
255
359
|
|
|
256
360
|
// Workflows
|
|
257
361
|
const workflowsStore = path.join(storePath, 'workflows');
|
|
258
362
|
if (harness.targets.workflows && await fs.pathExists(workflowsStore)) {
|
|
259
|
-
|
|
363
|
+
const workflowResult = await syncDirectory(
|
|
260
364
|
workflowsStore,
|
|
261
365
|
harness.targets.workflows,
|
|
262
|
-
`Workflows → ${harness.name}
|
|
366
|
+
`Workflows → ${harness.name}`,
|
|
367
|
+
{
|
|
368
|
+
owned: harnessOwned.workflows,
|
|
369
|
+
preserveManual: useOwnership,
|
|
370
|
+
prune: useOwnership,
|
|
371
|
+
returnDetails: useOwnership,
|
|
372
|
+
},
|
|
263
373
|
);
|
|
374
|
+
if (useOwnership) {
|
|
375
|
+
results.workflows = workflowResult.copied;
|
|
376
|
+
results.owned.workflows = workflowResult.owned;
|
|
377
|
+
} else {
|
|
378
|
+
results.workflows = workflowResult;
|
|
379
|
+
}
|
|
264
380
|
}
|
|
265
381
|
|
|
266
382
|
// YAMLs (KiloCode custom_modes.yaml dual-path)
|
|
@@ -283,11 +399,11 @@ export async function syncToHarness(harness, storePath) {
|
|
|
283
399
|
* @param {string} storePath - Path to the global store (~/.takomi/)
|
|
284
400
|
* @returns {Promise<object>} - Summary of sync results
|
|
285
401
|
*/
|
|
286
|
-
export async function syncToAllHarnesses(harnesses, storePath) {
|
|
402
|
+
export async function syncToAllHarnesses(harnesses, storePath, options = {}) {
|
|
287
403
|
const summary = {};
|
|
288
404
|
|
|
289
405
|
for (const harness of harnesses) {
|
|
290
|
-
summary[harness.id] = await syncToHarness(harness, storePath);
|
|
406
|
+
summary[harness.id] = await syncToHarness(harness, storePath, options);
|
|
291
407
|
}
|
|
292
408
|
|
|
293
409
|
return summary;
|
package/src/pi-harness.js
CHANGED
|
@@ -220,6 +220,9 @@ export async function ensurePiInstalled() {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
const attempts = [
|
|
223
|
+
{ command: 'npm', args: ['install', '-g', '@earendil-works/pi-coding-agent'] },
|
|
224
|
+
{ command: 'npm.cmd', args: ['install', '-g', '@earendil-works/pi-coding-agent'] },
|
|
225
|
+
// Legacy fallback for older environments that still publish under the original namespace.
|
|
223
226
|
{ command: 'npm', args: ['install', '-g', '@mariozechner/pi-coding-agent'] },
|
|
224
227
|
{ command: 'npm.cmd', args: ['install', '-g', '@mariozechner/pi-coding-agent'] },
|
|
225
228
|
];
|
|
@@ -301,19 +304,23 @@ export async function updatePiManagedPackages() {
|
|
|
301
304
|
const pi = await detectPiCommand();
|
|
302
305
|
if (!pi.installed) return { ok: false, changed: false, report: 'Pi is not installed.' };
|
|
303
306
|
|
|
307
|
+
// `pi update` reconciles every package listed in Pi settings, including
|
|
308
|
+
// user-installed npm/git/local packages. Keep this broad so Takomi refresh
|
|
309
|
+
// updates old, new, custom, and optional feature packages without needing to
|
|
310
|
+
// know their names ahead of time.
|
|
304
311
|
const command = pi.path || 'pi';
|
|
305
312
|
const result = runCommand(command, ['update']);
|
|
306
313
|
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
307
314
|
return {
|
|
308
315
|
ok: result.status === 0,
|
|
309
316
|
changed: result.status === 0,
|
|
310
|
-
report: output || (result.status === 0 ? 'Pi
|
|
317
|
+
report: output || (result.status === 0 ? 'All Pi-managed packages are up to date.' : 'pi update failed.'),
|
|
311
318
|
};
|
|
312
319
|
}
|
|
313
320
|
|
|
314
321
|
export function printPiManagedPackageUpdateResult(result) {
|
|
315
322
|
if (result.ok) {
|
|
316
|
-
console.log(pc.green('✔ Updated Pi-managed packages'));
|
|
323
|
+
console.log(pc.green('✔ Updated all Pi-managed packages'));
|
|
317
324
|
if (result.report) console.log(pc.dim(result.report.split(/\r?\n/).slice(-8).join('\n')));
|
|
318
325
|
return;
|
|
319
326
|
}
|
|
@@ -342,18 +349,32 @@ export async function inspectPiHarnessEnvironment(cwd = process.cwd()) {
|
|
|
342
349
|
};
|
|
343
350
|
}
|
|
344
351
|
|
|
352
|
+
function printFirstRunGuidance(reason) {
|
|
353
|
+
console.log(pc.magenta('\n🎯 Welcome to Takomi\n'));
|
|
354
|
+
if (reason) console.log(pc.yellow(reason));
|
|
355
|
+
console.log(pc.white('\nRecommended first step:'));
|
|
356
|
+
console.log(pc.cyan(' takomi setup pi'));
|
|
357
|
+
console.log(pc.dim(' Set up the Pi-native Takomi harness.'));
|
|
358
|
+
console.log(pc.white('\nOptional setup:'));
|
|
359
|
+
console.log(pc.dim(' takomi setup pi-features Add optional Pi feature packs'));
|
|
360
|
+
console.log(pc.dim(' takomi setup skills Install global Takomi skills'));
|
|
361
|
+
console.log(pc.dim(' takomi setup all Set up Pi + skills'));
|
|
362
|
+
console.log(pc.white('\nDiagnostics and help:'));
|
|
363
|
+
console.log(pc.dim(' takomi doctor Check installation health'));
|
|
364
|
+
console.log(pc.dim(' takomi status Show connected harnesses/toolkit status'));
|
|
365
|
+
console.log(pc.dim(' takomi --help Show all commands\n'));
|
|
366
|
+
}
|
|
367
|
+
|
|
345
368
|
export async function launchTakomiHarness(cwd = process.cwd()) {
|
|
346
369
|
const report = await inspectPiHarnessEnvironment(cwd);
|
|
347
370
|
|
|
348
371
|
if (!report.pi.installed) {
|
|
349
|
-
|
|
350
|
-
console.log(pc.dim('Run: takomi install pi'));
|
|
372
|
+
printFirstRunGuidance('Pi is not installed yet.');
|
|
351
373
|
return 1;
|
|
352
374
|
}
|
|
353
375
|
|
|
354
376
|
if (!report.installed.runtimeInstalled || !report.installed.subagentsInstalled) {
|
|
355
|
-
|
|
356
|
-
console.log(pc.dim('Run: takomi install pi'));
|
|
377
|
+
printFirstRunGuidance('Takomi Pi harness is not fully installed yet.');
|
|
357
378
|
return 1;
|
|
358
379
|
}
|
|
359
380
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { detectPiCommand, getPiGlobalTargets } from './pi-harness.js';
|
|
6
|
+
|
|
7
|
+
export const TAKOMI_PI_OPTIONAL_FEATURES = [
|
|
8
|
+
{
|
|
9
|
+
id: 'takomi-interview',
|
|
10
|
+
title: 'Takomi Interview',
|
|
11
|
+
packageSpec: 'npm:@juicesharp/rpiv-ask-user-question',
|
|
12
|
+
npmPackage: '@juicesharp/rpiv-ask-user-question',
|
|
13
|
+
selectedByDefault: true,
|
|
14
|
+
recommended: true,
|
|
15
|
+
description: 'Structured model questions for Genesis/design clarification. Default Takomi add-on.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'takomi-todo',
|
|
19
|
+
title: 'Takomi Todo',
|
|
20
|
+
packageSpec: 'npm:@juicesharp/rpiv-todo',
|
|
21
|
+
npmPackage: '@juicesharp/rpiv-todo',
|
|
22
|
+
selectedByDefault: false,
|
|
23
|
+
recommended: false,
|
|
24
|
+
description: 'Live model todo overlay. Optional while it is being tested.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'takomi-browser-qa',
|
|
28
|
+
title: 'Takomi Browser QA',
|
|
29
|
+
packageSpec: 'npm:pi-chrome',
|
|
30
|
+
npmPackage: 'pi-chrome',
|
|
31
|
+
selectedByDefault: false,
|
|
32
|
+
recommended: false,
|
|
33
|
+
description: 'Chrome/browser automation for UI QA and authenticated web workflows.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'takomi-doc-preview',
|
|
37
|
+
title: 'Takomi Doc Preview',
|
|
38
|
+
packageSpec: 'npm:pi-markdown-preview',
|
|
39
|
+
npmPackage: 'pi-markdown-preview',
|
|
40
|
+
selectedByDefault: false,
|
|
41
|
+
recommended: false,
|
|
42
|
+
description: 'Markdown/LaTeX/browser/PDF previews for Takomi docs and handoffs.',
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
function runCommand(command, args) {
|
|
47
|
+
return spawnSync(command, args, { stdio: 'pipe', encoding: 'utf8', shell: process.platform === 'win32' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function packageIdentity(packageSpec) {
|
|
51
|
+
if (!packageSpec.startsWith('npm:')) return packageSpec;
|
|
52
|
+
const withoutPrefix = packageSpec.slice(4);
|
|
53
|
+
if (withoutPrefix.startsWith('@')) {
|
|
54
|
+
const parts = withoutPrefix.split('@');
|
|
55
|
+
return `${parts[0]}@${parts[1]}`;
|
|
56
|
+
}
|
|
57
|
+
return withoutPrefix.split('@')[0];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function packageEntrySource(entry) {
|
|
61
|
+
if (typeof entry === 'string') return entry;
|
|
62
|
+
if (entry && typeof entry === 'object' && typeof entry.source === 'string') return entry.source;
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function packageMatches(entry, packageSpec) {
|
|
67
|
+
const source = packageEntrySource(entry);
|
|
68
|
+
if (!source) return false;
|
|
69
|
+
if (source === packageSpec) return true;
|
|
70
|
+
if (!source.startsWith('npm:') || !packageSpec.startsWith('npm:')) return false;
|
|
71
|
+
return packageIdentity(source) === packageIdentity(packageSpec);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readGlobalPiSettings() {
|
|
75
|
+
try {
|
|
76
|
+
return await fs.readJson(getPiGlobalTargets().settings);
|
|
77
|
+
} catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function getInstalledPiPackageSpecs() {
|
|
83
|
+
const settings = await readGlobalPiSettings();
|
|
84
|
+
return Array.isArray(settings.packages) ? settings.packages : [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function isPiPackageInstalled(packageSpec) {
|
|
88
|
+
const packages = await getInstalledPiPackageSpecs();
|
|
89
|
+
return packages.some((entry) => packageMatches(entry, packageSpec));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function installPiPackage(packageSpec) {
|
|
93
|
+
const pi = await detectPiCommand();
|
|
94
|
+
if (!pi.installed) return { ok: false, changed: false, report: 'Pi is not installed.' };
|
|
95
|
+
|
|
96
|
+
if (await isPiPackageInstalled(packageSpec)) {
|
|
97
|
+
return { ok: true, changed: false, report: `${packageSpec} already listed in Pi settings.` };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const command = pi.path || 'pi';
|
|
101
|
+
const result = runCommand(command, ['install', packageSpec]);
|
|
102
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
103
|
+
return {
|
|
104
|
+
ok: result.status === 0,
|
|
105
|
+
changed: result.status === 0,
|
|
106
|
+
report: output || (result.status === 0 ? `Installed ${packageSpec}.` : `pi install ${packageSpec} failed.`),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function installPiOptionalFeatures(featureIds) {
|
|
111
|
+
const selected = TAKOMI_PI_OPTIONAL_FEATURES.filter((feature) => featureIds.includes(feature.id));
|
|
112
|
+
const results = [];
|
|
113
|
+
for (const feature of selected) {
|
|
114
|
+
const result = await installPiPackage(feature.packageSpec);
|
|
115
|
+
results.push({ feature, ...result });
|
|
116
|
+
}
|
|
117
|
+
return results;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function printPiOptionalFeatureResults(results) {
|
|
121
|
+
if (!results.length) {
|
|
122
|
+
console.log(pc.dim(' Optional Pi features skipped.'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const result of results) {
|
|
127
|
+
const icon = result.ok ? (result.changed ? pc.green('✔') : pc.dim('•')) : pc.yellow('⚠');
|
|
128
|
+
const action = result.ok ? (result.changed ? 'Installed' : 'Already installed') : 'Could not install';
|
|
129
|
+
console.log(`${icon} ${action} ${result.feature.title} (${result.feature.packageSpec})`);
|
|
130
|
+
if (result.report) console.log(pc.dim(result.report.split(/\r?\n/).slice(-4).join('\n')));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function selectPiOptionalFeatures(options = {}) {
|
|
135
|
+
const { interactive = true } = options;
|
|
136
|
+
if (!interactive) {
|
|
137
|
+
return TAKOMI_PI_OPTIONAL_FEATURES
|
|
138
|
+
.filter((feature) => feature.selectedByDefault)
|
|
139
|
+
.map((feature) => feature.id);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const modeResponse = await prompts({
|
|
143
|
+
type: 'select',
|
|
144
|
+
name: 'mode',
|
|
145
|
+
message: 'Optional Pi features to install?',
|
|
146
|
+
choices: [
|
|
147
|
+
{ title: 'Recommended defaults', value: 'recommended', description: 'Takomi Interview now; keep other packs optional' },
|
|
148
|
+
{ title: 'Select all', value: 'all', description: 'Install every official optional Takomi Pi pack' },
|
|
149
|
+
{ title: 'Manual selection', value: 'manual', description: 'Choose individual optional packages' },
|
|
150
|
+
{ title: 'Skip optional features', value: 'skip', description: 'Only install the Takomi core harness' },
|
|
151
|
+
],
|
|
152
|
+
initial: 0,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!modeResponse.mode || modeResponse.mode === 'skip') return [];
|
|
156
|
+
if (modeResponse.mode === 'all') return TAKOMI_PI_OPTIONAL_FEATURES.map((feature) => feature.id);
|
|
157
|
+
if (modeResponse.mode === 'recommended') {
|
|
158
|
+
return TAKOMI_PI_OPTIONAL_FEATURES
|
|
159
|
+
.filter((feature) => feature.selectedByDefault || feature.recommended)
|
|
160
|
+
.map((feature) => feature.id);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const manualResponse = await prompts({
|
|
164
|
+
type: 'multiselect',
|
|
165
|
+
name: 'features',
|
|
166
|
+
message: 'Select optional Takomi Pi feature packs:',
|
|
167
|
+
choices: TAKOMI_PI_OPTIONAL_FEATURES.map((feature) => ({
|
|
168
|
+
title: feature.title,
|
|
169
|
+
value: feature.id,
|
|
170
|
+
selected: feature.selectedByDefault,
|
|
171
|
+
description: `${feature.description} (${feature.packageSpec})`,
|
|
172
|
+
})),
|
|
173
|
+
hint: '- Space to select. Press a to toggle all. Return to submit',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return manualResponse.features || [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function offerPiOptionalFeatures(options = {}) {
|
|
180
|
+
const featureIds = await selectPiOptionalFeatures(options);
|
|
181
|
+
const results = await installPiOptionalFeatures(featureIds);
|
|
182
|
+
printPiOptionalFeatureResults(results);
|
|
183
|
+
return results;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function renderPiOptionalFeatureCatalog() {
|
|
187
|
+
return TAKOMI_PI_OPTIONAL_FEATURES.map((feature) => ({
|
|
188
|
+
id: feature.id,
|
|
189
|
+
title: feature.title,
|
|
190
|
+
packageSpec: feature.packageSpec,
|
|
191
|
+
selectedByDefault: feature.selectedByDefault,
|
|
192
|
+
recommended: feature.recommended,
|
|
193
|
+
description: feature.description,
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
try {
|
|
2
|
+
if (process.env.TAKOMI_SUPPRESS_POSTINSTALL === '1') process.exit(0);
|
|
3
|
+
if (process.env.CI === 'true' || process.env.CI === '1') process.exit(0);
|
|
4
|
+
|
|
5
|
+
const lines = [
|
|
6
|
+
'',
|
|
7
|
+
'🎯 Takomi installed.',
|
|
8
|
+
'',
|
|
9
|
+
'Recommended next steps:',
|
|
10
|
+
' takomi setup pi Set up the Pi-native Takomi harness',
|
|
11
|
+
' takomi setup pi-features Add optional Pi feature packs',
|
|
12
|
+
' takomi setup skills Install global Takomi skills',
|
|
13
|
+
' takomi setup all Set up Pi + skills',
|
|
14
|
+
'',
|
|
15
|
+
'Useful commands:',
|
|
16
|
+
' takomi doctor Check installation health',
|
|
17
|
+
' takomi status Show connected harnesses/toolkit status',
|
|
18
|
+
' takomi refresh Update Takomi, Pi assets, skills, and Pi packages',
|
|
19
|
+
' takomi --help Show all commands',
|
|
20
|
+
'',
|
|
21
|
+
'Then run `takomi` from inside a project.',
|
|
22
|
+
'',
|
|
23
|
+
];
|
|
24
|
+
console.log(lines.join('\n'));
|
|
25
|
+
} catch {
|
|
26
|
+
// Postinstall guidance must never break package installation.
|
|
27
|
+
}
|