@tacuchi/agent-workflow-cli 16.2.0 → 17.1.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.
Files changed (86) hide show
  1. package/README.md +13 -0
  2. package/dist/adapters/node-file-system.d.ts +3 -1
  3. package/dist/adapters/node-file-system.d.ts.map +1 -1
  4. package/dist/adapters/node-file-system.js +18 -1
  5. package/dist/adapters/node-file-system.js.map +1 -1
  6. package/dist/application/mcp-doctor-service.d.ts.map +1 -1
  7. package/dist/application/mcp-doctor-service.js +3 -2
  8. package/dist/application/mcp-doctor-service.js.map +1 -1
  9. package/dist/application/mcp-host-writer.d.ts +1 -1
  10. package/dist/application/mcp-host-writer.d.ts.map +1 -1
  11. package/dist/application/mcp-host-writer.js +40 -10
  12. package/dist/application/mcp-host-writer.js.map +1 -1
  13. package/dist/application/mcp-remove-service.d.ts.map +1 -1
  14. package/dist/application/mcp-remove-service.js +3 -2
  15. package/dist/application/mcp-remove-service.js.map +1 -1
  16. package/dist/application/mcp-setup-service.d.ts.map +1 -1
  17. package/dist/application/mcp-setup-service.js +3 -2
  18. package/dist/application/mcp-setup-service.js.map +1 -1
  19. package/dist/application/self/install-plugin-skills-git.d.ts +1 -0
  20. package/dist/application/self/install-plugin-skills-git.d.ts.map +1 -1
  21. package/dist/application/self/install-plugin-skills-git.js +11 -2
  22. package/dist/application/self/install-plugin-skills-git.js.map +1 -1
  23. package/dist/application/self/install-plugin-skills.d.ts +6 -0
  24. package/dist/application/self/install-plugin-skills.d.ts.map +1 -1
  25. package/dist/application/self/install-plugin-skills.js +12 -3
  26. package/dist/application/self/install-plugin-skills.js.map +1 -1
  27. package/dist/application/self/install-skill.d.ts +1 -0
  28. package/dist/application/self/install-skill.d.ts.map +1 -1
  29. package/dist/application/self/install-skill.js +3 -1
  30. package/dist/application/self/install-skill.js.map +1 -1
  31. package/dist/application/self/mcp-config.d.ts +2 -0
  32. package/dist/application/self/mcp-config.d.ts.map +1 -1
  33. package/dist/application/self/mcp-config.js +29 -12
  34. package/dist/application/self/mcp-config.js.map +1 -1
  35. package/dist/application/self/skills-manager.d.ts +105 -0
  36. package/dist/application/self/skills-manager.d.ts.map +1 -0
  37. package/dist/application/self/skills-manager.js +594 -0
  38. package/dist/application/self/skills-manager.js.map +1 -0
  39. package/dist/application/self/skills-registry.d.ts +35 -0
  40. package/dist/application/self/skills-registry.d.ts.map +1 -0
  41. package/dist/application/self/skills-registry.js +87 -0
  42. package/dist/application/self/skills-registry.js.map +1 -0
  43. package/dist/cli/tui/app.js +5 -2
  44. package/dist/cli/tui/app.js.map +1 -1
  45. package/dist/cli/tui/components/focus-row.d.ts +1 -1
  46. package/dist/cli/tui/components/focus-row.js +1 -1
  47. package/dist/cli/tui/components/host-admin-section.d.ts +22 -0
  48. package/dist/cli/tui/components/host-admin-section.d.ts.map +1 -0
  49. package/dist/cli/tui/components/host-admin-section.js +321 -0
  50. package/dist/cli/tui/components/host-admin-section.js.map +1 -0
  51. package/dist/cli/tui/components/tabs-config.js +1 -1
  52. package/dist/cli/tui/components/tabs-config.js.map +1 -1
  53. package/dist/cli/tui/data/recommended-skills.d.ts +3 -0
  54. package/dist/cli/tui/data/recommended-skills.d.ts.map +1 -0
  55. package/dist/cli/tui/data/recommended-skills.js +90 -0
  56. package/dist/cli/tui/data/recommended-skills.js.map +1 -0
  57. package/dist/cli/tui/data/workflow-content.d.ts +5 -4
  58. package/dist/cli/tui/data/workflow-content.d.ts.map +1 -1
  59. package/dist/cli/tui/data/workflow-content.js +14 -134
  60. package/dist/cli/tui/data/workflow-content.js.map +1 -1
  61. package/dist/cli/tui/tabs/mcp-tab-helpers.d.ts +14 -7
  62. package/dist/cli/tui/tabs/mcp-tab-helpers.d.ts.map +1 -1
  63. package/dist/cli/tui/tabs/mcp-tab-helpers.js +16 -5
  64. package/dist/cli/tui/tabs/mcp-tab-helpers.js.map +1 -1
  65. package/dist/cli/tui/tabs/mcp-tab.d.ts.map +1 -1
  66. package/dist/cli/tui/tabs/mcp-tab.js +15 -13
  67. package/dist/cli/tui/tabs/mcp-tab.js.map +1 -1
  68. package/dist/cli/tui/tabs/skills-tab.d.ts +0 -1
  69. package/dist/cli/tui/tabs/skills-tab.d.ts.map +1 -1
  70. package/dist/cli/tui/tabs/skills-tab.js +292 -229
  71. package/dist/cli/tui/tabs/skills-tab.js.map +1 -1
  72. package/dist/cli/tui/tabs/status-tab.d.ts +1 -1
  73. package/dist/cli/tui/tabs/status-tab.d.ts.map +1 -1
  74. package/dist/cli/tui/tabs/status-tab.js +3 -2
  75. package/dist/cli/tui/tabs/status-tab.js.map +1 -1
  76. package/dist/cli/tui/tabs/workflow-tab.d.ts +6 -1
  77. package/dist/cli/tui/tabs/workflow-tab.d.ts.map +1 -1
  78. package/dist/cli/tui/tabs/workflow-tab.js +15 -28
  79. package/dist/cli/tui/tabs/workflow-tab.js.map +1 -1
  80. package/dist/domain/mcp-entry.d.ts +11 -0
  81. package/dist/domain/mcp-entry.d.ts.map +1 -1
  82. package/dist/domain/mcp-entry.js +13 -0
  83. package/dist/domain/mcp-entry.js.map +1 -1
  84. package/dist/ports/file-system.d.ts +13 -0
  85. package/dist/ports/file-system.d.ts.map +1 -1
  86. package/package.json +1 -1
