@meistrari/tela-cli 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/ingredients.js +1 -22
- package/dist/commands/skills.d.ts +2 -19
- package/dist/commands/skills.js +29 -251
- package/dist/index.js +3247 -2157
- package/package.json +1 -1
- package/dist/skills.d.ts +0 -87
- package/dist/skills.js +0 -303
|
@@ -145,7 +145,7 @@ async function callIngredientApi(args, _options, io) {
|
|
|
145
145
|
const url = new URL((0, tela_pantry_1.joinUrl)(environment.url, routePath));
|
|
146
146
|
for (const [key, value] of request.queryParams)
|
|
147
147
|
url.searchParams.append(key, value);
|
|
148
|
-
for (const [key, value] of Object.entries(
|
|
148
|
+
for (const [key, value] of Object.entries((0, tela_pantry_1.serviceCredentialHeaders)(targetSession.credential))) {
|
|
149
149
|
if (!request.headers.has(key))
|
|
150
150
|
request.headers.set(key, value);
|
|
151
151
|
}
|
|
@@ -368,27 +368,6 @@ async function loadIngredient(slug, flags) {
|
|
|
368
368
|
const { client } = await (0, session_1.loadPantry)(flags);
|
|
369
369
|
return { client, detail: await (0, tela_pantry_1.fetchIngredientDetail)(client, slug) };
|
|
370
370
|
}
|
|
371
|
-
/**
|
|
372
|
-
* Only `tela service api` still builds its own headers — every Pantry route now
|
|
373
|
-
* goes through the generated client, which sends `Authorization: Bearer` and
|
|
374
|
-
* nothing else. The `x-data-token` branch stays because arbitrary ingredient
|
|
375
|
-
* APIs may accept it; Pantry itself demonstrably does not.
|
|
376
|
-
*/
|
|
377
|
-
function requestHeaders(credential) {
|
|
378
|
-
if (credential.kind === 'data-token' && credential.dataToken) {
|
|
379
|
-
return {
|
|
380
|
-
'Accept': 'application/json',
|
|
381
|
-
'x-data-token': credential.dataToken,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
if (credential.accessToken) {
|
|
385
|
-
return {
|
|
386
|
-
Accept: 'application/json',
|
|
387
|
-
Authorization: `Bearer ${credential.accessToken}`,
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
throw new Error(`Credential ${credential.kind} does not contain usable token material`);
|
|
391
|
-
}
|
|
392
371
|
async function fetchIngredientCatalog(credential, baseUrl = tela_pantry_1.DEFAULT_PANTRY_BASE_URL) {
|
|
393
372
|
return await (0, tela_pantry_1.fetchIngredientCatalog)((0, tela_pantry_1.createPantryClient)(baseUrl, credential), baseUrl);
|
|
394
373
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PantrySkillManifestEntry, PantrySkillSummary } from '@meistrari/tela-pantry';
|
|
1
|
+
import type { PantrySkillManifestEntry, PantrySkillSummary, SkillManagerDiagnostic } from '@meistrari/tela-pantry';
|
|
2
2
|
import type { InstalledSkill } from '@meistrari/tela-local-config';
|
|
3
3
|
import type { CliIo, GlobalOptions } from '../output';
|
|
4
4
|
export declare function runSkillsCommand(args: string[], options: GlobalOptions, io: CliIo): Promise<number>;
|
|
@@ -12,22 +12,5 @@ export declare function annotateInstalled(skills: readonly PantrySkillSummary[],
|
|
|
12
12
|
export declare function formatSkillList(entries: readonly SkillListEntry[]): string[];
|
|
13
13
|
export declare function formatManifestTree(entries: readonly PantrySkillManifestEntry[]): string[];
|
|
14
14
|
export declare function formatBytes(size: number): string;
|
|
15
|
-
|
|
16
|
-
* No slugs means every installed skill: upgrading is additive, and "all of
|
|
17
|
-
* them" is the only reading of a bare `skills upgrade`.
|
|
18
|
-
*
|
|
19
|
-
* A named slug the registry does not know is refused for the whole command
|
|
20
|
-
* rather than skipped, so a typo among several cannot leave some skills
|
|
21
|
-
* upgraded and report success.
|
|
22
|
-
*/
|
|
23
|
-
export declare function selectUpgradeTargets(installed: readonly InstalledSkill[], slugs: readonly string[]): InstalledSkill[];
|
|
24
|
-
/**
|
|
25
|
-
* Is this the same installation, rather than one that replaced it?
|
|
26
|
-
*
|
|
27
|
-
* `slug` and `dir` are both derived from the slug, so neither moves across a
|
|
28
|
-
* reinstall; `installedAt` and `sha` are what actually distinguish one install
|
|
29
|
-
* from the next.
|
|
30
|
-
*/
|
|
31
|
-
export declare function isSameInstall(left: InstalledSkill, right: InstalledSkill): boolean;
|
|
32
|
-
export declare function scriptFailureMessage(entry: Pick<InstalledSkill, 'slug' | 'dir'>, phase: 'install' | 'uninstall'): string;
|
|
15
|
+
export declare function writeSkillManagerDiagnostic(io: CliIo, diagnostic: SkillManagerDiagnostic): void;
|
|
33
16
|
export {};
|
package/dist/commands/skills.js
CHANGED
|
@@ -5,16 +5,12 @@ exports.annotateInstalled = annotateInstalled;
|
|
|
5
5
|
exports.formatSkillList = formatSkillList;
|
|
6
6
|
exports.formatManifestTree = formatManifestTree;
|
|
7
7
|
exports.formatBytes = formatBytes;
|
|
8
|
-
exports.
|
|
9
|
-
exports.isSameInstall = isSameInstall;
|
|
10
|
-
exports.scriptFailureMessage = scriptFailureMessage;
|
|
11
|
-
const promises_1 = require("node:fs/promises");
|
|
8
|
+
exports.writeSkillManagerDiagnostic = writeSkillManagerDiagnostic;
|
|
12
9
|
const tela_pantry_1 = require("@meistrari/tela-pantry");
|
|
13
10
|
const tela_local_config_1 = require("@meistrari/tela-local-config");
|
|
14
11
|
const markdown_1 = require("../markdown");
|
|
15
12
|
const output_1 = require("../output");
|
|
16
13
|
const session_1 = require("../session");
|
|
17
|
-
const skills_1 = require("../skills");
|
|
18
14
|
async function runSkillsCommand(args, options, io) {
|
|
19
15
|
const [command, ...rest] = args;
|
|
20
16
|
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
@@ -172,18 +168,15 @@ async function installSkills(args, options, io) {
|
|
|
172
168
|
return usageError(flags.error, io);
|
|
173
169
|
if (flags.rest.length === 0)
|
|
174
170
|
return usageError('Usage: tela skills install <slug...> [--force] [--no-symlink] [--ignore-scripts]', io);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (entry)
|
|
185
|
-
installed.push(entry);
|
|
186
|
-
}
|
|
171
|
+
const manager = (0, tela_pantry_1.createSkillManager)({
|
|
172
|
+
getClient: async () => (await (0, session_1.loadPantry)(flags)).client,
|
|
173
|
+
onDiagnostic: diagnostic => writeSkillManagerDiagnostic(io, diagnostic),
|
|
174
|
+
});
|
|
175
|
+
const { installed } = await manager.installSkills(flags.rest, {
|
|
176
|
+
force: flags.force,
|
|
177
|
+
ignoreScripts: flags.ignoreScripts,
|
|
178
|
+
...(flags.symlink ? {} : { targets: [] }),
|
|
179
|
+
});
|
|
187
180
|
if (options.json) {
|
|
188
181
|
(0, output_1.writeJson)(io, { installed });
|
|
189
182
|
return 0;
|
|
@@ -192,148 +185,29 @@ async function installSkills(args, options, io) {
|
|
|
192
185
|
(0, output_1.writeLine)(io, `Installed ${entry.slug}@${entry.sha.slice(0, 7)} to ${entry.dir}`);
|
|
193
186
|
return 0;
|
|
194
187
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const target = (0, skills_1.resolveInstallDir)(roots, slug);
|
|
198
|
-
const { ingredient, snapshot } = await (0, tela_pantry_1.fetchSkillFile)(pantry.client, slug);
|
|
199
|
-
// Verified before anything is written: the order is the security property.
|
|
200
|
-
// Nothing is removed from disk until every file in the archive has been
|
|
201
|
-
// checked against the manifest, so a tampered or truncated download can
|
|
202
|
-
// never delete a working install. Downloading outside the lock also keeps
|
|
203
|
-
// the network round-trip out of the critical section.
|
|
204
|
-
const files = (0, tela_pantry_1.verifySnapshotFiles)((0, tela_pantry_1.parseTarGz)(await (0, tela_pantry_1.downloadSkillArchive)(snapshot.archiveUrl)), snapshot.manifest);
|
|
205
|
-
const stagedInstalls = [];
|
|
206
|
-
let skippedReason = null;
|
|
207
|
-
let entry = null;
|
|
208
|
-
try {
|
|
209
|
-
entry = await (0, tela_local_config_1.updateInstalledSkill)(slug, async (registered) => {
|
|
210
|
-
if (expectedCurrent && (registered === null || !isSameInstall(registered, expectedCurrent))) {
|
|
211
|
-
skippedReason = `Skipped ${slug}: it was changed while this upgrade ran`;
|
|
212
|
-
return registered;
|
|
213
|
-
}
|
|
214
|
-
// Re-checked against the registry as it stands inside the lock. Read
|
|
215
|
-
// before acquiring it, this guard would answer for a moment that has
|
|
216
|
-
// passed by the time the files are written.
|
|
217
|
-
//
|
|
218
|
-
// An upgrade needs no exemption here: the entry it is upgrading names
|
|
219
|
-
// this very directory, so the guard already recognises it as ours.
|
|
220
|
-
if ((0, skills_1.isUnownedInstallDir)(target, registered?.dir ?? null) && !flags.force) {
|
|
221
|
-
throw new Error(`${target} already exists and was not installed by tela.\n`
|
|
222
|
-
+ `Run \`tela skills install ${slug} --force\` to replace it, or remove it by hand.`);
|
|
223
|
-
}
|
|
224
|
-
const staged = await (0, skills_1.stageSkillFiles)(target, files);
|
|
225
|
-
stagedInstalls.push(staged);
|
|
226
|
-
const link = flags.symlink
|
|
227
|
-
? await (0, skills_1.linkSkill)(roots, slug, target, flags.force)
|
|
228
|
-
: await removeRecordedSymlinks(registered?.symlinks ?? [], target, io);
|
|
229
|
-
if (link.skipped)
|
|
230
|
-
(0, output_1.writeError)(io, `Skipped symlink: ${link.skipped}`);
|
|
231
|
-
return {
|
|
232
|
-
slug,
|
|
233
|
-
dir: target,
|
|
234
|
-
sha: snapshot.sha,
|
|
235
|
-
bundle: ingredient.bundle,
|
|
236
|
-
symlinks: link.linked,
|
|
237
|
-
scripts: ingredient.scripts,
|
|
238
|
-
installedAt: new Date().toISOString(),
|
|
239
|
-
};
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
catch (error) {
|
|
243
|
-
await rollbackStagedInstall(stagedInstalls.at(-1) ?? null, error);
|
|
244
|
-
}
|
|
245
|
-
if (skippedReason) {
|
|
246
|
-
(0, output_1.writeError)(io, skippedReason);
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
for (const staged of stagedInstalls)
|
|
250
|
-
await staged.commit();
|
|
251
|
-
if (!entry)
|
|
252
|
-
return null;
|
|
253
|
-
// Outside the lock on purpose: the script is arbitrary shell from the
|
|
254
|
-
// catalog and may run for as long as it likes, which is exactly what must
|
|
255
|
-
// not happen while every other tela process waits on the inventory.
|
|
256
|
-
//
|
|
257
|
-
// That leaves a window, knowingly: a concurrent install of the same slug
|
|
258
|
-
// can replace this directory once the lock is released, and this script
|
|
259
|
-
// would then run against the newer install's files. Reaching it takes two
|
|
260
|
-
// tela processes on one slug, overlapping, a skill that declares an install
|
|
261
|
-
// script, and two snapshots whose scripts differ enough for the older to do
|
|
262
|
-
// damage. Guarding it was tried and reverted: the check costs a registry
|
|
263
|
-
// read on every install to catch only the case where the replacement has
|
|
264
|
-
// already finished, and a replacement landing mid-script is undetectable
|
|
265
|
-
// without holding the lock across arbitrary shell — which the refresh now
|
|
266
|
-
// sustains for as long as the script runs, so every other tela process
|
|
267
|
-
// would wait behind it instead of the lock ageing out.
|
|
268
|
-
await runLifecycleScript(entry, 'install', flags.ignoreScripts, io);
|
|
269
|
-
return entry;
|
|
270
|
-
}
|
|
271
|
-
async function removeRecordedSymlinks(symlinks, target, io) {
|
|
272
|
-
for (const linkPath of symlinks) {
|
|
273
|
-
const outcome = await (0, skills_1.removeManagedSymlink)(linkPath, target);
|
|
274
|
-
if (outcome.reason)
|
|
275
|
-
(0, output_1.writeError)(io, outcome.reason);
|
|
276
|
-
}
|
|
277
|
-
return { linked: [], skipped: null };
|
|
278
|
-
}
|
|
279
|
-
async function rollbackStagedInstall(staged, error) {
|
|
280
|
-
if (!staged)
|
|
281
|
-
throw error;
|
|
282
|
-
try {
|
|
283
|
-
await staged.rollback();
|
|
284
|
-
}
|
|
285
|
-
catch (cleanupError) {
|
|
286
|
-
throw new Error(`${formatError(error)}\nCleanup failed: ${formatError(cleanupError)}`);
|
|
287
|
-
}
|
|
288
|
-
throw error;
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* No slugs means every installed skill: upgrading is additive, and "all of
|
|
292
|
-
* them" is the only reading of a bare `skills upgrade`.
|
|
293
|
-
*
|
|
294
|
-
* A named slug the registry does not know is refused for the whole command
|
|
295
|
-
* rather than skipped, so a typo among several cannot leave some skills
|
|
296
|
-
* upgraded and report success.
|
|
297
|
-
*/
|
|
298
|
-
function selectUpgradeTargets(installed, slugs) {
|
|
299
|
-
if (slugs.length === 0)
|
|
300
|
-
return [...installed];
|
|
301
|
-
const bySlug = new Map(installed.map(entry => [entry.slug, entry]));
|
|
302
|
-
const missing = slugs.filter(slug => !bySlug.has(slug));
|
|
303
|
-
if (missing.length > 0)
|
|
304
|
-
throw new Error(`${missing.join(', ')} not installed by tela. Nothing to upgrade.`);
|
|
305
|
-
return slugs.map(slug => bySlug.get(slug));
|
|
188
|
+
function writeSkillManagerDiagnostic(io, diagnostic) {
|
|
189
|
+
(0, output_1.writeError)(io, diagnostic.message);
|
|
306
190
|
}
|
|
307
191
|
async function upgradeSkills(args, options, io) {
|
|
308
192
|
const flags = parseSkillFlags(args);
|
|
309
193
|
if ('error' in flags)
|
|
310
194
|
return usageError(flags.error, io);
|
|
311
|
-
const
|
|
312
|
-
|
|
195
|
+
const manager = (0, tela_pantry_1.createSkillManager)({
|
|
196
|
+
getClient: async () => (await (0, session_1.loadPantry)(flags)).client,
|
|
197
|
+
onDiagnostic: diagnostic => writeSkillManagerDiagnostic(io, diagnostic),
|
|
198
|
+
});
|
|
199
|
+
const { upgraded, unchanged } = await manager.upgradeSkills(flags.rest, {
|
|
200
|
+
force: flags.force,
|
|
201
|
+
ignoreScripts: flags.ignoreScripts,
|
|
202
|
+
...(flags.symlink ? {} : { targets: [] }),
|
|
203
|
+
});
|
|
204
|
+
if (upgraded.length === 0 && unchanged.length === 0) {
|
|
313
205
|
if (options.json)
|
|
314
206
|
(0, output_1.writeJson)(io, { upgraded: [], unchanged: [] });
|
|
315
207
|
else
|
|
316
208
|
(0, output_1.writeLine)(io, 'No skills installed.');
|
|
317
209
|
return 0;
|
|
318
210
|
}
|
|
319
|
-
const pantry = await (0, session_1.loadPantry)(flags);
|
|
320
|
-
const upgraded = [];
|
|
321
|
-
const unchanged = [];
|
|
322
|
-
for (const entry of targets) {
|
|
323
|
-
const { snapshot } = await (0, tela_pantry_1.fetchSkillFile)(pantry.client, entry.slug);
|
|
324
|
-
// Same snapshot means the same bytes: reinstalling would re-run the
|
|
325
|
-
// install script for no change.
|
|
326
|
-
if (snapshot.sha === entry.sha) {
|
|
327
|
-
unchanged.push(entry.slug);
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
// `flags` as the user gave them. Synthesising `force: true` here would
|
|
331
|
-
// reach past the install directory the upgrade owns and into the link
|
|
332
|
-
// path, where a directory another tool put there would be deleted.
|
|
333
|
-
const upgradedEntry = await installOne(pantry, entry.slug, flags, io, entry);
|
|
334
|
-
if (upgradedEntry)
|
|
335
|
-
upgraded.push(upgradedEntry);
|
|
336
|
-
}
|
|
337
211
|
if (options.json) {
|
|
338
212
|
(0, output_1.writeJson)(io, { upgraded, unchanged });
|
|
339
213
|
return 0;
|
|
@@ -350,57 +224,13 @@ async function uninstallSkills(args, options, io) {
|
|
|
350
224
|
return usageError(flags.error, io);
|
|
351
225
|
if (flags.rest.length === 0)
|
|
352
226
|
return usageError('Usage: tela skills uninstall <slug...> [--ignore-scripts]', io);
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return usageError(`${missing.join(', ')} not installed by tela. Nothing to remove.`, io);
|
|
361
|
-
const removed = [];
|
|
362
|
-
const roots = (0, skills_1.resolveSkillRoots)();
|
|
363
|
-
for (const slug of flags.rest) {
|
|
364
|
-
// Registry-driven: an unregistered slug never reaches this loop, so a
|
|
365
|
-
// recursive delete can only ever be pointed at a directory tela
|
|
366
|
-
// recorded writing.
|
|
367
|
-
const entry = bySlug.get(slug);
|
|
368
|
-
if (!entry)
|
|
369
|
-
continue;
|
|
370
|
-
assertManagedInstallDir(slug, entry, roots);
|
|
371
|
-
// Before the lock: the script is arbitrary shell, it runs in the
|
|
372
|
-
// directory about to be deleted, and holding the inventory for its
|
|
373
|
-
// duration would block every other tela process.
|
|
374
|
-
await runLifecycleScript(entry, 'uninstall', flags.ignoreScripts, io);
|
|
375
|
-
let gone = false;
|
|
376
|
-
await (0, tela_local_config_1.updateInstalledSkill)(slug, async (registered) => {
|
|
377
|
-
// The entry read before the script ran may be stale: a concurrent
|
|
378
|
-
// uninstall already finished, or a concurrent install replaced it.
|
|
379
|
-
if (registered === null)
|
|
380
|
-
return null;
|
|
381
|
-
// Compared whole rather than by `dir`, which is derived from the
|
|
382
|
-
// slug and so is the same string for a reinstall — it cannot tell
|
|
383
|
-
// "still the entry whose script just ran" from "someone reinstalled
|
|
384
|
-
// while it ran". A reinstall carries a fresh `installedAt`, and an
|
|
385
|
-
// entry matching in every field describes the same sha recorded at
|
|
386
|
-
// the same instant, which this uninstall is entitled to remove.
|
|
387
|
-
if (!isSameInstall(registered, entry)) {
|
|
388
|
-
(0, output_1.writeError)(io, `Skipped ${slug}: it was reinstalled while this uninstall ran`);
|
|
389
|
-
return registered;
|
|
390
|
-
}
|
|
391
|
-
assertManagedInstallDir(slug, registered, roots);
|
|
392
|
-
for (const linkPath of registered.symlinks) {
|
|
393
|
-
const outcome = await (0, skills_1.removeManagedSymlink)(linkPath, registered.dir);
|
|
394
|
-
if (outcome.reason)
|
|
395
|
-
(0, output_1.writeError)(io, outcome.reason);
|
|
396
|
-
}
|
|
397
|
-
await (0, promises_1.rm)(registered.dir, { recursive: true, force: true });
|
|
398
|
-
gone = true;
|
|
399
|
-
return null;
|
|
400
|
-
});
|
|
401
|
-
if (gone)
|
|
402
|
-
removed.push(slug);
|
|
403
|
-
}
|
|
227
|
+
const manager = (0, tela_pantry_1.createSkillManager)({
|
|
228
|
+
getClient: async () => (await (0, session_1.loadPantry)(flags)).client,
|
|
229
|
+
onDiagnostic: diagnostic => writeSkillManagerDiagnostic(io, diagnostic),
|
|
230
|
+
});
|
|
231
|
+
const { removed } = await manager.uninstallSkills(flags.rest, {
|
|
232
|
+
ignoreScripts: flags.ignoreScripts,
|
|
233
|
+
});
|
|
404
234
|
if (options.json) {
|
|
405
235
|
(0, output_1.writeJson)(io, { removed });
|
|
406
236
|
return 0;
|
|
@@ -409,58 +239,6 @@ async function uninstallSkills(args, options, io) {
|
|
|
409
239
|
(0, output_1.writeLine)(io, `Removed ${slug}`);
|
|
410
240
|
return 0;
|
|
411
241
|
}
|
|
412
|
-
function assertManagedInstallDir(slug, entry, roots) {
|
|
413
|
-
const expectedDir = (0, skills_1.resolveInstallDir)(roots, slug);
|
|
414
|
-
if (entry.slug !== slug || entry.dir !== expectedDir) {
|
|
415
|
-
throw new Error(`${slug} is recorded at ${entry.dir}, not the managed install directory ${expectedDir}. `
|
|
416
|
-
+ 'Refusing to remove it.');
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* Is this the same installation, rather than one that replaced it?
|
|
421
|
-
*
|
|
422
|
-
* `slug` and `dir` are both derived from the slug, so neither moves across a
|
|
423
|
-
* reinstall; `installedAt` and `sha` are what actually distinguish one install
|
|
424
|
-
* from the next.
|
|
425
|
-
*/
|
|
426
|
-
function isSameInstall(left, right) {
|
|
427
|
-
return left.installedAt === right.installedAt
|
|
428
|
-
&& left.sha === right.sha
|
|
429
|
-
&& left.dir === right.dir;
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Run a skill's install or uninstall script, unless asked not to.
|
|
433
|
-
*
|
|
434
|
-
* A failing install script does not undo the install: by the time it runs the
|
|
435
|
-
* files are already on disk, and dropping the registry entry would strand a
|
|
436
|
-
* directory `uninstall` could no longer be pointed at. The failure is reported
|
|
437
|
-
* and the command exits non-zero instead.
|
|
438
|
-
*/
|
|
439
|
-
async function runLifecycleScript(entry, phase, ignoreScripts, io) {
|
|
440
|
-
const script = entry.scripts?.[phase];
|
|
441
|
-
if (!script)
|
|
442
|
-
return;
|
|
443
|
-
if (ignoreScripts) {
|
|
444
|
-
(0, output_1.writeError)(io, `Skipped ${phase} script for ${entry.slug}: ${script}`);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
(0, output_1.writeError)(io, `Running ${phase} script for ${entry.slug}: ${script}`);
|
|
448
|
-
const outcome = await (0, skills_1.runSkillScript)(script, entry.dir);
|
|
449
|
-
if (outcome.output)
|
|
450
|
-
(0, output_1.writeError)(io, outcome.output);
|
|
451
|
-
if (!outcome.ok)
|
|
452
|
-
throw new Error(scriptFailureMessage(entry, phase));
|
|
453
|
-
}
|
|
454
|
-
function scriptFailureMessage(entry, phase) {
|
|
455
|
-
const opener = `${phase} script failed for ${entry.slug}.`;
|
|
456
|
-
if (phase === 'uninstall')
|
|
457
|
-
return `${opener}\n${entry.slug} was left in place. Re-run with --ignore-scripts to remove it anyway.`;
|
|
458
|
-
return `${opener}\n`
|
|
459
|
-
+ `${entry.slug} was installed to ${entry.dir} and recorded, but its install script did not `
|
|
460
|
-
+ 'finish, so the skill may not work.\n'
|
|
461
|
-
+ `Remove it with \`tela skills uninstall ${entry.slug}\`, or reinstall without the script: `
|
|
462
|
-
+ `\`tela skills install ${entry.slug} --ignore-scripts\`.`;
|
|
463
|
-
}
|
|
464
242
|
function parseSkillFlags(args) {
|
|
465
243
|
const flags = { force: false, symlink: true, ignoreScripts: false, rest: [] };
|
|
466
244
|
for (let index = 0; index < args.length; index += 1) {
|