@meistrari/tela-cli 1.11.0 → 1.12.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.
@@ -177,15 +177,21 @@ async function installSkills(args, options, io) {
177
177
  ignoreScripts: flags.ignoreScripts,
178
178
  ...(flags.symlink ? {} : { targets: [] }),
179
179
  });
180
+ // A failed install script rolls back only its own skill, so report the rest and still fail.
181
+ const installedSlugs = new Set(installed.map(entry => entry.slug));
182
+ const failed = flags.rest.filter(slug => !installedSlugs.has(slug));
180
183
  if (options.json) {
181
- (0, output_1.writeJson)(io, { installed });
182
- return 0;
184
+ (0, output_1.writeJson)(io, failed.length > 0 ? { installed, failed } : { installed });
185
+ return failed.length > 0 ? 1 : 0;
183
186
  }
184
187
  for (const entry of installed)
185
188
  (0, output_1.writeLine)(io, `Installed ${entry.slug}@${entry.sha.slice(0, 7)} to ${entry.dir}`);
186
- return 0;
189
+ return failed.length > 0 ? 1 : 0;
187
190
  }
188
191
  function writeSkillManagerDiagnostic(io, diagnostic) {
192
+ // A failed uninstall script throws, and the command prints that error.
193
+ if (diagnostic.type === 'lifecycle-failed' && diagnostic.phase === 'uninstall')
194
+ return;
189
195
  (0, output_1.writeError)(io, diagnostic.message);
190
196
  }