@@ -1,160 +1,210 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // [Skills] — administrador de skills sueltas (modelo skills.sh): lista única
3
+ // con badges (installed → unmanaged → registered → recommended), detail con
4
+ // acciones por estado (unmanaged = informativa) y wizard [a] fuente → picker
5
+ // → warning de terceros → registrar.
6
+ // Respaldado por skills-manager; la administración del bundle `w` vive en
7
+ // [Workflows] (HostAdminSection).
2
8
  import { Box, Text, useInput, useStdout } from "ink";
3
9
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
10
  import { formatTuiEvent } from "../../../application/logging/log-events.js";
5
- import { selfCleanLegacy } from "../../../application/self/clean-legacy.js";
6
- import { SKILL_DIR_NAME, TARGET_ROOTS, selfInstallSkill, } from "../../../application/self/install-skill.js";
7
- import { selfClearPluginCache } from "../../../application/self/plugin-cache-clear.js";
8
- import { selfUninstall } from "../../../application/self/uninstall.js";
11
+ import { canonicalSkillsRoot, installSkill, listSkills, probeSkillSource, registerSkill, reinstallSkill, removeSkill, resolveSkillSource, uninstallSkill, updateSkill, } from "../../../application/self/skills-manager.js";
9
12
  import { ConfirmBanner } from "../components/confirm-banner.js";
10
13
  import { DetailPanel } from "../components/detail-panel.js";
14
+ import { InputPrompt } from "../components/input-prompt.js";
11
15
  import { ListRow } from "../components/list-row.js";
12
16
  import { PageHead } from "../components/page-head.js";
13
17
  import { QuickActions } from "../components/quick-actions.js";
14
18
  import { SectionHead } from "../components/section-head.js";
15
- import { WORKFLOW_CONTENT } from "../data/workflow-content.js";
16
- import { HOSTS } from "../hosts.js";
19
+ import { RECOMMENDED_SKILLS } from "../data/recommended-skills.js";
17
20
  import { useInputLock } from "../input-lock.js";
18
21
  import { rowWidth } from "../row-width.js";
19
22
  import { colors, icons } from "../theme.js";
20
- const HOOKS_SUPPORTED_TARGETS = new Set(["claude"]);
21
- // Derived from the backend's own target map so the tab can't drift from what
22
- // `self install/uninstall-skill` actually supports (clean-legacy v14.5.1 lesson).
23
- const BACKED_INSTALL_TARGETS = new Set(Object.keys(TARGET_ROOTS));
23
+ const STATUS_GLYPH = {
24
+ installed: { glyph: "◆", active: true },
25
+ unmanaged: { glyph: "◈", active: true },
26
+ registered: { glyph: "◇", active: false },
27
+ recommended: { glyph: "·", active: false },
28
+ };
24
29
  export function SkillsTab({ ctx, isActive, onToast }) {
25
- const [skills, setSkills] = useState([]);
30
+ const [items, setItems] = useState([]);
26
31
  const [cursor, setCursor] = useState(0);
27
32
  const [actionCursor, setActionCursor] = useState(0);
28
33
  const [mode, setMode] = useState({ kind: "list" });
29
- const [busy, setBusy] = useState(null);
34
+ // Espejo de items para preservar la selección POR NOMBRE en refresh: la lista
35
+ // se re-ordena tras cada operación (installed→registered→recommended) y un
36
+ // cursor numérico saltaría a otra skill.
37
+ const itemsRef = useRef([]);
30
38
  const startedRef = useRef(false);
31
39
  const { lock, unlock } = useInputLock();
32
40
  const { stdout } = useStdout();
33
41
  useEffect(() => {
34
- if (busy)
35
- lock();
36
- else
42
+ if (mode.kind === "list" || mode.kind === "detail")
37
43
  unlock();
38
- }, [busy, lock, unlock]);
44
+ else
45
+ lock();
46
+ }, [mode, lock, unlock]);
39
47
  useEffect(() => () => unlock(), [unlock]);
