@meistrari/tela-cli 1.11.0 → 1.12.1

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);
@@ -77598,54 +77680,69 @@ Run \`tela skills install ${slug} --force\` to replace it, or remove it by hand.
77598
77680
  return { upgraded, unchanged, replaced, diagnostics };
77599
77681
  const targets = normalizeTargets(upgradeOptions.targets ?? paths.targets, paths.installRoot);
77600
77682
  const client = await resolveClient();
77601
- for (const current of selected) {
77602
- const { ingredient, snapshot } = await (0, skills_1.fetchSkillFile)(client, current.slug);
77603
- const replacementSlug = await resolveReplacementSlug(client, current.slug, ingredient);
77604
- if (replacementSlug) {
77605
- const installedReplacement = (await (0, tela_local_config_1.listInstalledSkills)(inventoryFile)).find((entry2) => entry2.slug === replacementSlug);
77606
- const replacement = await installOne({
77683
+ const signal = upgradeOptions.signal;
77684
+ let currentSlug = "";
77685
+ try {
77686
+ for (const current of selected) {
77687
+ currentSlug = current.slug;
77688
+ signal?.throwIfAborted();
77689
+ const { ingredient, snapshot } = await (0, skills_1.fetchSkillFile)(client, current.slug);
77690
+ const replacementSlug = await resolveReplacementSlug(client, current.slug, ingredient);
77691
+ if (replacementSlug) {
77692
+ const installedReplacement = (await (0, tela_local_config_1.listInstalledSkills)(inventoryFile)).find((entry2) => entry2.slug === replacementSlug);
77693
+ const replacement = await installOne({
77694
+ client,
77695
+ diagnostics: emit,
77696
+ ...installedReplacement ? { expectedCurrent: installedReplacement, operation: "upgrade" } : {},
77697
+ force: upgradeOptions.force ?? false,
77698
+ ignoreScripts: upgradeOptions.ignoreScripts ?? false,
77699
+ inventoryFile,
77700
+ paths,
77701
+ progress: options2.onInstallProgress,
77702
+ signal,
77703
+ slug: replacementSlug,
77704
+ targets
77705
+ });
77706
+ if (!replacement)
77707
+ continue;
77708
+ signal?.throwIfAborted();
77709
+ if (await uninstallOne({
77710
+ diagnostics: emit,
77711
+ entry: current,
77712
+ ignoreScripts: upgradeOptions.ignoreScripts ?? false,
77713
+ inventoryFile,
77714
+ operation: "upgrade",
77715
+ paths
77716
+ })) {
77717
+ replaced.push({ from: current.slug, to: replacement });
77718
+ }
77719
+ continue;
77720
+ }
77721
+ if (snapshot.sha === current.sha) {
77722
+ unchanged.push(current.slug);
77723
+ continue;
77724
+ }
77725
+ const entry = await installOne({
77607
77726
  client,
77608
77727
  diagnostics: emit,
77609
- ...installedReplacement ? { expectedCurrent: installedReplacement, operation: "upgrade" } : {},
77728
+ expectedCurrent: current,
77610
77729
  force: upgradeOptions.force ?? false,
77611
77730
  ignoreScripts: upgradeOptions.ignoreScripts ?? false,
77612
77731
  inventoryFile,
77732
+ operation: "upgrade",
77613
77733
  paths,
77614
- slug: replacementSlug,
77734
+ progress: options2.onInstallProgress,
77735
+ signal,
77736
+ slug: current.slug,
77615
77737
  targets
77616
77738
  });
77617
- if (!replacement)
77618
- continue;
77619
- if (await uninstallOne({
77620
- diagnostics: emit,
77621
- entry: current,
77622
- ignoreScripts: upgradeOptions.ignoreScripts ?? false,
77623
- inventoryFile,
77624
- operation: "upgrade",
77625
- paths
77626
- })) {
77627
- replaced.push({ from: current.slug, to: replacement });
77628
- }
77629
- continue;
77630
- }
77631
- if (snapshot.sha === current.sha) {
77632
- unchanged.push(current.slug);
77633
- continue;
77739
+ if (entry)
77740
+ upgraded.push(entry);
77634
77741
  }
77635
- const entry = await installOne({
77636
- client,
77637
- diagnostics: emit,
77638
- expectedCurrent: current,
77639
- force: upgradeOptions.force ?? false,
77640
- ignoreScripts: upgradeOptions.ignoreScripts ?? false,
77641
- inventoryFile,
77642
- operation: "upgrade",
77643
- paths,
77644
- slug: current.slug,
77645
- targets
77646
- });
77647
- if (entry)
77648
- upgraded.push(entry);
77742
+ } catch (error48) {
77743
+ if (signal?.aborted)
77744
+ throw new SkillInstallCancelledError(currentSlug, upgraded);
77745
+ throw error48;
77649
77746
  }
77650
77747
  return { upgraded, unchanged, replaced, diagnostics };
77651
77748
  },
@@ -77739,30 +77836,68 @@ Run \`tela skills install ${slug} --force\` to replace it, or remove it by hand.
77739
77836
  };
77740
77837
  }
77741
77838
  async function installOne(options2) {
77839
+ options2.progress?.({
77840
+ type: "phase",
77841
+ slug: options2.slug,
77842
+ phase: "fetching-metadata",
77843
+ message: "Fetching skill metadata"
77844
+ });
77742
77845
  const { ingredient, snapshot } = await (0, skills_1.fetchSkillFile)(options2.client, options2.slug);
77743
- const archive = await (0, skills_1.downloadSkillArchive)(snapshot.archiveUrl);
77846
+ options2.progress?.({
77847
+ type: "phase",
77848
+ slug: options2.slug,
77849
+ phase: "downloading",
77850
+ message: "Downloading skill files"
77851
+ });
77852
+ const archive = await (0, skills_1.downloadSkillArchive)(snapshot.archiveUrl, options2.signal);
77853
+ options2.signal?.throwIfAborted();
77854
+ options2.progress?.({
77855
+ type: "phase",
77856
+ slug: options2.slug,
77857
+ phase: "extracting",
77858
+ message: "Verifying and extracting files"
77859
+ });
77744
77860
  const files = (0, archive_1.verifySnapshotFiles)((0, archive_1.parseTarGz)(archive), snapshot.manifest);
77745
77861
  const target = (0, skill_filesystem_1.resolveInstallDir)(options2.paths, options2.slug);
77746
- const stagedInstalls = [];
77862
+ const reportConcurrentChange = () => options2.diagnostics({
77863
+ type: "concurrent-change",
77864
+ slug: options2.slug,
77865
+ operation: options2.operation ?? "install",
77866
+ message: `Skipped ${options2.slug}: it was changed while this ${options2.operation ?? "install"} ran`
77867
+ });
77868
+ const before = (await (0, tela_local_config_1.listInstalledSkills)(options2.inventoryFile)).find((entry2) => entry2.slug === options2.slug) ?? null;
77869
+ if (options2.expectedCurrent && !isSameRegistration(before, options2.expectedCurrent)) {
77870
+ reportConcurrentChange();
77871
+ return null;
77872
+ }
77873
+ if ((0, skill_filesystem_1.isUnownedInstallDir)(target, before?.dir ?? null) && !options2.force)
77874
+ throw new SkillInstallDirectoryConflictError(options2.slug, target);
77875
+ const staged = await (0, skill_filesystem_1.stageSkillFiles)(target, files);
77747
77876
  let concurrentChange = false;
77748
77877
  let entry = null;
77749
77878
  try {
77879
+ const lifecycleEntry = {
77880
+ slug: options2.slug,
77881
+ dir: target,
77882
+ sha: snapshot.sha,
77883
+ bundle: ingredient.bundle,
77884
+ symlinks: [],
77885
+ scripts: ingredient.scripts,
77886
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
77887
+ };
77888
+ await runLifecycle(lifecycleEntry, "install", options2.ignoreScripts, options2.diagnostics, options2.progress, options2.signal);
77889
+ options2.signal?.throwIfAborted();
77890
+ options2.progress?.({
77891
+ type: "phase",
77892
+ slug: options2.slug,
77893
+ phase: "finalizing",
77894
+ message: "Linking and finalizing installation"
77895
+ });
77750
77896
  entry = await (0, tela_local_config_1.updateInstalledSkill)(options2.slug, async (registered) => {
77751
- if (options2.expectedCurrent && (registered === null || !isSameInstall(registered, options2.expectedCurrent))) {
77897
+ if (!isSameRegistration(registered, before)) {
77752
77898
  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
77899
  return registered;
77760
77900
  }
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
77901
  const desiredLinkPaths = new Set(options2.targets.map((linkTarget) => node_path_1.default.join(linkTarget.root, options2.slug)));
77767
77902
  for (const linkPath of registered?.symlinks ?? []) {
77768
77903
  if (desiredLinkPaths.has(linkPath))
@@ -77791,25 +77926,27 @@ Run \`tela skills install ${slug} --force\` to replace it, or remove it by hand.
77791
77926
  }
77792
77927
  }
77793
77928
  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()
77929
+ ...lifecycleEntry,
77930
+ symlinks
77801
77931
  };
77802
77932
  }, options2.inventoryFile);
77803
77933
  } catch (error48) {
77804
- await rollbackStagedInstall(stagedInstalls.at(-1) ?? null, error48);
77934
+ options2.progress?.({
77935
+ type: "phase",
77936
+ slug: options2.slug,
77937
+ phase: "rolling-back",
77938
+ message: "Rolling back failed installation"
77939
+ });
77940
+ await rollbackStagedInstall(staged, error48);
77805
77941
  }
77806
- if (concurrentChange)
77942
+ if (concurrentChange) {
77943
+ await staged.rollback();
77944
+ reportConcurrentChange();
77807
77945
  return null;
77808
- for (const staged of stagedInstalls)
77809
- await staged.commit();
77946
+ }
77947
+ await staged.commit();
77810
77948
  if (!entry)
77811
77949
  throw new Error(`Skill install for ${options2.slug} did not produce an inventory entry.`);
77812
- await runLifecycle(entry, "install", options2.ignoreScripts, options2.diagnostics);
77813
77950
  return entry;
77814
77951
  }
77815
77952
  function selectUpgradeTargets(installed, slugs) {
@@ -77828,6 +77965,11 @@ Run \`tela skills install ${slug} --force\` to replace it, or remove it by hand.
77828
77965
  throw new Error(`${missing.join(", ")} not installed by tela. Nothing to remove.`);
77829
77966
  return slugs.map((slug) => bySlug.get(slug));
77830
77967
  }
77968
+ function isSameRegistration(left, right) {
77969
+ if (left === null || right === null)
77970
+ return left === right;
77971
+ return isSameInstall(left, right);
77972
+ }
77831
77973
  function isSameInstall(left, right) {
77832
77974
  return left.installedAt === right.installedAt && left.sha === right.sha && left.dir === right.dir;
77833
77975
  }
@@ -77848,7 +77990,7 @@ Cleanup failed: ${describeError(cleanupError)}`);
77848
77990
  }