191
197
  async function upgradeSkills(args, options, io) {
package/dist/index.js CHANGED
@@ -77236,9 +77236,7 @@ var require_skill_filesystem = __commonJS({
77236
77236
  var node_os_1 = require("node:os");
77237
77237
  var node_path_1 = __importDefault(require("node:path"));
77238
77238
  var node_process_1 = __importDefault(require("node:process"));
77239
- var node_util_1 = require("node:util");
77240
77239
  var archive_1 = require_archive();
77241
- var run = (0, node_util_1.promisify)(node_child_process_1.execFile);
77242
77240
  function resolveDefaultSkillManagerPaths(home = (0, node_os_1.homedir)()) {
77243
77241
  return {
77244
77242
  installRoot: node_path_1.default.join(home, ".agents", "skills"),
@@ -77425,14 +77423,72 @@ There is now nothing at ${target}. The previous installation is intact at ${back
77425
77423
  return { removed: false, reason: `Skipped ${linkPath}: ${describeError(error48)}` };
77426
77424
  }
77427
77425
  }
77428
- async function runSkillScript(script, cwd) {
77426
+ var SCRIPT_KILL_GRACE_MS = 3e3;
77427
+ var SCRIPT_GROUP_POLL_MS = 50;
77428
+ var MAX_SCRIPT_OUTPUT_LENGTH = 64 * 1024;
77429
+ async function runSkillScript(script, cwd, onOutput, signal) {
77430
+ if (signal?.aborted)
77431
+ return { ok: false, cancelled: true, output: "" };
77432
+ return await new Promise((resolve) => {
77433
+ const child = (0, node_child_process_1.spawn)("sh", ["-c", script], { cwd, detached: node_process_1.default.platform !== "win32" });
77434
+ let output = "";
77435
+ let settled = false;
77436
+ let cancelled = false;
77437
+ const append2 = (chunk) => {
77438
+ if (settled)
77439
+ return;
77440
+ const text2 = chunk.toString();
77441
+ output = `${output}${text2}`.slice(-MAX_SCRIPT_OUTPUT_LENGTH);
77442
+ onOutput?.(text2);
77443
+ };
77444
+ const finish = (ok) => {
77445
+ if (settled)
77446
+ return;
77447
+ settled = true;
77448
+ signal?.removeEventListener("abort", cancel2);
77449
+ resolve({ ok: ok && !cancelled, cancelled, output: output.trimEnd() });
77450
+ };
77451
+ function cancel2() {
77452
+ cancelled = true;
77453
+ killProcessTree(child, "SIGTERM");
77454
+ setTimeout(() => killProcessTree(child, "SIGKILL"), SCRIPT_KILL_GRACE_MS).unref();
77455
+ }
77456
+ signal?.addEventListener("abort", cancel2, { once: true });
77457
+ child.stdout.on("data", append2);
77458
+ child.stderr.on("data", append2);
77459
+ child.on("error", (error48) => {
77460
+ append2(`${describeError(error48)}
77461
+ `);
77462
+ finish(false);
77463
+ });
77464
+ child.on("exit", () => {
77465
+ if (cancelled)
77466
+ void waitForProcessGroupExit(child).then(() => finish(false));
77467
+ });
77468
+ child.on("close", (code) => finish(code === 0));
77469
+ });
77470
+ }
77471
+ async function waitForProcessGroupExit(child) {
77472
+ if (child.pid === void 0 || node_process_1.default.platform === "win32")
77473
+ return;
77474
+ while (true) {
77475
+ try {
77476
+ node_process_1.default.kill(-child.pid, 0);
77477
+ } catch {
77478
+ return;
77479
+ }
77480
+ await new Promise((resolve) => setTimeout(resolve, SCRIPT_GROUP_POLL_MS));
77481
+ }
77482
+ }
77483
+ function killProcessTree(child, signal) {
77484
+ if (child.pid === void 0)
77485
+ return;
77429
77486
  try {
77430
- const { stdout: stdout2, stderr } = await run("sh", ["-c", script], { cwd });
77431
- return { ok: true, output: [stdout2, stderr].map((text2) => text2.trimEnd()).filter(Boolean).join("\n") };
77432
- } catch (error48) {
77433
- const shell = error48;
77434
- const output = [shell.stdout ?? "", shell.stderr ?? "", describeError(error48)].map((text2) => text2.trimEnd()).filter(Boolean).join("\n");
77435
- return { ok: false, output };
77487
+ if (node_process_1.default.platform === "win32")
77488
+ (0, node_child_process_1.spawn)("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
77489
+ else
77490
+ node_process_1.default.kill(-child.pid, signal);
77491
+ } catch {
77436
77492
  }
77437
77493
  }
77438
77494
  function describeError(error48) {
@@ -77489,8 +77545,8 @@ var require_skills = __commonJS({
77489
77545
  ...path2 ? { queries: { path: path2 } } : {}
77490
77546
  }));
77491
77547
  }
77492
- async function downloadSkillArchive(archiveUrl) {
77493
- const response = await fetch(archiveUrl);
77548
+ async function downloadSkillArchive(archiveUrl, signal) {
77549
+ const response = await fetch(archiveUrl, { signal });
77494
77550
  if (!response.ok) {
77495
77551
  (0, http_1.assertHttpOk)(response, await response.text().catch(() => ""), "Failed to download skill archive");
77496
77552
  }
@@ -77507,7 +77563,7 @@ var require_skill_manager = __commonJS({
77507
77563
  return mod && mod.__esModule ? mod : { "default": mod };
77508
77564
  };
77509
77565
  Object.defineProperty(exports2, "__esModule", { value: true });
77510
- exports2.SkillInstallDirectoryConflictError = exports2.SkillLifecycleError = exports2.resolveDefaultSkillManagerPaths = void 0;
77566
+ exports2.SkillInstallDirectoryConflictError = exports2.SkillInstallCancelledError = exports2.SkillLifecycleError = exports2.resolveDefaultSkillManagerPaths = void 0;
77511
77567
  exports2.createSkillManager = createSkillManager2;
77512
77568
  var promises_1 = require("node:fs/promises");
77513
77569
  var node_path_1 = __importDefault(require("node:path"));
@@ -77529,10 +77585,22 @@ var require_skill_manager = __commonJS({
77529
77585
  this.phase = phase;
77530
77586
  this.name = "SkillLifecycleError";
77531
77587
  this.slug = entry.slug;
77532
- this.retainedState = phase === "install" ? "installed" : "unchanged";
77588
+ this.retainedState = "unchanged";
77533
77589
  }
77534
77590
  };
77535
77591
  exports2.SkillLifecycleError = SkillLifecycleError;
77592
+ var SkillInstallCancelledError = class extends Error {
77593
+ slug;
77594
+ installed;
77595
+ code = "skill-install-cancelled";
77596
+ constructor(slug, installed) {
77597
+ super(`Installation was cancelled before ${slug} finished.`);
77598
+ this.slug = slug;
77599
+ this.installed = installed;
77600
+ this.name = "SkillInstallCancelledError";
77601
+ }
77602
+ };
77603
+ exports2.SkillInstallCancelledError = SkillInstallCancelledError;
77536
77604
  var SkillInstallDirectoryConflictError = class extends Error {
77537
77605
  code = "skill-install-directory-conflict";
77538
77606
  path;
@@ -77567,17 +77635,31 @@ Run \`tela skills install ${slug} --force\` to replace it, or remove it by hand.
77567
77635
  const diagnostics = [];
77568
77636
  const installed = [];
77569
77637
  const emit = createDiagnosticEmitter(options2, diagnostics);
77638
+ const signal = installOptions.signal;
77570
77639
  for (const slug of slugs) {
77571
- const entry = await installOne({
77572
- client,
77573
- diagnostics: emit,
77574
- force: installOptions.force ?? false,
77575
- ignoreScripts: installOptions.ignoreScripts ?? false,
77576
- inventoryFile,
77577
- paths,
77578
- slug,
77579
- targets
77580
- });
77640
+ if (signal?.aborted)
77641
+ throw new SkillInstallCancelledError(slug, installed);
77642
+ let entry;
77643
+ try {
77644
+ entry = await installOne({
77645
+ client,
77646
+ diagnostics: emit,
77647
+ force: installOptions.force ?? false,
77648
+ ignoreScripts: installOptions.ignoreScripts ?? false,
77649
+ inventoryFile,
77650
+ paths,
77651
+ progress: options2.onInstallProgress,
77652
+ signal,
77653
+ slug,
77654
+ targets
77655
+ });
77656
+ } catch (error48) {
77657
+ if (signal?.aborted)
77658
+ throw new SkillInstallCancelledError(slug, installed);
77659
+ if (error48 instanceof SkillLifecycleError && error48.phase === "install")
77660
+ continue;
77661
+ throw error48;
77662
+ }
77581
77663
  if (!entry)
77582
77664
  throw new Error(`Skill install for ${slug} did not produce an inventory entry.`);
77583
77665
  installed.push(entry);
@@ -77611,6 +77693,7 @@ Run \`tela skills install ${slug} --force\` to replace it, or remove it by hand.
77611
77693
  ignoreScripts: upgradeOptions.ignoreScripts ?? false,
77612
77694
  inventoryFile,
77613
77695
  paths,
77696
+ progress: options2.onInstallProgress,
77614
77697
  slug: replacementSlug,
77615
77698
  targets
77616
77699
  });
@@ -77641,6 +77724,7 @@ Run \`tela skills install ${slug} --force\` to replace it, or remove it by hand.
77641
77724
  inventoryFile,
77642
77725
  operation: "upgrade",
77643
77726
  paths,
77727
+ progress: options2.onInstallProgress,
77644
77728
  slug: current.slug,
77645
77729
  targets
77646
77730
  });
@@ -77739,30 +77823,68 @@ Run \`tela skills install ${slug} --force\` to replace it, or remove it by hand.
77739
77823
  };
77740
77824
  }
77741
77825
  async function installOne(options2) {
77826
+ options2.progress?.({
77827
+ type: "phase",
77828
+ slug: options2.slug,
77829
+ phase: "fetching-metadata",
77830
+ message: "Fetching skill metadata"
77831
+ });
77742
77832
  const { ingredient, snapshot } = await (0, skills_1.fetchSkillFile)(options2.client, options2.slug);
77743
- const archive = await (0, skills_1.downloadSkillArchive)(snapshot.archiveUrl);
77833
+ options2.progress?.({
77834
+ type: "phase",
77835
+ slug: options2.slug,
77836
+ phase: "downloading",
77837
+ message: "Downloading skill files"
77838
+ });
77839
+ const archive = await (0, skills_1.downloadSkillArchive)(snapshot.archiveUrl, options2.signal);
77840
+ options2.signal?.throwIfAborted();
77841
+ options2.progress?.({
77842
+ type: "phase",
77843
+ slug: options2.slug,
77844
+ phase: "extracting",
77845
+ message: "Verifying and extracting files"
77846
+ });
77744
77847
  const files = (0, archive_1.verifySnapshotFiles)((0, archive_1.parseTarGz)(archive), snapshot.manifest);
77745
77848
  const target = (0, skill_filesystem_1.resolveInstallDir)(options2.paths, options2.slug);
77746
- const stagedInstalls = [];
77849
+ const reportConcurrentChange = () => options2.diagnostics({
77850
+ type: "concurrent-change",
77851
+ slug: options2.slug,
77852
+ operation: options2.operation ?? "install",
77853
+ message: `Skipped ${options2.slug}: it was changed while this ${options2.operation ?? "install"} ran`
77854
+ });
77855
+ const before = (await (0, tela_local_config_1.listInstalledSkills)(options2.inventoryFile)).find((entry2) => entry2.slug === options2.slug) ?? null;
77856
+ if (options2.expectedCurrent && !isSameRegistration(before, options2.expectedCurrent)) {
77857
+ reportConcurrentChange();
77858
+ return null;
77859
+ }
77860
+ if ((0, skill_filesystem_1.isUnownedInstallDir)(target, before?.dir ?? null) && !options2.force)
77861
+ throw new SkillInstallDirectoryConflictError(options2.slug, target);
77862
+ const staged = await (0, skill_filesystem_1.stageSkillFiles)(target, files);
77747
77863
  let concurrentChange = false;
77748
77864
  let entry = null;
77749
77865
  try {
77866
+ const lifecycleEntry = {
77867
+ slug: options2.slug,
77868
+ dir: target,
77869
+ sha: snapshot.sha,
77870
+ bundle: ingredient.bundle,
77871
+ symlinks: [],
77872
+ scripts: ingredient.scripts,
77873
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
77874
+ };
77875
+ await runLifecycle(lifecycleEntry, "install", options2.ignoreScripts, options2.diagnostics, options2.progress, options2.signal);
77876
+ options2.signal?.throwIfAborted();
77877
+ options2.progress?.({
77878
+ type: "phase",
77879
+ slug: options2.slug,
77880
+ phase: "finalizing",
77881
+ message: "Linking and finalizing installation"
77882
+ });
77750
77883
  entry = await (0, tela_local_config_1.updateInstalledSkill)(options2.slug, async (registered) => {
77751
- if (options2.expectedCurrent && (registered === null || !isSameInstall(registered, options2.expectedCurrent))) {
77884
+ if (!isSameRegistration(registered, before)) {
77752
77885
  concurrentChange = true;
77753
- options2.diagnostics({
77754
- type: "concurrent-change",
77755
- slug: options2.slug,
77756
- operation: options2.operation ?? "install",
77757
- message: `Skipped ${options2.slug}: it was changed while this upgrade ran`
77758
- });
77759
77886
  return registered;
77760
77887
  }
77761
- if ((0, skill_filesystem_1.isUnownedInstallDir)(target, registered?.dir ?? null) && !options2.force) {
77762
- throw new SkillInstallDirectoryConflictError(options2.slug, target);
77763
- }
77764
- const staged = await (0, skill_filesystem_1.stageSkillFiles)(target, files);
77765
- stagedInstalls.push(staged);
77766
77888
  const desiredLinkPaths = new Set(options2.targets.map((linkTarget) => node_path_1.default.join(linkTarget.root, options2.slug)));
77767
77889
  for (const linkPath of registered?.symlinks ?? []) {
77768
77890
  if (desiredLinkPaths.has(linkPath))
@@ -77791,25 +77913,27 @@ Run \`tela skills install ${slug} --force\` to replace it, or remove it by hand.
77791
77913
  }
77792
77914
  }
77793
77915
  return {
77794
- slug: options2.slug,
77795
- dir: target,
77796
- sha: snapshot.sha,
77797
- bundle: ingredient.bundle,
77798
- symlinks,
77799
- scripts: ingredient.scripts,
77800
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
77916
+ ...lifecycleEntry,
77917
+ symlinks
77801
77918
  };
77802
77919
  }, options2.inventoryFile);
77803
77920
  } catch (error48) {
77804
- await rollbackStagedInstall(stagedInstalls.at(-1) ?? null, error48);
77921
+ options2.progress?.({
77922
+ type: "phase",
77923
+ slug: options2.slug,
77924
+ phase: "rolling-back",
77925
+ message: "Rolling back failed installation"
77926
+ });
77927
+ await rollbackStagedInstall(staged, error48);
77805
77928
  }
77806
- if (concurrentChange)
77929
+ if (concurrentChange) {
77930
+ await staged.rollback();
77931
+ reportConcurrentChange();
77807
77932
  return null;
77808
- for (const staged of stagedInstalls)
77809
- await staged.commit();
77933
+ }
77934
+ await staged.commit();
77810
77935
  if (!entry)
77811
77936
  throw new Error(`Skill install for ${options2.slug} did not produce an inventory entry.`);
77812
- await runLifecycle(entry, "install", options2.ignoreScripts, options2.diagnostics);
77813
77937
  return entry;
77814
77938
  }
77815
77939
  function selectUpgradeTargets(installed, slugs) {
@@ -77828,6 +77952,11 @@ Run \`tela skills install ${slug} --force\` to replace it, or remove it by hand.
77828
77952
  throw new Error(`${missing.join(", ")} not installed by tela. Nothing to remove.`);
77829
77953
  return slugs.map((slug) => bySlug.get(slug));
77830
77954
  }
77955
+ function isSameRegistration(left, right) {
77956
+ if (left === null || right === null)
77957
+ return left === right;
77958
+ return isSameInstall(left, right);
77959
+ }
77831
77960
  function isSameInstall(left, right) {
77832
77961
  return left.installedAt === right.installedAt && left.sha === right.sha && left.dir === right.dir;
77833
77962
  }
@@ -77848,7 +77977,7 @@ Cleanup failed: ${describeError(cleanupError)}`);
77848
77977
  }
77849
77978
  throw error48;
77850
77979
  }
77851
- async function runLifecycle(entry, phase, ignoreScripts, emit) {
77980
+ async function runLifecycle(entry, phase, ignoreScripts, emit, progress2, signal) {
77852
77981
  const script = entry.scripts?.[phase];
77853
77982
  if (!script)
77854
77983
  return;
@@ -77869,7 +77998,17 @@ Cleanup failed: ${describeError(cleanupError)}`);
77869
77998
  script,
77870
77999
  message: `Running ${phase} script for ${entry.slug}: ${script}`
77871
78000
  });
77872
- const outcome = await (0, skill_filesystem_1.runSkillScript)(script, entry.dir);
78001
+ if (phase === "install") {
78002
+ progress2?.({
78003
+ type: "phase",
78004
+ slug: entry.slug,
78005
+ phase: "running-install-script",
78006
+ message: "Running install script"
78007
+ });
78008
+ }
78009
+ const outcome = await (0, skill_filesystem_1.runSkillScript)(script, entry.dir, phase === "install" ? (output) => progress2?.({ type: "script-output", slug: entry.slug, output }) : void 0, signal);
78010
+ if (outcome.cancelled)
78011
+ signal?.throwIfAborted();
77873
78012
  if (outcome.output) {
77874
78013
  emit({
77875
78014
  type: "lifecycle-output",
@@ -77880,7 +78019,14 @@ Cleanup failed: ${describeError(cleanupError)}`);
77880
78019
  });
77881
78020
  }
77882
78021
  if (!outcome.ok) {
77883
- throw new SkillLifecycleError(entry, phase);
78022
+ const error48 = new SkillLifecycleError(entry, phase);
78023
+ emit({
78024
+ type: "lifecycle-failed",
78025
+ slug: entry.slug,
78026
+ phase,
78027
+ message: error48.message
78028
+ });
78029
+ throw error48;
77884
78030
  }
77885
78031
  }
77886
78032
  function lifecycleFailureMessage(entry, phase) {
@@ -77889,8 +78035,8 @@ Cleanup failed: ${describeError(cleanupError)}`);
77889
78035
  return `${opener}
77890
78036
  ${entry.slug} was left in place. Re-run with --ignore-scripts to remove it anyway.`;
77891
78037
  return `${opener}
77892
- ${entry.slug} was installed to ${entry.dir} and recorded, but its install script did not finish, so the skill may not work.
77893
- Remove it with \`tela skills uninstall ${entry.slug}\`, or reinstall without the script: \`tela skills install ${entry.slug} --ignore-scripts\`.`;
78038
+ ${entry.slug} was not installed because its files and inventory entry were rolled back.
78039
+ Retry without the script with \`tela skills install ${entry.slug} --ignore-scripts\`.`;
77894
78040
  }
77895
78041
  function normalizePaths(paths) {
77896
78042
  const installRoot = normalizeAbsolutePath(paths.installRoot, "installRoot");
@@ -389332,15 +389478,19 @@ async function installSkills(args, options2, io) {
389332
389478
  ignoreScripts: flags.ignoreScripts,
389333
389479
  ...flags.symlink ? {} : { targets: [] }
389334
389480
  });
389481
+ const installedSlugs = new Set(installed.map((entry) => entry.slug));
389482
+ const failed = flags.rest.filter((slug) => !installedSlugs.has(slug));
389335
389483
  if (options2.json) {
389336
- writeJson(io, { installed });
389337
- return 0;
389484
+ writeJson(io, failed.length > 0 ? { installed, failed } : { installed });
389485
+ return failed.length > 0 ? 1 : 0;
389338
389486
  }
389339
389487
  for (const entry of installed)
389340
389488
  writeLine(io, `Installed ${entry.slug}@${entry.sha.slice(0, 7)} to ${entry.dir}`);
389341
- return 0;
389489
+ return failed.length > 0 ? 1 : 0;
389342
389490
  }
389343
389491
  function writeSkillManagerDiagnostic(io, diagnostic) {
389492
+ if (diagnostic.type === "lifecycle-failed" && diagnostic.phase === "uninstall")
389493
+ return;
389344
389494
  writeError(io, diagnostic.message);
389345
389495
  }
389346
389496
  async function upgradeSkills(args, options2, io) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-cli",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Tela command line tools for local state and agent workflows.",
5
5
  "engines": {
6
6
  "node": ">=22"