40
48
  const refresh = useCallback(async () => {
41
- const home = ctx.env.homeDir();
42
- const settingsPath = `${home}/.claude/settings.json`;
43
- let hooksInstalled = false;
44
- if (await ctx.fs.exists(settingsPath)) {
45
- try {
46
- const parsed = JSON.parse(await ctx.fs.readText(settingsPath));
47
- hooksInstalled =
48
- typeof parsed === "object" &&
49
- parsed !== null &&
50
- "hooks" in parsed &&
51
- typeof parsed.hooks === "object" &&
52
- Object.keys(parsed.hooks).length > 0;
53
- }
54
- catch {
55
- hooksInstalled = false;
56
- }
57
- }
58
- const next = [];
59
- for (const host of HOSTS) {
60
- const path = pathForHost(host, home);
61
- const installed = host.backed && path ? await ctx.fs.exists(path) : false;
62
- next.push({
63
- host,
64
- installed,
65
- hooks_installed: host.id === "claude" ? hooksInstalled : false,
66
- path: installed
67
- ? friendlyPath(host, home)
68
- : host.backed
69
- ? "not installed"
70
- : "(not wired yet)",
49
+ try {
50
+ const next = await listSkills(ctx, RECOMMENDED_SKILLS);
51
+ setCursor((c) => {
52
+ const prevName = itemsRef.current[c]?.name;
53
+ const idx = prevName === undefined ? -1 : next.findIndex((s) => s.name === prevName);
54
+ return idx >= 0 ? idx : Math.min(Math.max(0, c), Math.max(0, next.length - 1));
71
55
  });
56
+ itemsRef.current = next;
57
+ setItems(next);
58
+ }
59
+ catch (err) {
60
+ onToast?.({ tone: "err", title: "Error loading skills", body: err.message });
72
61
  }
73
- setSkills(next);
74
- setCursor((c) => Math.min(Math.max(0, c), Math.max(0, next.length - 1)));
75
- }, [ctx]);
62
+ }, [ctx, onToast]);
76
63
  useEffect(() => {
77
64
  if (startedRef.current)
78
65
  return;
79
66
  startedRef.current = true;
80
67
  void refresh();
81
68
  }, [refresh]);
82
- const installedCount = skills.filter((s) => s.installed).length;
83
- const totalCount = skills.length;
84
- const backedHosts = HOSTS.filter((h) => h.backed).length;
85
- const pendingHosts = HOSTS.length - backedHosts;
86
- const focused = skills[cursor] ?? null;
87
- const isInstalled = focused?.installed === true;
88
- const isBackedFocused = focused ? BACKED_INSTALL_TARGETS.has(focused.host.id) : false;
69
+ const current = items[cursor] ?? null;
70
+ const installedCount = items.filter((s) => s.status === "installed").length;
71
+ const unmanagedCount = items.filter((s) => s.status === "unmanaged").length;
72
+ const registeredCount = items.filter((s) => s.status === "registered").length;
73
+ const recommendedCount = items.filter((s) => s.status === "recommended").length;
74
+ // Acciones del detail según estado (SPEC 003): recommended/registered →
75
+ // Install; installed Update (solo git) / Reinstall / Uninstall; Remove para
76
+ // todo lo registrado (una recomendada vuelve a `recommended`). `unmanaged`
77
+ // (fuera del registro) no es operable: el guard de ownership del motor
78
+ // rechaza register/uninstall sobre dirs ajenos — fila informativa.
89
79
  const detailActions = useMemo(() => {
90
- if (!focused)
80
+ if (!current || current.status === "unmanaged")
91
81
  return [];
92
- const reinstall = {
93
- name: isInstalled ? "Reinstall" : "Install",
94
- description: isInstalled ? "Overwrite files (--force)." : "Copy files (--force).",
95
- };
96
- if (isInstalled) {
82
+ if (current.status === "recommended") {
97
83
  return [
98
- reinstall,
99
84
  {
100
- name: "Uninstall",
101
- description: "Remove files. Reversible.",
102
- danger: true,
85
+ id: "install",
86
+ action: {
87
+ name: "Install",
88
+ description: "Register + install (canonical + Claude replica).",
89
+ },
103
90
  },
104
91
  ];
105
92
  }
106
- return [reinstall];
107
- }, [focused, isInstalled]);
108
- const runComposite = useCallback(async (kind, host) => {
109
- if (!BACKED_INSTALL_TARGETS.has(host.id)) {
110
- onToast?.({
111
- tone: "info",
112
- title: `Host '${host.name}' not supported yet`,
113
- body: "Install/uninstall backend without path mapping.",
114
- });
115
- return;
93
+ if (current.status === "registered") {
94
+ return [
95
+ {
96
+ id: "install",
97
+ action: { name: "Install", description: "Materialize canonical + Claude replica." },
98
+ },
99
+ {
100
+ id: "remove",
101
+ action: { name: "Remove", description: "Drop from the registry.", danger: true },
102
+ },
103
+ ];
116
104
  }
117
- const target = host.id;
118
- const steps = kind === "install"
119
- ? ["clean-legacy", "clean-cache", "install-full"]
120
- : ["uninstall-full", "clean-cache"];
121
- const startLabel = kind === "install" ? `installing on ${host.name}…` : `uninstalling from ${host.name}…`;
122
- setBusy(startLabel);
105
+ // El clasificador canónico del motor (isAbsolute cubre paths Windows como
106
+ // C:\… que un startsWith("/") clasificaría mal como git).
107
+ const resolved = resolveSkillSource(current.source, current.ref);
108
+ const gitSource = !("error" in resolved) && resolved.kind === "git";
109
+ return [
110
+ ...(gitSource
111
+ ? [
112
+ {
113
+ id: "update",
114
+ action: {
115
+ name: "Update",
116
+ description: "Re-fetch the registered ref (staging + swap).",
117
+ },
118
+ },
119
+ ]
120
+ : []),
121
+ {
122
+ id: "reinstall",
123
+ action: { name: "Reinstall", description: "Repair the replica from the canonical." },
124
+ },
125
+ {
126
+ id: "uninstall",
127
+ action: {
128
+ name: "Uninstall",
129
+ description: "Delete canonical + replica; keeps the registration.",
130
+ danger: true,
131
+ },
132
+ },
133
+ {
134
+ id: "remove",
135
+ action: {
136
+ name: "Remove",
137
+ description: "Uninstall + drop from the registry.",
138
+ danger: true,
139
+ },
140
+ },
141
+ ];
142
+ }, [current]);
143
+ // Superficie TUI en EN (SPEC 007); los summaries del motor (ES) van al body.
144
+ const runAction = useCallback(async (label, successTitle, op) => {
145
+ setMode({ kind: "busy", label });
123
146
  try {
124
- for (const step of steps) {
125
- setBusy(buildBusyLabel(step, host.name));
126
- const result = await dispatchAction(step, target, ctx);
127
- if (!result.ok) {
128
- const failMsg = result.error?.message;
129
- onToast?.(failMsg !== undefined
130
- ? { tone: "err", title: `Step ${step} failed`, body: failMsg }
131
- : { tone: "err", title: `Step ${step} failed` });
132
- // The err toast is mirrored to the log by the notification-center
133
- // safety net; nothing more to log here.
134
- await refresh();
135
- return;
147
+ const result = await op();
148
+ if (result.ok) {
149
+ onToast?.({ tone: "ok", title: successTitle, body: result.data?.summary ?? "" });
150
+ if (result.data?.warning) {
151
+ onToast?.({ tone: "info", title: "Notice", body: result.data.warning });
136
152
  }
153
+ void ctx.logger?.info(formatTuiEvent(`skill-manager ${label}`, "ok"));
154
+ }
155
+ else {
156
+ onToast?.({
157
+ tone: "err",
158
+ title: "Operation refused",
159
+ body: result.error?.message ?? "",
160
+ });
137
161
  }
138
- const finalAction = kind === "install" ? "install-full" : "uninstall-full";
139
- onToast?.({ tone: "ok", title: buildSuccessMessage(finalAction, host.name) });
140
- void ctx.logger?.info(formatTuiEvent(`skill ${kind} ${host.name}`, "ok"));
141
- await refresh();
142
162
  }
143
163
  catch (err) {
144
164
  onToast?.({ tone: "err", title: "Error", body: err.message });
145
165
  }
146
- finally {
147
- setBusy(null);
166
+ await refresh();
167
+ setMode({ kind: "list" });
168
+ }, [ctx, onToast, refresh]);
169
+ const triggerAction = useCallback((id) => {
170
+ if (!current)
171
+ return;
172
+ const name = current.name;
173
+ switch (id) {
174
+ case "install":
175
+ if (current.status === "recommended") {
176
+ // Registrar + instalar en un paso (la semilla ya trae la fuente).
177
+ void runAction(`installing ${name}…`, `Installed · ${name}`, async () => {
178
+ const reg = await registerSkill(ctx, { source: current.source, pick: name });
179
+ if (!reg.ok)
180
+ return reg;
181
+ return installSkill(ctx, name);
182
+ });
183
+ }
184
+ else {
185
+ void runAction(`installing ${name}…`, `Installed · ${name}`, () => installSkill(ctx, name));
186
+ }
187
+ return;
188
+ case "update":
189
+ void runAction(`updating ${name}…`, `Updated · ${name}`, () => updateSkill(ctx, name));
190
+ return;
191
+ case "reinstall":
192
+ void runAction(`reinstalling ${name}…`, `Reinstalled · ${name}`, () => reinstallSkill(ctx, name));
193
+ return;
194
+ case "uninstall":
195
+ setMode({ kind: "confirm", action: "uninstall" });
196
+ return;
197
+ case "remove":
198
+ setMode({ kind: "confirm", action: "remove" });
199
+ return;
148
200
  }
149
- }, [ctx, refresh, onToast]);
150
- // input — list mode (↑↓ navega · ⏎ abre detail · Esc no-op · 'i' empty-state install)
201
+ }, [ctx, current, runAction]);
202
+ // input — list (↑↓ navega · ⏎ detail · a wizard)
151
203
  useInput((input, key) => {
152
- if (!isActive || busy || mode.kind !== "list")
204
+ if (!isActive || mode.kind !== "list")
153
205
  return;
154
- if ((input === "i" || input === "I") && installedCount === 0) {
155
- const claude = HOSTS.find((h) => h.id === "claude");
156
- if (claude)
157
- void runComposite("install", claude);
206
+ if (input === "a" || input === "A") {
207
+ setMode({ kind: "wizard-source" });
158
208
  return;
159
209
  }
160
210
  if (key.upArrow) {
@@ -162,32 +212,24 @@ export function SkillsTab({ ctx, isActive, onToast }) {
162
212
  return;
163
213
  }
164
214
  if (key.downArrow) {
165
- setCursor((c) => (skills.length === 0 ? 0 : Math.min(skills.length - 1, c + 1)));
215
+ setCursor((c) => (items.length === 0 ? 0 : Math.min(items.length - 1, c + 1)));
166
216
  return;
167
217
  }
168
- if (key.return && focused) {
169
- if (!BACKED_INSTALL_TARGETS.has(focused.host.id)) {
170
- onToast?.({
171
- tone: "info",
172
- title: `Host '${focused.host.name}'`,
173
- body: "pending — backend without path mapping yet",
174
- });
175
- return;
176
- }
218
+ if (key.return && current) {
177
219
  setActionCursor(0);
178
220
  setMode({ kind: "detail" });
179
221
  }
180
222
  }, { isActive });
181
- // input — detail mode (↑↓ navega actions · ⏎ ejecuta focused · Esc cierra)
223
+ // input — detail (↑↓ acciones · ⏎ ejecuta · esc cierra)
182
224
  useInput((_input, key) => {
183
- if (!isActive || busy || mode.kind !== "detail" || !focused)
225
+ if (!isActive || mode.kind !== "detail" || !current)
184
226
  return;
185
227
  if (key.upArrow) {
186
228
  setActionCursor((c) => Math.max(0, c - 1));
187
229
  return;
188
230
  }
189
231
  if (key.downArrow) {
190
- setActionCursor((c) => Math.min(detailActions.length - 1, c + 1));
232
+ setActionCursor((c) => Math.min(Math.max(0, detailActions.length - 1), c + 1));
191
233
  return;
192
234
  }
193
235
  if (key.escape) {
@@ -195,129 +237,150 @@ export function SkillsTab({ ctx, isActive, onToast }) {
195
237
  return;
196
238
  }
197
239
  if (key.return) {
198
- const action = detailActions[actionCursor];
199
- if (!action)
200
- return;
201
- if (action.danger) {
202
- setMode({ kind: "confirm-uninstall", host: focused.host });
240
+ const entry = detailActions[actionCursor];
241
+ if (entry)
242
+ triggerAction(entry.id);
243
+ }
244
+ }, { isActive });
245
+ // input — confirm (y confirma · n/esc vuelve al detail)
246
+ useInput((input, key) => {
247
+ if (!isActive || mode.kind !== "confirm" || !current)
248
+ return;
249
+ if (input === "y" || input === "Y") {
250
+ const name = current.name;
251
+ if (mode.action === "uninstall") {
252
+ void runAction(`uninstalling ${name}…`, `Uninstalled · ${name}`, () => uninstallSkill(ctx, name));
203
253
  }
204
254
  else {
205
- void runComposite("install", focused.host);
206
- setMode({ kind: "list" });
255
+ void runAction(`removing ${name}…`, `Removed · ${name}`, () => removeSkill(ctx, name));
207
256
  }
208
257
  }
258
+ else if (key.escape || input === "n" || input === "N") {
259
+ setMode({ kind: "detail" });
260
+ }
209
261
  }, { isActive });
210
- // input — confirm-uninstall (y confirma · n/esc cancela)
262
+ // input — wizard-source esc
263
+ useInput((_input, key) => {
264
+ if (!isActive || mode.kind !== "wizard-source")
265
+ return;
266
+ if (key.escape)
267
+ setMode({ kind: "list" });
268
+ }, { isActive });
269
+ // input — wizard-pick (↑↓ · ⏎ elige · esc cancela)
270
+ useInput((_input, key) => {
271
+ if (!isActive || mode.kind !== "wizard-pick")
272
+ return;
273
+ if (key.escape) {
274
+ setMode({ kind: "list" });
275
+ return;
276
+ }
277
+ if (key.upArrow) {
278
+ setMode({ ...mode, cursor: Math.max(0, mode.cursor - 1) });
279
+ return;
280
+ }
281
+ if (key.downArrow) {
282
+ setMode({ ...mode, cursor: Math.min(mode.candidates.length - 1, mode.cursor + 1) });
283
+ return;
284
+ }
285
+ if (key.return) {
286
+ const pick = mode.candidates[mode.cursor];
287
+ if (pick)
288
+ setMode({ kind: "wizard-warning", source: mode.source, pick });
289
+ }
290
+ }, { isActive });
291
+ // input — wizard-warning (r registra · ⏎ registra+instala · esc cancela)
211
292
  useInput((input, key) => {
212
- if (!isActive || mode.kind !== "confirm-uninstall")
293
+ if (!isActive || mode.kind !== "wizard-warning")
213
294
  return;
214
- if (input === "y" || input === "Y") {
215
- void runComposite("uninstall", mode.host);
295
+ if (key.escape) {
216
296
  setMode({ kind: "list" });
297
+ return;
217
298
  }
218
- else if (key.escape || input === "n" || input === "N") {
219
- setMode({ kind: "detail" });
299
+ const { source, pick } = mode;
300
+ if (input === "r" || input === "R") {
301
+ void runAction(`registering ${pick}…`, `Registered · ${pick}`, () => registerSkill(ctx, { source, pick }));
302
+ return;
303
+ }
304
+ if (key.return) {
305
+ void runAction(`installing ${pick}…`, `Installed · ${pick}`, async () => {
306
+ const reg = await registerSkill(ctx, { source, pick });
307
+ if (!reg.ok)
308
+ return reg;
309
+ return installSkill(ctx, pick);
310
+ });
220
311
  }
221
312
  }, { isActive });
222
- const detailVisible = mode.kind === "detail" || mode.kind === "confirm-uninstall";
313
+ const probeSource = useCallback(async (source) => {
314
+ setMode({ kind: "busy", label: "inspecting source…" });
315
+ try {
316
+ const probe = await probeSkillSource(ctx, { source });
317
+ if (!probe.ok || !probe.data) {
318
+ onToast?.({ tone: "err", title: "Invalid source", body: probe.error?.message ?? "" });
319
+ setMode({ kind: "list" });
320
+ return;
321
+ }
322
+ const candidates = probe.data.candidates;
323
+ const single = candidates.length === 1 ? candidates[0] : undefined;
324
+ setMode(single !== undefined
325
+ ? { kind: "wizard-warning", source, pick: single }
326
+ : { kind: "wizard-pick", source, candidates, cursor: 0 });
327
+ }
328
+ catch (err) {
329
+ onToast?.({ tone: "err", title: "Error", body: err.message });
330
+ setMode({ kind: "list" });
331
+ }
332
+ }, [ctx, onToast]);
333
+ const overlayVisible = mode.kind !== "list";
334
+ const home = ctx.env.homeDir();
223
335
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PageHead, { title: "Skills", count: {
224
- label: `${installedCount}/${totalCount} hosts · ${subSkillsTotal()} sub-skills · ${WORKFLOW_CONTENT.slashCommands.length} slash commands`,
225
- tone: installedCount === 0 ? "warn" : "accent",
226
- }, action: _jsx(Text, { color: colors.mute, children: "one universal SKILL \u00B7 agent-workflow" }) }), _jsx(SectionHead, { label: "Hosts", count: totalCount, hint: `backed ${backedHosts} · pending ${pendingHosts}`, ...(detailVisible ? { rightAction: "esc to close detail" } : {}), marginTop: 0 }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, paddingRight: 2, children: skills.map((s, i) => (_jsx(ListRow, { icon: icons.diamond, iconActive: s.installed, title: s.host.name, subtitle: friendlyPath(s.host, ctx.env.homeDir()), meta: HOOKS_SUPPORTED_TARGETS.has(s.host.id) && s.hooks_installed
227
- ? [{ label: "hooks armed", tone: "ok" }]
228
- : [], state: {
229
- label: s.installed ? "installed" : s.host.backed ? "backed" : "pending",
230
- tone: s.installed ? "ok" : s.host.backed ? "dim" : "warn",
231
- }, chevron: true, active: cursor === i, widthHint: rowWidth(stdout?.columns, detailVisible) }, s.host.id))) }), focused && isBackedFocused && detailVisible ? (_jsx(DetailPanel, { bordered: true, header: {
232
- name: focused.host.name,
233
- meta: `${focused.path}${focused.hooks_installed
234
- ? `\nhooks armed · SKILL + ${WORKFLOW_CONTENT.slashCommands.length} slash + ${WORKFLOW_CONTENT.hooks.length} hooks`
235
- : ""}`,
336
+ label: `${installedCount} installed${unmanagedCount > 0 ? ` · ${unmanagedCount} unmanaged` : ""} · ${registeredCount} registered · ${recommendedCount} recommended`,
337
+ tone: installedCount > 0 ? "accent" : "warn",
338
+ }, action: _jsx(Text, { color: colors.mute, children: "~/.agents/skills + host replicas" }) }), _jsx(SectionHead, { label: "Skills", count: items.length, hint: "installed \u2192 unmanaged \u2192 registered \u2192 recommended", ...(overlayVisible ? { rightAction: "esc to close" } : {}), marginTop: 0 }), _jsxs(Box, { flexDirection: "row", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingRight: 2, children: [items.map((s, i) => {
339
+ const glyph = STATUS_GLYPH[s.status];
340
+ return (_jsx(ListRow, { icon: glyph.glyph, iconActive: glyph.active, title: s.name, subtitle: s.status === "unmanaged" && s.source === ""
341
+ ? "outside the registry"
342
+ : `${s.source}${s.ref ? ` #${s.ref}` : ""}`, meta: s.mode === "copy" ? [{ label: "copy", tone: "warn" }] : [], state: {
343
+ label: s.status,
344
+ tone: s.status === "installed"
345
+ ? "ok"
346
+ : s.status === "unmanaged"
347
+ ? "warn"
348
+ : s.status === "registered"
349
+ ? "dim"
350
+ : "info",
351
+ }, chevron: true, active: cursor === i, dimmed: mode.kind.startsWith("wizard"), widthHint: rowWidth(stdout?.columns, overlayVisible) }, s.name));
352
+ }), mode.kind === "wizard-source" ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(SectionHead, { label: "Add skill", hint: "Step 1 \u00B7 Source", rightAction: "\u23CE inspect \u00B7 esc cancel" }), _jsx(Box, { marginLeft: 2, children: _jsx(InputPrompt, { message: "source (owner/repo \u00B7 git URL \u00B7 absolute path):", onSubmit: (value) => {
353
+ const source = value.trim();
354
+ if (!source) {
355
+ setMode({ kind: "list" });
356
+ return;
357
+ }
358
+ void probeSource(source);
359
+ }, isActive: isActive }) })] })) : null, mode.kind === "wizard-pick" ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(SectionHead, { label: "Add skill", hint: `Step 2 · Pick one of ${mode.candidates.length}`, rightAction: "\u23CE choose \u00B7 esc cancel" }), _jsx(Box, { marginTop: 0, flexDirection: "column", children: mode.candidates.map((name, i) => (_jsx(ListRow, { icon: "\u00B7", title: name, active: mode.cursor === i, widthHint: rowWidth(stdout?.columns, true) }, name))) })] })) : null, mode.kind === "wizard-warning" ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(SectionHead, { label: `Add skill · ${mode.pick}`, hint: "Step 3 \u00B7 Review", rightAction: "esc cancel" }), _jsxs(Box, { marginLeft: 2, marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.warn, children: "\u26A0 A third-party skill runs with your host's permissions \u2014 review it before installing." }), _jsx(Text, { color: colors.dim, wrap: "truncate-end", children: mode.source }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.faint, children: "[\u23CE] register + install \u00B7 [r] register only" }) })] })] })) : null, mode.kind === "busy" ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warn, children: [icons.spinner, " ", mode.label] }) })) : null] }), current && (mode.kind === "detail" || mode.kind === "confirm") ? (_jsx(DetailPanel, { bordered: true, header: {
360
+ name: current.name,
361
+ meta: detailMeta(current, home),
236
362
  }, statePill: {
237
- label: isInstalled ? "installed" : "missing",
238
- tone: isInstalled ? "ok" : "dim",
239
- }, actions: detailActions, focusedAction: actionCursor, banner: mode.kind === "confirm-uninstall" ? (_jsx(ConfirmBanner, { title: `× Uninstall ${mode.host.name}?`, body: `Removes SKILL + commands + hooks from ${friendlyPath(mode.host, ctx.env.homeDir())}. Reversible with Reinstall.` })) : null })) : null] }), busy ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warn, children: [icons.spinner, " ", busy] }) })) : null, installedCount === 0 ? (_jsx(Box, { marginTop: 1, children: _jsx(QuickActions, { actions: [{ key: "i", label: "install on Claude" }] }) })) : null] }));
240
- }
241
- function subSkillsTotal() {
242
- return WORKFLOW_CONTENT.commandFamilies.reduce((n, f) => n + f.items.length, 0);
243
- }
244
- function pathForHost(host, home) {
245
- if (!BACKED_INSTALL_TARGETS.has(host.id))
246
- return null;
247
- const root = TARGET_ROOTS[host.id];
248
- if (!root)
249
- return null;
250
- return `${home}/${root.join("/")}/${SKILL_DIR_NAME}`;
251
- }
252
- function friendlyPath(host, _home) {
253
- const root = TARGET_ROOTS[host.id];
254
- if (!root)
255
- return "(not wired yet)";
256
- return `~/${root.join("/")}/${SKILL_DIR_NAME}/`;
257
- }
258
- function buildArgsFor(action, target) {
259
- const flags = new Set();
260
- const values = new Map();
261
- values.set("target", target);
262
- if (action === "install-full" || action === "uninstall-full")
263
- flags.add("--force");
264
- if (action === "clean-cache")
265
- values.set("plugin", SKILL_DIR_NAME);
266
- return {
267
- rest: [actionToSubcommand(action)],
268
- plugin: {},
269
- flags,
270
- values,
271
- valuesMulti: new Map(),
272
- };
273
- }
274
- function actionToSubcommand(action) {
275
- switch (action) {
276
- case "install-full":
277
- return "install-skill";
278
- case "uninstall-full":
279
- return "uninstall";
280
- case "clean-cache":
281
- return "clean-cache";
282
- case "clean-legacy":
283
- return "clean-legacy";
284
- }
285
- }
286
- async function dispatchAction(action, target, ctx) {
287
- const args = buildArgsFor(action, target);
288
- switch (action) {
289
- case "install-full":
290
- return selfInstallSkill(args, ctx);
291
- case "uninstall-full":
292
- return selfUninstall(args, ctx);
293
- case "clean-cache":
294
- return selfClearPluginCache(args, ctx);
295
- case "clean-legacy":
296
- return selfCleanLegacy(args, ctx);
297
- }
298
- }
299
- function buildBusyLabel(action, label) {
300
- switch (action) {
301
- case "install-full":
302
- return `installing on ${label}…`;
303
- case "uninstall-full":
304
- return `uninstalling from ${label}…`;
305
- case "clean-cache":
306
- return `cleaning cache on ${label}…`;
307
- case "clean-legacy":
308
- return `removing legacy skills from ${label}…`;
309
- }
363
+ label: current.status,
364
+ tone: current.status === "installed"
365
+ ? "ok"
366
+ : current.status === "unmanaged"
367
+ ? "warn"
368
+ : "dim",
369
+ }, actions: detailActions.map((a) => a.action), focusedAction: actionCursor, banner: mode.kind === "confirm" ? (_jsx(ConfirmBanner, { title: `× ${mode.action === "uninstall" ? "Uninstall" : "Remove"} ${current.name}?`, body: mode.action === "uninstall"
370
+ ? "Deletes canonical + replica; the registration stays."
371
+ : current.status === "installed"
372
+ ? "Uninstalls and drops the registration. A recommended skill returns to the recommended list."
373
+ : "Drops the registration. A recommended skill returns to the recommended list." })) : null })) : null] }), _jsx(Box, { marginTop: 1, children: _jsx(QuickActions, { actions: [{ key: "a", label: "add skill" }] }) })] }));
310
374
  }