77849
77991
  throw error48;
77850
77992
  }
77851
- async function runLifecycle(entry, phase, ignoreScripts, emit) {
77993
+ async function runLifecycle(entry, phase, ignoreScripts, emit, progress2, signal) {
77852
77994
  const script = entry.scripts?.[phase];
77853
77995
  if (!script)
77854
77996
  return;
@@ -77869,7 +78011,17 @@ Cleanup failed: ${describeError(cleanupError)}`);
77869
78011
  script,
77870
78012
  message: `Running ${phase} script for ${entry.slug}: ${script}`
77871
78013
  });
77872
- const outcome = await (0, skill_filesystem_1.runSkillScript)(script, entry.dir);
78014
+ if (phase === "install") {
78015
+ progress2?.({
78016
+ type: "phase",
78017
+ slug: entry.slug,
78018
+ phase: "running-install-script",
78019
+ message: "Running install script"
78020
+ });
78021
+ }
78022
+ 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);
78023
+ if (outcome.cancelled)
78024
+ signal?.throwIfAborted();
77873
78025
  if (outcome.output) {
77874
78026
  emit({
77875
78027
  type: "lifecycle-output",
@@ -77880,7 +78032,14 @@ Cleanup failed: ${describeError(cleanupError)}`);
77880
78032
  });