311
- function buildSuccessMessage(action, label) {
312
- switch (action) {
313
- case "install-full":
314
- return `Install complete OK on ${label}.`;
315
- case "uninstall-full":
316
- return `Uninstall complete OK on ${label}.`;
317
- case "clean-cache":
318
- return `Cache cleaned on ${label}.`;
319
- case "clean-legacy":
320
- return `Legacy skills removed from ${label}.`;
375
+ function detailMeta(item, home) {
376
+ const source = `${item.source}${item.ref ? ` #${item.ref}` : ""}`;
377
+ if (item.status === "recommended")
378
+ return `${source}\n${item.description ?? ""}`;
379
+ const canonical = `${canonicalSkillsRoot(home)}/${item.name}`.replace(home, "~");
380
+ const replicas = `agents ${item.replicas.agents ? "✓" : "·"} · claude ${item.replicas.claude ? "✓" : "·"}${item.mode === "copy" ? " (copy)" : ""}`;
381
+ if (item.status === "unmanaged") {
382
+ return `${source === "" ? "unknown source" : source}\n${canonical}\n${replicas}\nInstalled outside the registry (e.g. skills.sh) — not operable from here.`;
321
383
  }
384
+ return `${source}\n${canonical}\n${replicas}`;
322
385
  }
323
386
  //# sourceMappingURL=skills-tab.js.map