77881
78033
  }
77882
78034
  if (!outcome.ok) {
77883
- throw new SkillLifecycleError(entry, phase);
78035
+ const error48 = new SkillLifecycleError(entry, phase);
78036
+ emit({
78037
+ type: "lifecycle-failed",
78038
+ slug: entry.slug,
78039
+ phase,
78040
+ message: error48.message
78041
+ });
78042
+ throw error48;
77884
78043
  }
77885
78044
  }
77886
78045
  function lifecycleFailureMessage(entry, phase) {
@@ -77889,8 +78048,8 @@ Cleanup failed: ${describeError(cleanupError)}`);
77889
78048
  return `${opener}
77890
78049
  ${entry.slug} was left in place. Re-run with --ignore-scripts to remove it anyway.`;
77891
78050
  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\`.`;
78051
+ ${entry.slug} was not installed because its files and inventory entry were rolled back.
78052
+ Retry without the script with \`tela skills install ${entry.slug} --ignore-scripts\`.`;
77894
78053
  }
77895
78054
  function normalizePaths(paths) {
77896
78055
  const installRoot = normalizeAbsolutePath(paths.installRoot, "installRoot");
@@ -386211,7 +386370,11 @@ var require_setup = __commonJS({
386211
386370
  exports2.findAgentSetupOrganization = findAgentSetupOrganization;
386212
386371
  exports2.validatePreparedAgentSetupInput = validatePreparedAgentSetupInput;
386213
386372
  exports2.saveAgentContextSetup = saveAgentContextSetup;
386373
+ exports2.provisionForgejoUserWithSshKey = provisionForgejoUserWithSshKey;
386374
+ exports2.createUniqueSshKeyTitle = createUniqueSshKeyTitle;
386214
386375
  exports2.completeAgentSetup = completeAgentSetup;
386376
+ var node_buffer_1 = require("node:buffer");
386377
+ var node_crypto_1 = require("node:crypto");
386215
386378
  var node_fs_1 = require("node:fs");
386216
386379
  var node_os_1 = require("node:os");
386217
386380
  var node_path_1 = require("node:path");
@@ -386596,6 +386759,28 @@ var require_setup = __commonJS({
386596
386759
  }
386597
386760
  return savedSetup;
386598
386761
  }
386762
+ var SSH_KEY_TITLE_CONFLICT = /ssh key title already belongs to a different key/i;
386763
+ async function provisionForgejoUserWithSshKey(organizationName, sshKey, config2, provisionFn, fetchFn) {
386764
+ try {
386765
+ return {
386766
+ response: await provisionFn({ organizationName, sshKey }, config2, fetchFn),
386767
+ title: sshKey.title
386768
+ };
386769
+ } catch (error48) {
386770
+ if (!(error48 instanceof agent_api_1.AgentApiError) || !SSH_KEY_TITLE_CONFLICT.test(error48.message))
386771
+ throw error48;
386772
+ }
386773
+ const title = createUniqueSshKeyTitle(sshKey);
386774
+ return {
386775
+ response: await provisionFn({ organizationName, sshKey: { ...sshKey, title } }, config2, fetchFn),
386776
+ title
386777
+ };
386778
+ }
386779
+ function createUniqueSshKeyTitle(sshKey, host = (0, node_os_1.hostname)()) {
386780
+ const blob = sshKey.key.split(/\s+/)[1] ?? sshKey.key;
386781
+ const fingerprint = (0, node_crypto_1.createHash)("sha256").update(node_buffer_1.Buffer.from(blob, "base64")).digest("base64").replace(/=+$/, "").slice(0, 12);
386782
+ return `${sshKey.title} (${host}, SHA256:${fingerprint})`;
386783
+ }
386599
386784
  async function completeAgentSetup(input, options2 = {}) {
386600
386785
  const credentials = (0, credentials_1.createAgentCredentialAdapter)(options2.env);
386601
386786
  const config2 = await (0, config_1.resolveAgentConfig)({
@@ -386611,15 +386796,11 @@ var require_setup = __commonJS({
386611
386796
  const sshKey = loadSshPublicKey(input.sshPublicKeyPath);
386612
386797
  const gitSshKey = createGitSshKey(sshKey, input.sshPrivateKeyPath);
386613
386798
  const cloneRoot = input.cloneRoot ? resolveCloneRootPath(input.cloneRoot) : config2.cloneRoot;
386614
- const provisionFn = options2.provisionFn ?? agent_api_1.provisionForgejoUser;
386615
- const response = await provisionFn({
386616
- organizationName: organization.name,
386617
- sshKey
386618
- }, config2, options2.fetchFn);
386799
+ const { response, title } = await provisionForgejoUserWithSshKey(organization.name, sshKey, config2, options2.provisionFn ?? agent_api_1.provisionForgejoUser, options2.fetchFn);
386619
386800
  const setup = await saveAgentContextSetup({
386620
386801
  config: config2,
386621
386802
  organization,
386622
- gitSshKey,
386803
+ gitSshKey: { ...gitSshKey, title },
386623
386804
  cloneRoot
386624
386805
  });
386625
386806
  return {
@@ -389332,15 +389513,19 @@ async function installSkills(args, options2, io) {
389332
389513
  ignoreScripts: flags.ignoreScripts,
389333
389514
  ...flags.symlink ? {} : { targets: [] }
389334
389515
  });
389516
+ const installedSlugs = new Set(installed.map((entry) => entry.slug));
389517
+ const failed = flags.rest.filter((slug) => !installedSlugs.has(slug));
389335
389518
  if (options2.json) {
389336
- writeJson(io, { installed });
389337
- return 0;
389519
+ writeJson(io, failed.length > 0 ? { installed, failed } : { installed });
389520
+ return failed.length > 0 ? 1 : 0;
389338
389521
  }
389339
389522
  for (const entry of installed)
389340
389523
  writeLine(io, `Installed ${entry.slug}@${entry.sha.slice(0, 7)} to ${entry.dir}`);
389341
- return 0;
389524
+ return failed.length > 0 ? 1 : 0;
389342
389525
  }
389343
389526
  function writeSkillManagerDiagnostic(io, diagnostic) {
389527
+ if (diagnostic.type === "lifecycle-failed" && diagnostic.phase === "uninstall")
389528
+ return;
389344
389529
  writeError(io, diagnostic.message);
389345
389530
  }
389346
389531
  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.1",
4
4
  "description": "Tela command line tools for local state and agent workflows.",
5
5
  "engines": {
6
6
  "node": ">=22"