yourskills 0.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.
- package/dist/main.js +4313 -0
- package/package.json +58 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,4313 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/main.tsx
|
|
4
|
+
import { realpathSync } from "fs";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { render } from "ink";
|
|
7
|
+
|
|
8
|
+
// src/auth/auth.ts
|
|
9
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
10
|
+
|
|
11
|
+
// src/settings/store.ts
|
|
12
|
+
import { chmodSync, mkdirSync, rmSync } from "node:fs";
|
|
13
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
var DEFAULT_URL = "http://localhost:3000";
|
|
17
|
+
function configDir() {
|
|
18
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
19
|
+
return join(xdg && xdg.length > 0 ? xdg : join(homedir(), ".config"), "yourskills");
|
|
20
|
+
}
|
|
21
|
+
function configPath() {
|
|
22
|
+
return join(configDir(), "config.json");
|
|
23
|
+
}
|
|
24
|
+
async function readConfig() {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(await readFile(configPath(), "utf8"));
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function writeConfig(config) {
|
|
32
|
+
mkdirSync(configDir(), { recursive: true, mode: 448 });
|
|
33
|
+
await writeFile(configPath(), `${JSON.stringify(config, null, 2)}
|
|
34
|
+
`);
|
|
35
|
+
chmodSync(configPath(), 384);
|
|
36
|
+
}
|
|
37
|
+
function clearConfig() {
|
|
38
|
+
rmSync(configPath(), { force: true });
|
|
39
|
+
}
|
|
40
|
+
function serverUrl(config = {}, override) {
|
|
41
|
+
return override || process.env.YOURSKILLS_URL || config.serverUrl || DEFAULT_URL;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/settings/agents.ts
|
|
45
|
+
async function resolveAgents(flagAgents) {
|
|
46
|
+
if (flagAgents && flagAgents.length > 0)
|
|
47
|
+
return flagAgents;
|
|
48
|
+
const config = await readConfig();
|
|
49
|
+
return config.agents ?? [];
|
|
50
|
+
}
|
|
51
|
+
// src/settings/keys.ts
|
|
52
|
+
var CONFIG_KEYS = ["scope", "agents", "server", "track"];
|
|
53
|
+
function isConfigKey(key) {
|
|
54
|
+
return CONFIG_KEYS.includes(key);
|
|
55
|
+
}
|
|
56
|
+
var REDACTED = "<redacted>";
|
|
57
|
+
var UNSET = "(unset)";
|
|
58
|
+
function configValue(config, key) {
|
|
59
|
+
switch (key) {
|
|
60
|
+
case "scope":
|
|
61
|
+
return config.defaultScope ?? UNSET;
|
|
62
|
+
case "agents":
|
|
63
|
+
return config.agents?.length ? config.agents.join(",") : UNSET;
|
|
64
|
+
case "server":
|
|
65
|
+
return config.serverUrl ?? UNSET;
|
|
66
|
+
case "track":
|
|
67
|
+
return config.track === undefined ? UNSET : String(config.track);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function setConfigValue(config, key, value) {
|
|
71
|
+
if (!isConfigKey(key)) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
message: `Unknown config key: ${key}. Known keys: ${CONFIG_KEYS.join(", ")}.`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
switch (key) {
|
|
78
|
+
case "scope": {
|
|
79
|
+
if (value !== "global" && value !== "project") {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
message: "scope must be `global` or `project`."
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
config: { ...config, defaultScope: value },
|
|
88
|
+
shown: value
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
case "agents": {
|
|
92
|
+
const agents = value.split(",").map((a) => a.trim()).filter((a) => a.length > 0);
|
|
93
|
+
if (agents.length === 0) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
message: "agents must be a comma separated list, for example `claude-code,codex`."
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
ok: true,
|
|
101
|
+
config: { ...config, agents },
|
|
102
|
+
shown: agents.join(",")
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
case "server": {
|
|
106
|
+
let url;
|
|
107
|
+
try {
|
|
108
|
+
url = new URL(value);
|
|
109
|
+
} catch {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
message: `server must be a URL, for example \`https://skills.acme.dev\`. Got: ${value}`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
116
|
+
return { ok: false, message: "server must be an http or https URL." };
|
|
117
|
+
}
|
|
118
|
+
const serverUrl2 = url.toString().replace(/\/$/, "");
|
|
119
|
+
return { ok: true, config: { ...config, serverUrl: serverUrl2 }, shown: serverUrl2 };
|
|
120
|
+
}
|
|
121
|
+
case "track": {
|
|
122
|
+
if (value !== "true" && value !== "false") {
|
|
123
|
+
return { ok: false, message: "track must be `true` or `false`." };
|
|
124
|
+
}
|
|
125
|
+
const track = value === "true";
|
|
126
|
+
return { ok: true, config: { ...config, track }, shown: String(track) };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function configLines(config) {
|
|
131
|
+
const width = Math.max(...CONFIG_KEYS.map((k) => k.length), "user".length);
|
|
132
|
+
const lines = CONFIG_KEYS.map((key) => ({
|
|
133
|
+
mark: "none",
|
|
134
|
+
text: `${key.padEnd(width)} ${configValue(config, key)}`
|
|
135
|
+
}));
|
|
136
|
+
lines.push({
|
|
137
|
+
mark: "none",
|
|
138
|
+
text: `${"user".padEnd(width)} ${config.userEmail ?? UNSET}`
|
|
139
|
+
});
|
|
140
|
+
lines.push({
|
|
141
|
+
mark: "none",
|
|
142
|
+
text: `${"token".padEnd(width)} ${config.token ? REDACTED : UNSET}`
|
|
143
|
+
});
|
|
144
|
+
return lines;
|
|
145
|
+
}
|
|
146
|
+
function configGetReport(config, key, path) {
|
|
147
|
+
if (key === undefined) {
|
|
148
|
+
return {
|
|
149
|
+
title: "config",
|
|
150
|
+
lines: configLines(config),
|
|
151
|
+
json: {
|
|
152
|
+
path,
|
|
153
|
+
...Object.fromEntries(CONFIG_KEYS.map((k) => [k, configValue(config, k)])),
|
|
154
|
+
user: config.userEmail ?? null,
|
|
155
|
+
token: config.token ? REDACTED : null
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (!isConfigKey(key)) {
|
|
160
|
+
return {
|
|
161
|
+
title: "config",
|
|
162
|
+
lines: [
|
|
163
|
+
{
|
|
164
|
+
mark: "fail",
|
|
165
|
+
text: `Unknown config key: ${key}`,
|
|
166
|
+
note: `Known keys: ${CONFIG_KEYS.join(", ")}`
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
json: { path, error: `unknown key: ${key}`, keys: CONFIG_KEYS },
|
|
170
|
+
failed: true
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const value = configValue(config, key);
|
|
174
|
+
return {
|
|
175
|
+
title: "config",
|
|
176
|
+
lines: [{ mark: "none", text: value }],
|
|
177
|
+
json: { path, key, value }
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/settings/command.ts
|
|
182
|
+
async function configGet(key) {
|
|
183
|
+
return configGetReport(await readConfig(), key, configPath());
|
|
184
|
+
}
|
|
185
|
+
async function configSet(key, value) {
|
|
186
|
+
const path = configPath();
|
|
187
|
+
const config = await readConfig();
|
|
188
|
+
const result = setConfigValue(config, key, value);
|
|
189
|
+
if (!result.ok) {
|
|
190
|
+
return {
|
|
191
|
+
title: "config",
|
|
192
|
+
lines: [{ mark: "fail", text: result.message }],
|
|
193
|
+
json: { path, error: result.message },
|
|
194
|
+
failed: true
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
await writeConfig(result.config);
|
|
198
|
+
return {
|
|
199
|
+
title: "config",
|
|
200
|
+
lines: [{ mark: "ok", text: `${key} = ${result.shown}`, note: path }],
|
|
201
|
+
json: { path, key, value: result.shown }
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// src/settings/scope.ts
|
|
205
|
+
import { existsSync } from "node:fs";
|
|
206
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
207
|
+
import { dirname, join as join2, resolve } from "node:path";
|
|
208
|
+
var PROJECT_MANIFEST = "yourskills.json";
|
|
209
|
+
function findProjectManifest(from) {
|
|
210
|
+
let dir = resolve(from ?? process.cwd());
|
|
211
|
+
for (;; ) {
|
|
212
|
+
const candidate = join2(dir, PROJECT_MANIFEST);
|
|
213
|
+
if (existsSync(candidate))
|
|
214
|
+
return candidate;
|
|
215
|
+
const parent = dirname(dir);
|
|
216
|
+
if (parent === dir)
|
|
217
|
+
return null;
|
|
218
|
+
dir = parent;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function scopeInManifest(path) {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = JSON.parse(await readFile2(path, "utf8"));
|
|
224
|
+
return parsed.scope === "global" || parsed.scope === "project" ? parsed.scope : null;
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function resolveScope(opts = {}) {
|
|
230
|
+
const manifestPath = findProjectManifest(opts.cwd);
|
|
231
|
+
if (opts.flag)
|
|
232
|
+
return { scope: opts.flag, source: "flag", manifestPath };
|
|
233
|
+
if (manifestPath) {
|
|
234
|
+
const declared = await scopeInManifest(manifestPath);
|
|
235
|
+
if (declared)
|
|
236
|
+
return { scope: declared, source: "project", manifestPath };
|
|
237
|
+
}
|
|
238
|
+
const config = await readConfig();
|
|
239
|
+
if (config.defaultScope)
|
|
240
|
+
return { scope: config.defaultScope, source: "config", manifestPath };
|
|
241
|
+
return { scope: "global", source: "default", manifestPath };
|
|
242
|
+
}
|
|
243
|
+
// src/auth/auth.ts
|
|
244
|
+
var CLIENT_ID = "yourskills-cli";
|
|
245
|
+
var GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
246
|
+
var SLOW_DOWN_STEP_MS = 5000;
|
|
247
|
+
var authBase = (base) => `${base.replace(/\/$/, "")}/api/auth`;
|
|
248
|
+
async function post(url, body) {
|
|
249
|
+
const res = await fetch(url, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: { "content-type": "application/json" },
|
|
252
|
+
body: JSON.stringify(body)
|
|
253
|
+
});
|
|
254
|
+
const text = await res.text();
|
|
255
|
+
let parsed;
|
|
256
|
+
try {
|
|
257
|
+
parsed = JSON.parse(text);
|
|
258
|
+
} catch {
|
|
259
|
+
return {
|
|
260
|
+
error: "invalid_response",
|
|
261
|
+
error_description: text.slice(0, 200) || res.statusText
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (res.ok)
|
|
265
|
+
return { ok: true, data: parsed };
|
|
266
|
+
const err = parsed;
|
|
267
|
+
return {
|
|
268
|
+
error: err.error ?? `http_${res.status}`,
|
|
269
|
+
error_description: err.error_description
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
async function* login() {
|
|
273
|
+
const config = await readConfig();
|
|
274
|
+
const base = serverUrl(config);
|
|
275
|
+
const start = await post(`${authBase(base)}/device/code`, {
|
|
276
|
+
client_id: CLIENT_ID
|
|
277
|
+
});
|
|
278
|
+
if (!("ok" in start)) {
|
|
279
|
+
throw new Error(`Could not start device login: ${start.error_description ?? start.error}`);
|
|
280
|
+
}
|
|
281
|
+
const device = start.data;
|
|
282
|
+
const deadline = Date.now() + device.expires_in * 1000;
|
|
283
|
+
yield {
|
|
284
|
+
kind: "prompt",
|
|
285
|
+
userCode: device.user_code,
|
|
286
|
+
verificationUri: device.verification_uri_complete ?? device.verification_uri,
|
|
287
|
+
expiresAt: deadline
|
|
288
|
+
};
|
|
289
|
+
let intervalMs = Math.max(device.interval, 1) * 1000;
|
|
290
|
+
while (Date.now() < deadline) {
|
|
291
|
+
await sleep(intervalMs);
|
|
292
|
+
const poll = await post(`${authBase(base)}/device/token`, {
|
|
293
|
+
grant_type: GRANT_TYPE,
|
|
294
|
+
device_code: device.device_code,
|
|
295
|
+
client_id: CLIENT_ID
|
|
296
|
+
});
|
|
297
|
+
if ("ok" in poll) {
|
|
298
|
+
const token = poll.data.access_token;
|
|
299
|
+
const who = await fetchSession(base, token);
|
|
300
|
+
await writeConfig({
|
|
301
|
+
...config,
|
|
302
|
+
token,
|
|
303
|
+
serverUrl: base,
|
|
304
|
+
userEmail: who?.user?.email
|
|
305
|
+
});
|
|
306
|
+
yield { kind: "done", email: who?.user?.email };
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
switch (poll.error) {
|
|
310
|
+
case "authorization_pending":
|
|
311
|
+
yield { kind: "waiting" };
|
|
312
|
+
break;
|
|
313
|
+
case "slow_down":
|
|
314
|
+
intervalMs += SLOW_DOWN_STEP_MS;
|
|
315
|
+
yield { kind: "waiting" };
|
|
316
|
+
break;
|
|
317
|
+
case "access_denied":
|
|
318
|
+
throw new Error("Login was denied.");
|
|
319
|
+
case "expired_token":
|
|
320
|
+
throw new Error("The device code expired. Run `yourskills login` again.");
|
|
321
|
+
default:
|
|
322
|
+
throw new Error(`Login failed: ${poll.error_description ?? poll.error}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
throw new Error("The device code expired. Run `yourskills login` again.");
|
|
326
|
+
}
|
|
327
|
+
async function logout() {
|
|
328
|
+
const config = await readConfig();
|
|
329
|
+
if (config.token) {
|
|
330
|
+
await fetch(`${authBase(serverUrl(config))}/sign-out`, {
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: { authorization: `Bearer ${config.token}` }
|
|
333
|
+
}).catch(() => {
|
|
334
|
+
return;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
clearConfig();
|
|
338
|
+
}
|
|
339
|
+
async function fetchSession(base, token) {
|
|
340
|
+
const res = await fetch(`${authBase(base)}/get-session`, {
|
|
341
|
+
headers: { authorization: `Bearer ${token}` }
|
|
342
|
+
});
|
|
343
|
+
if (!res.ok)
|
|
344
|
+
return null;
|
|
345
|
+
return await res.json();
|
|
346
|
+
}
|
|
347
|
+
async function whoami(server) {
|
|
348
|
+
const config = await readConfig();
|
|
349
|
+
if (!config.token)
|
|
350
|
+
throw new Error("Not logged in. Run `yourskills login`.");
|
|
351
|
+
const base = serverUrl(config, server);
|
|
352
|
+
const session = await fetchSession(base, config.token);
|
|
353
|
+
if (!session?.user)
|
|
354
|
+
throw new Error("Session is no longer valid. Run `yourskills login`.");
|
|
355
|
+
return {
|
|
356
|
+
user: session.user.email ?? session.user.id,
|
|
357
|
+
org: session.session?.activeOrganizationId ?? null,
|
|
358
|
+
server: base
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
async function requireToken(server) {
|
|
362
|
+
const config = await readConfig();
|
|
363
|
+
if (!config.token)
|
|
364
|
+
throw new Error("Not logged in. Run `yourskills login`.");
|
|
365
|
+
return { token: config.token, base: serverUrl(config, server) };
|
|
366
|
+
}
|
|
367
|
+
async function hasSession() {
|
|
368
|
+
return Boolean((await readConfig()).token);
|
|
369
|
+
}
|
|
370
|
+
// src/auth/screens/gate.tsx
|
|
371
|
+
import { Box as Box5, Text as Text7, useApp as useApp2, useInput } from "ink";
|
|
372
|
+
import {
|
|
373
|
+
useCallback,
|
|
374
|
+
useEffect as useEffect3,
|
|
375
|
+
useMemo,
|
|
376
|
+
useState as useState3
|
|
377
|
+
} from "react";
|
|
378
|
+
|
|
379
|
+
// src/ui/command.tsx
|
|
380
|
+
import { Box as Box2, Text as Text3 } from "ink";
|
|
381
|
+
|
|
382
|
+
// ../../packages/ui/src/lib/ascii.ts
|
|
383
|
+
var DEFAULT_ASCII_CHARS = {
|
|
384
|
+
top: "-",
|
|
385
|
+
bottom: "-",
|
|
386
|
+
left: "|",
|
|
387
|
+
right: "|",
|
|
388
|
+
divider: "-",
|
|
389
|
+
junction: "+"
|
|
390
|
+
};
|
|
391
|
+
function fill(n, seq = "-") {
|
|
392
|
+
const count = Math.max(n, 0);
|
|
393
|
+
if (count === 0) {
|
|
394
|
+
return "";
|
|
395
|
+
}
|
|
396
|
+
const unit = seq.length > 0 ? seq : " ";
|
|
397
|
+
return unit.repeat(Math.ceil(count / unit.length)).slice(0, count);
|
|
398
|
+
}
|
|
399
|
+
function junctionGlyph(chars) {
|
|
400
|
+
return chars.junction.length > 0 ? chars.junction[0] : DEFAULT_ASCII_CHARS.junction;
|
|
401
|
+
}
|
|
402
|
+
function topBorder(width, title, chars = DEFAULT_ASCII_CHARS) {
|
|
403
|
+
const j = junctionGlyph(chars);
|
|
404
|
+
if (!title) {
|
|
405
|
+
return `${j}${fill(width - 2, chars.top)}${j}`;
|
|
406
|
+
}
|
|
407
|
+
const label = ` ${title} `;
|
|
408
|
+
const left = 2;
|
|
409
|
+
const right = Math.max(width - 2 - left - label.length, 1);
|
|
410
|
+
return `${j}${fill(left, chars.top)}${label}${fill(right, chars.top)}${j}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/ui/frame.tsx
|
|
414
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
415
|
+
|
|
416
|
+
// src/ui/theme.ts
|
|
417
|
+
var tone = {
|
|
418
|
+
primary: "cyan",
|
|
419
|
+
notice: "yellow",
|
|
420
|
+
danger: "red"
|
|
421
|
+
};
|
|
422
|
+
var stateTone = {
|
|
423
|
+
"state-active": undefined,
|
|
424
|
+
"state-experimental": "yellow",
|
|
425
|
+
"state-deprecated": "red"
|
|
426
|
+
};
|
|
427
|
+
var mark = {
|
|
428
|
+
ok: "+",
|
|
429
|
+
fail: "x",
|
|
430
|
+
pending: ".",
|
|
431
|
+
cursor: ">",
|
|
432
|
+
selected: "*",
|
|
433
|
+
unselected: " "
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// src/ui/frame.tsx
|
|
437
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
438
|
+
var MAX_WIDTH = 96;
|
|
439
|
+
var MIN_WIDTH = 44;
|
|
440
|
+
var FRAME_CHROME = 4;
|
|
441
|
+
function useFrameWidth() {
|
|
442
|
+
const { columns } = useWindowSize();
|
|
443
|
+
return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, columns - 2));
|
|
444
|
+
}
|
|
445
|
+
var j = DEFAULT_ASCII_CHARS.junction;
|
|
446
|
+
var BORDER = {
|
|
447
|
+
topLeft: j,
|
|
448
|
+
top: DEFAULT_ASCII_CHARS.top,
|
|
449
|
+
topRight: j,
|
|
450
|
+
right: DEFAULT_ASCII_CHARS.right,
|
|
451
|
+
bottomRight: j,
|
|
452
|
+
bottom: DEFAULT_ASCII_CHARS.bottom,
|
|
453
|
+
bottomLeft: j,
|
|
454
|
+
left: DEFAULT_ASCII_CHARS.left
|
|
455
|
+
};
|
|
456
|
+
function Frame({
|
|
457
|
+
title,
|
|
458
|
+
width,
|
|
459
|
+
children
|
|
460
|
+
}) {
|
|
461
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
462
|
+
flexDirection: "column",
|
|
463
|
+
width,
|
|
464
|
+
children: [
|
|
465
|
+
/* @__PURE__ */ jsx(Text, {
|
|
466
|
+
color: tone.primary,
|
|
467
|
+
children: topBorder(width, title)
|
|
468
|
+
}),
|
|
469
|
+
/* @__PURE__ */ jsx(Box, {
|
|
470
|
+
flexDirection: "column",
|
|
471
|
+
width,
|
|
472
|
+
paddingLeft: 1,
|
|
473
|
+
paddingRight: 1,
|
|
474
|
+
borderStyle: BORDER,
|
|
475
|
+
borderColor: tone.primary,
|
|
476
|
+
borderTop: false,
|
|
477
|
+
children
|
|
478
|
+
})
|
|
479
|
+
]
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function Divider({ width }) {
|
|
483
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
484
|
+
marginLeft: -1,
|
|
485
|
+
marginRight: -1,
|
|
486
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
487
|
+
color: tone.primary,
|
|
488
|
+
children: fill(width - 2, DEFAULT_ASCII_CHARS.divider)
|
|
489
|
+
})
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function Blank() {
|
|
493
|
+
return /* @__PURE__ */ jsx(Text, {
|
|
494
|
+
children: " "
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
function clip(text, width) {
|
|
498
|
+
if (width <= 0)
|
|
499
|
+
return "";
|
|
500
|
+
if (text.length <= width)
|
|
501
|
+
return text;
|
|
502
|
+
return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}~`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/ui/text-input.tsx
|
|
506
|
+
import { Text as Text2 } from "ink";
|
|
507
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
508
|
+
function SearchInput({
|
|
509
|
+
value,
|
|
510
|
+
placeholder = "",
|
|
511
|
+
focus = true
|
|
512
|
+
}) {
|
|
513
|
+
return /* @__PURE__ */ jsxs2(Text2, {
|
|
514
|
+
children: [
|
|
515
|
+
value.length > 0 ? /* @__PURE__ */ jsx2(Text2, {
|
|
516
|
+
children: value
|
|
517
|
+
}) : /* @__PURE__ */ jsx2(Text2, {
|
|
518
|
+
dimColor: true,
|
|
519
|
+
children: placeholder
|
|
520
|
+
}),
|
|
521
|
+
focus ? /* @__PURE__ */ jsx2(Text2, {
|
|
522
|
+
color: tone.primary,
|
|
523
|
+
children: "_"
|
|
524
|
+
}) : null
|
|
525
|
+
]
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/ui/command.tsx
|
|
530
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
531
|
+
function filterCommands(groups, query) {
|
|
532
|
+
if (query.length === 0)
|
|
533
|
+
return groups;
|
|
534
|
+
const q = query.toLowerCase();
|
|
535
|
+
return groups.map((group) => ({
|
|
536
|
+
group: group.group,
|
|
537
|
+
items: group.items.filter((item) => item.label.toLowerCase().includes(q) || (item.hint ?? "").toLowerCase().includes(q))
|
|
538
|
+
})).filter((group) => group.items.length > 0);
|
|
539
|
+
}
|
|
540
|
+
function flattenCommands(groups) {
|
|
541
|
+
return groups.flatMap((group) => group.items);
|
|
542
|
+
}
|
|
543
|
+
var POINTER = 2;
|
|
544
|
+
var GAP = 2;
|
|
545
|
+
function CommandRow({
|
|
546
|
+
item,
|
|
547
|
+
width,
|
|
548
|
+
active
|
|
549
|
+
}) {
|
|
550
|
+
const hint = item.hint ?? "";
|
|
551
|
+
const room = width - POINTER - (hint ? hint.length + GAP : 0);
|
|
552
|
+
const label = clip(item.label, Math.max(room, 0));
|
|
553
|
+
const gap = Math.max(width - POINTER - label.length - hint.length, hint ? 1 : 0);
|
|
554
|
+
return /* @__PURE__ */ jsxs3(Text3, {
|
|
555
|
+
children: [
|
|
556
|
+
/* @__PURE__ */ jsx3(Text3, {
|
|
557
|
+
color: tone.primary,
|
|
558
|
+
children: `${active ? mark.cursor : " "} `
|
|
559
|
+
}),
|
|
560
|
+
/* @__PURE__ */ jsx3(Text3, {
|
|
561
|
+
bold: active,
|
|
562
|
+
color: active ? tone.primary : undefined,
|
|
563
|
+
children: label
|
|
564
|
+
}),
|
|
565
|
+
/* @__PURE__ */ jsx3(Text3, {
|
|
566
|
+
dimColor: true,
|
|
567
|
+
children: `${" ".repeat(gap)}${hint}`
|
|
568
|
+
})
|
|
569
|
+
]
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
function CommandPalette({
|
|
573
|
+
groups,
|
|
574
|
+
query,
|
|
575
|
+
cursor,
|
|
576
|
+
width,
|
|
577
|
+
placeholder = "type a command or search",
|
|
578
|
+
emptyMessage = "No matching commands."
|
|
579
|
+
}) {
|
|
580
|
+
const visible = filterCommands(groups, query);
|
|
581
|
+
const rows = flattenCommands(visible);
|
|
582
|
+
const current = rows[cursor];
|
|
583
|
+
const interior = width - FRAME_CHROME;
|
|
584
|
+
return /* @__PURE__ */ jsxs3(Box2, {
|
|
585
|
+
flexDirection: "column",
|
|
586
|
+
children: [
|
|
587
|
+
/* @__PURE__ */ jsxs3(Box2, {
|
|
588
|
+
children: [
|
|
589
|
+
/* @__PURE__ */ jsx3(Text3, {
|
|
590
|
+
color: tone.primary,
|
|
591
|
+
children: "> "
|
|
592
|
+
}),
|
|
593
|
+
/* @__PURE__ */ jsx3(SearchInput, {
|
|
594
|
+
value: query,
|
|
595
|
+
placeholder
|
|
596
|
+
})
|
|
597
|
+
]
|
|
598
|
+
}),
|
|
599
|
+
/* @__PURE__ */ jsx3(Divider, {
|
|
600
|
+
width
|
|
601
|
+
}),
|
|
602
|
+
rows.length === 0 ? /* @__PURE__ */ jsx3(Text3, {
|
|
603
|
+
dimColor: true,
|
|
604
|
+
children: emptyMessage
|
|
605
|
+
}) : visible.map((group, i) => /* @__PURE__ */ jsxs3(Box2, {
|
|
606
|
+
flexDirection: "column",
|
|
607
|
+
children: [
|
|
608
|
+
i > 0 ? /* @__PURE__ */ jsx3(Divider, {
|
|
609
|
+
width
|
|
610
|
+
}) : null,
|
|
611
|
+
/* @__PURE__ */ jsx3(Text3, {
|
|
612
|
+
dimColor: true,
|
|
613
|
+
children: group.group.toUpperCase()
|
|
614
|
+
}),
|
|
615
|
+
group.items.map((item) => /* @__PURE__ */ jsx3(CommandRow, {
|
|
616
|
+
item,
|
|
617
|
+
width: interior,
|
|
618
|
+
active: item.value === current?.value
|
|
619
|
+
}, item.value))
|
|
620
|
+
]
|
|
621
|
+
}, group.group))
|
|
622
|
+
]
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
// ../../packages/skills/src/lifecycle.ts
|
|
626
|
+
var LIFECYCLES = [
|
|
627
|
+
{ status: "active", word: "active", glyph: "#", tone: "state-active" },
|
|
628
|
+
{
|
|
629
|
+
status: "experimental",
|
|
630
|
+
word: "experimental",
|
|
631
|
+
glyph: "?",
|
|
632
|
+
tone: "state-experimental"
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
status: "deprecated",
|
|
636
|
+
word: "deprecated",
|
|
637
|
+
glyph: "x",
|
|
638
|
+
tone: "state-deprecated"
|
|
639
|
+
}
|
|
640
|
+
];
|
|
641
|
+
var LIFECYCLE_ORDER = LIFECYCLES.map((lifecycle) => lifecycle.status);
|
|
642
|
+
var BY_STATUS = new Map(LIFECYCLES.map((lifecycle) => [lifecycle.status, lifecycle]));
|
|
643
|
+
function lifecycleOf(status) {
|
|
644
|
+
const lifecycle = BY_STATUS.get(status);
|
|
645
|
+
if (!lifecycle) {
|
|
646
|
+
throw new Error(`No lifecycle vocabulary for "${status}". Add it to LIFECYCLES.`);
|
|
647
|
+
}
|
|
648
|
+
return lifecycle;
|
|
649
|
+
}
|
|
650
|
+
// src/ui/rows.tsx
|
|
651
|
+
import { Box as Box3, Text as Text4 } from "ink";
|
|
652
|
+
import { jsx as jsx4, jsxs as jsxs4, Fragment } from "react/jsx-runtime";
|
|
653
|
+
function lifecycleTag(status) {
|
|
654
|
+
if (!status || status === "active")
|
|
655
|
+
return "";
|
|
656
|
+
return `[${lifecycleOf(status).word}]`;
|
|
657
|
+
}
|
|
658
|
+
function lifecycleColor(status) {
|
|
659
|
+
return stateTone[lifecycleOf(status ?? "active").tone];
|
|
660
|
+
}
|
|
661
|
+
var GUTTER = 4;
|
|
662
|
+
var GAP2 = 2;
|
|
663
|
+
function nameWidth(rows, max = 28) {
|
|
664
|
+
return Math.min(max, Math.max(8, ...rows.map((r) => r.name.length)));
|
|
665
|
+
}
|
|
666
|
+
function SkillRow({
|
|
667
|
+
row,
|
|
668
|
+
width,
|
|
669
|
+
names,
|
|
670
|
+
cursor,
|
|
671
|
+
selected,
|
|
672
|
+
selectable = false
|
|
673
|
+
}) {
|
|
674
|
+
const tag = lifecycleTag(row.status);
|
|
675
|
+
const meta = row.count !== undefined ? `${row.count} skills` : tag;
|
|
676
|
+
const gutter = selectable ? GUTTER : 0;
|
|
677
|
+
const rest = width - gutter - names - GAP2 - (meta ? meta.length + GAP2 : 0);
|
|
678
|
+
return /* @__PURE__ */ jsxs4(Box3, {
|
|
679
|
+
children: [
|
|
680
|
+
selectable ? /* @__PURE__ */ jsx4(Text4, {
|
|
681
|
+
color: cursor ? "cyan" : undefined,
|
|
682
|
+
children: `${cursor ? mark.cursor : " "} ${selected ? mark.selected : mark.unselected} `
|
|
683
|
+
}) : null,
|
|
684
|
+
/* @__PURE__ */ jsx4(Text4, {
|
|
685
|
+
bold: cursor,
|
|
686
|
+
color: lifecycleColor(row.status),
|
|
687
|
+
children: clip(row.name, names).padEnd(names)
|
|
688
|
+
}),
|
|
689
|
+
/* @__PURE__ */ jsx4(Text4, {
|
|
690
|
+
children: " ".repeat(GAP2)
|
|
691
|
+
}),
|
|
692
|
+
/* @__PURE__ */ jsx4(Text4, {
|
|
693
|
+
dimColor: true,
|
|
694
|
+
children: clip(row.description ?? "", Math.max(rest, 0))
|
|
695
|
+
}),
|
|
696
|
+
meta ? /* @__PURE__ */ jsxs4(Fragment, {
|
|
697
|
+
children: [
|
|
698
|
+
/* @__PURE__ */ jsx4(Text4, {
|
|
699
|
+
children: " ".repeat(GAP2)
|
|
700
|
+
}),
|
|
701
|
+
/* @__PURE__ */ jsx4(Text4, {
|
|
702
|
+
color: lifecycleColor(row.status),
|
|
703
|
+
dimColor: !tag,
|
|
704
|
+
children: meta
|
|
705
|
+
})
|
|
706
|
+
]
|
|
707
|
+
}) : null
|
|
708
|
+
]
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
function Hints({ hints }) {
|
|
712
|
+
return /* @__PURE__ */ jsx4(Box3, {
|
|
713
|
+
children: hints.map(([key, label], i) => /* @__PURE__ */ jsxs4(Text4, {
|
|
714
|
+
children: [
|
|
715
|
+
i > 0 ? /* @__PURE__ */ jsx4(Text4, {
|
|
716
|
+
dimColor: true,
|
|
717
|
+
children: " "
|
|
718
|
+
}) : null,
|
|
719
|
+
/* @__PURE__ */ jsx4(Text4, {
|
|
720
|
+
color: "cyan",
|
|
721
|
+
children: key
|
|
722
|
+
}),
|
|
723
|
+
/* @__PURE__ */ jsx4(Text4, {
|
|
724
|
+
dimColor: true,
|
|
725
|
+
children: ` ${label}`
|
|
726
|
+
})
|
|
727
|
+
]
|
|
728
|
+
}, key))
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
// src/ui/spinner.tsx
|
|
732
|
+
import { Text as Text5 } from "ink";
|
|
733
|
+
import { useEffect, useState } from "react";
|
|
734
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
735
|
+
var FRAMES = ["-", "\\", "|", "/"];
|
|
736
|
+
var INTERVAL_MS = 90;
|
|
737
|
+
function Spinner() {
|
|
738
|
+
const [frame, setFrame] = useState(0);
|
|
739
|
+
useEffect(() => {
|
|
740
|
+
const timer = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), INTERVAL_MS);
|
|
741
|
+
return () => clearInterval(timer);
|
|
742
|
+
}, []);
|
|
743
|
+
return /* @__PURE__ */ jsx5(Text5, {
|
|
744
|
+
color: tone.primary,
|
|
745
|
+
children: FRAMES[frame]
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
// src/auth/screens/login.tsx
|
|
749
|
+
import { Box as Box4, Text as Text6, useApp } from "ink";
|
|
750
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
751
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
752
|
+
function minutesLeft(expiresAt) {
|
|
753
|
+
return Math.max(0, Math.ceil((expiresAt - Date.now()) / 60000));
|
|
754
|
+
}
|
|
755
|
+
function LoginScreen({
|
|
756
|
+
onError,
|
|
757
|
+
onDone
|
|
758
|
+
}) {
|
|
759
|
+
const { exit } = useApp();
|
|
760
|
+
const width = useFrameWidth();
|
|
761
|
+
const [state, setState] = useState2({ phase: "starting" });
|
|
762
|
+
useEffect2(() => {
|
|
763
|
+
let live = true;
|
|
764
|
+
(async () => {
|
|
765
|
+
try {
|
|
766
|
+
for await (const event of login()) {
|
|
767
|
+
if (!live)
|
|
768
|
+
return;
|
|
769
|
+
if (event.kind === "prompt") {
|
|
770
|
+
setState({ phase: "waiting", prompt: event });
|
|
771
|
+
} else if (event.kind === "done") {
|
|
772
|
+
setState({ phase: "done", email: event.email });
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
} catch (error) {
|
|
776
|
+
if (live)
|
|
777
|
+
onError(error);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (!live)
|
|
781
|
+
return;
|
|
782
|
+
if (onDone)
|
|
783
|
+
onDone();
|
|
784
|
+
else
|
|
785
|
+
exit();
|
|
786
|
+
})();
|
|
787
|
+
return () => {
|
|
788
|
+
live = false;
|
|
789
|
+
};
|
|
790
|
+
}, [exit, onError, onDone]);
|
|
791
|
+
if (state.phase === "done") {
|
|
792
|
+
return /* @__PURE__ */ jsx6(Frame, {
|
|
793
|
+
title: "login",
|
|
794
|
+
width,
|
|
795
|
+
children: /* @__PURE__ */ jsxs5(Text6, {
|
|
796
|
+
children: [
|
|
797
|
+
/* @__PURE__ */ jsx6(Text6, {
|
|
798
|
+
color: "green",
|
|
799
|
+
children: mark.ok
|
|
800
|
+
}),
|
|
801
|
+
/* @__PURE__ */ jsx6(Text6, {
|
|
802
|
+
children: ` Logged in${state.email ? ` as ${state.email}` : ""}.`
|
|
803
|
+
})
|
|
804
|
+
]
|
|
805
|
+
})
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
if (state.phase === "starting") {
|
|
809
|
+
return /* @__PURE__ */ jsx6(Frame, {
|
|
810
|
+
title: "login",
|
|
811
|
+
width,
|
|
812
|
+
children: /* @__PURE__ */ jsxs5(Text6, {
|
|
813
|
+
children: [
|
|
814
|
+
/* @__PURE__ */ jsx6(Spinner, {}),
|
|
815
|
+
/* @__PURE__ */ jsx6(Text6, {
|
|
816
|
+
dimColor: true,
|
|
817
|
+
children: " Requesting a device code..."
|
|
818
|
+
})
|
|
819
|
+
]
|
|
820
|
+
})
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
const { prompt } = state;
|
|
824
|
+
return /* @__PURE__ */ jsx6(Frame, {
|
|
825
|
+
title: "login",
|
|
826
|
+
width,
|
|
827
|
+
children: /* @__PURE__ */ jsxs5(Box4, {
|
|
828
|
+
flexDirection: "column",
|
|
829
|
+
children: [
|
|
830
|
+
/* @__PURE__ */ jsx6(Text6, {
|
|
831
|
+
dimColor: true,
|
|
832
|
+
children: "Open this URL in your browser:"
|
|
833
|
+
}),
|
|
834
|
+
/* @__PURE__ */ jsx6(Text6, {
|
|
835
|
+
color: tone.primary,
|
|
836
|
+
children: prompt.verificationUri
|
|
837
|
+
}),
|
|
838
|
+
/* @__PURE__ */ jsx6(Box4, {
|
|
839
|
+
height: 1
|
|
840
|
+
}),
|
|
841
|
+
/* @__PURE__ */ jsx6(Text6, {
|
|
842
|
+
dimColor: true,
|
|
843
|
+
children: "and enter the code:"
|
|
844
|
+
}),
|
|
845
|
+
/* @__PURE__ */ jsx6(Text6, {
|
|
846
|
+
bold: true,
|
|
847
|
+
color: tone.notice,
|
|
848
|
+
children: prompt.userCode
|
|
849
|
+
}),
|
|
850
|
+
/* @__PURE__ */ jsx6(Box4, {
|
|
851
|
+
height: 1
|
|
852
|
+
}),
|
|
853
|
+
/* @__PURE__ */ jsxs5(Text6, {
|
|
854
|
+
children: [
|
|
855
|
+
/* @__PURE__ */ jsx6(Spinner, {}),
|
|
856
|
+
/* @__PURE__ */ jsx6(Text6, {
|
|
857
|
+
dimColor: true,
|
|
858
|
+
children: ` Waiting for approval — expires in ${minutesLeft(prompt.expiresAt)} min`
|
|
859
|
+
})
|
|
860
|
+
]
|
|
861
|
+
})
|
|
862
|
+
]
|
|
863
|
+
})
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/auth/screens/gate.tsx
|
|
868
|
+
import { jsx as jsx7, jsxs as jsxs6, Fragment as Fragment2 } from "react/jsx-runtime";
|
|
869
|
+
function AuthGate({
|
|
870
|
+
title,
|
|
871
|
+
build,
|
|
872
|
+
onError,
|
|
873
|
+
check = hasSession,
|
|
874
|
+
connect
|
|
875
|
+
}) {
|
|
876
|
+
const { exit } = useApp2();
|
|
877
|
+
const width = useFrameWidth();
|
|
878
|
+
const [phase, setPhase] = useState3({ kind: "checking" });
|
|
879
|
+
const open = useCallback(() => {
|
|
880
|
+
connect().then((client) => {
|
|
881
|
+
process.exitCode = 0;
|
|
882
|
+
setPhase({ kind: "ready", client });
|
|
883
|
+
}).catch((error) => onError(error));
|
|
884
|
+
}, [connect, onError]);
|
|
885
|
+
useEffect3(() => {
|
|
886
|
+
let live = true;
|
|
887
|
+
check().then((ok) => {
|
|
888
|
+
if (!live)
|
|
889
|
+
return;
|
|
890
|
+
if (ok)
|
|
891
|
+
return open();
|
|
892
|
+
process.exitCode = 1;
|
|
893
|
+
setPhase({ kind: "blocked" });
|
|
894
|
+
}).catch((error) => {
|
|
895
|
+
if (live)
|
|
896
|
+
onError(error);
|
|
897
|
+
});
|
|
898
|
+
return () => {
|
|
899
|
+
live = false;
|
|
900
|
+
};
|
|
901
|
+
}, [check, open, onError]);
|
|
902
|
+
useInput((input, key) => {
|
|
903
|
+
if (input === "l")
|
|
904
|
+
setPhase({ kind: "logging-in" });
|
|
905
|
+
if (input === "q" || key.escape)
|
|
906
|
+
exit();
|
|
907
|
+
}, { isActive: phase.kind === "blocked" });
|
|
908
|
+
const child = useMemo(() => phase.kind === "ready" ? build(phase.client) : null, [phase, build]);
|
|
909
|
+
if (phase.kind === "ready")
|
|
910
|
+
return /* @__PURE__ */ jsx7(Fragment2, {
|
|
911
|
+
children: child
|
|
912
|
+
});
|
|
913
|
+
if (phase.kind === "logging-in")
|
|
914
|
+
return /* @__PURE__ */ jsx7(LoginScreen, {
|
|
915
|
+
onError,
|
|
916
|
+
onDone: open
|
|
917
|
+
});
|
|
918
|
+
if (phase.kind === "checking")
|
|
919
|
+
return /* @__PURE__ */ jsx7(Frame, {
|
|
920
|
+
title,
|
|
921
|
+
width,
|
|
922
|
+
children: /* @__PURE__ */ jsxs6(Text7, {
|
|
923
|
+
children: [
|
|
924
|
+
/* @__PURE__ */ jsx7(Spinner, {}),
|
|
925
|
+
/* @__PURE__ */ jsx7(Text7, {
|
|
926
|
+
dimColor: true,
|
|
927
|
+
children: " Checking session..."
|
|
928
|
+
})
|
|
929
|
+
]
|
|
930
|
+
})
|
|
931
|
+
});
|
|
932
|
+
return /* @__PURE__ */ jsx7(Frame, {
|
|
933
|
+
title,
|
|
934
|
+
width,
|
|
935
|
+
children: /* @__PURE__ */ jsxs6(Box5, {
|
|
936
|
+
flexDirection: "column",
|
|
937
|
+
children: [
|
|
938
|
+
/* @__PURE__ */ jsxs6(Text7, {
|
|
939
|
+
children: [
|
|
940
|
+
/* @__PURE__ */ jsx7(Text7, {
|
|
941
|
+
color: tone.notice,
|
|
942
|
+
children: mark.fail
|
|
943
|
+
}),
|
|
944
|
+
/* @__PURE__ */ jsx7(Text7, {
|
|
945
|
+
children: ` Not signed in — ${title} needs a session.`
|
|
946
|
+
})
|
|
947
|
+
]
|
|
948
|
+
}),
|
|
949
|
+
/* @__PURE__ */ jsx7(Blank, {}),
|
|
950
|
+
/* @__PURE__ */ jsxs6(Text7, {
|
|
951
|
+
dimColor: true,
|
|
952
|
+
children: [
|
|
953
|
+
"press ",
|
|
954
|
+
/* @__PURE__ */ jsx7(Text7, {
|
|
955
|
+
color: tone.primary,
|
|
956
|
+
children: "l"
|
|
957
|
+
}),
|
|
958
|
+
" to log in, ",
|
|
959
|
+
/* @__PURE__ */ jsx7(Text7, {
|
|
960
|
+
color: tone.primary,
|
|
961
|
+
children: "q"
|
|
962
|
+
}),
|
|
963
|
+
" to quit"
|
|
964
|
+
]
|
|
965
|
+
}),
|
|
966
|
+
/* @__PURE__ */ jsxs6(Text7, {
|
|
967
|
+
dimColor: true,
|
|
968
|
+
children: [
|
|
969
|
+
"or run ",
|
|
970
|
+
/* @__PURE__ */ jsx7(Text7, {
|
|
971
|
+
color: tone.primary,
|
|
972
|
+
children: "yourskills login"
|
|
973
|
+
}),
|
|
974
|
+
" elsewhere"
|
|
975
|
+
]
|
|
976
|
+
})
|
|
977
|
+
]
|
|
978
|
+
})
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
// src/catalog/screens/browser.tsx
|
|
982
|
+
import { Box as Box7, Text as Text9, useApp as useApp4, useInput as useInput2, useWindowSize as useWindowSize2 } from "ink";
|
|
983
|
+
import { useCallback as useCallback2, useEffect as useEffect5, useMemo as useMemo2, useState as useState5 } from "react";
|
|
984
|
+
|
|
985
|
+
// src/install/install.ts
|
|
986
|
+
import { spawn } from "node:child_process";
|
|
987
|
+
import { createRequire } from "node:module";
|
|
988
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
989
|
+
|
|
990
|
+
// src/manifest/manifest.ts
|
|
991
|
+
import { mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
|
|
992
|
+
import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
|
|
993
|
+
function emptyManifest(scope) {
|
|
994
|
+
return { version: 1, scope, agents: [], bundles: {}, skills: {} };
|
|
995
|
+
}
|
|
996
|
+
function manifestPathFor(scope, cwd) {
|
|
997
|
+
return scope === "global" ? join3(configDir(), "installed.json") : join3(resolve2(cwd ?? process.cwd()), PROJECT_MANIFEST);
|
|
998
|
+
}
|
|
999
|
+
function readManifest(path) {
|
|
1000
|
+
let parsed;
|
|
1001
|
+
try {
|
|
1002
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1003
|
+
} catch {
|
|
1004
|
+
return emptyManifest(path.endsWith(PROJECT_MANIFEST) ? "project" : "global");
|
|
1005
|
+
}
|
|
1006
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1007
|
+
return emptyManifest(path.endsWith(PROJECT_MANIFEST) ? "project" : "global");
|
|
1008
|
+
}
|
|
1009
|
+
const raw = parsed;
|
|
1010
|
+
const scope = raw.scope === "project" ? "project" : "global";
|
|
1011
|
+
return {
|
|
1012
|
+
version: 1,
|
|
1013
|
+
scope,
|
|
1014
|
+
agents: Array.isArray(raw.agents) ? raw.agents : [],
|
|
1015
|
+
bundles: raw.bundles && typeof raw.bundles === "object" ? raw.bundles : {},
|
|
1016
|
+
skills: raw.skills && typeof raw.skills === "object" ? raw.skills : {}
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function writeManifest(path, m) {
|
|
1020
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1021
|
+
writeFileSync(path, `${JSON.stringify(m, null, 2)}
|
|
1022
|
+
`);
|
|
1023
|
+
}
|
|
1024
|
+
function recordSkill(m, entry) {
|
|
1025
|
+
return { ...m, skills: { ...m.skills, [entry.name]: entry } };
|
|
1026
|
+
}
|
|
1027
|
+
function recordBundle(m, name, ref) {
|
|
1028
|
+
return { ...m, bundles: { ...m.bundles, [name]: ref } };
|
|
1029
|
+
}
|
|
1030
|
+
function forgetSkill(m, name) {
|
|
1031
|
+
const { [name]: _removed, ...rest } = m.skills;
|
|
1032
|
+
return { ...m, skills: rest };
|
|
1033
|
+
}
|
|
1034
|
+
// src/manifest/nearest.ts
|
|
1035
|
+
async function nearestManifest(flag, cwd) {
|
|
1036
|
+
const { scope, source, manifestPath } = await resolveScope({ flag, cwd });
|
|
1037
|
+
const path = scope === "project" && manifestPath ? manifestPath : manifestPathFor(scope, cwd);
|
|
1038
|
+
return { path, scope, source, manifest: readManifest(path) };
|
|
1039
|
+
}
|
|
1040
|
+
// src/install/install.ts
|
|
1041
|
+
function skillsAddArgv(location, opts = {}) {
|
|
1042
|
+
const path = location.path.replace(/^\/+|\/+$/g, "");
|
|
1043
|
+
const spec = `${location.source}${path ? `/${path}` : ""}#${location.ref}`;
|
|
1044
|
+
const argv = ["add", spec, "--yes"];
|
|
1045
|
+
if (opts.scope !== "project")
|
|
1046
|
+
argv.push("--global");
|
|
1047
|
+
for (const agent of opts.agents ?? [])
|
|
1048
|
+
argv.push("--agent", agent);
|
|
1049
|
+
return argv;
|
|
1050
|
+
}
|
|
1051
|
+
function childEnv(base = process.env) {
|
|
1052
|
+
const env = {};
|
|
1053
|
+
for (const [k, v] of Object.entries(base))
|
|
1054
|
+
if (v !== undefined)
|
|
1055
|
+
env[k] = v;
|
|
1056
|
+
env.DISABLE_TELEMETRY = "1";
|
|
1057
|
+
env.DO_NOT_TRACK = "1";
|
|
1058
|
+
return env;
|
|
1059
|
+
}
|
|
1060
|
+
var nodeRequire = createRequire(import.meta.url);
|
|
1061
|
+
function skillsBin() {
|
|
1062
|
+
let pkgPath;
|
|
1063
|
+
try {
|
|
1064
|
+
pkgPath = nodeRequire.resolve("skills/package.json");
|
|
1065
|
+
} catch {
|
|
1066
|
+
throw new Error("The bundled `skills` CLI could not be resolved. Re-install yourskills; its `skills` dependency is missing.");
|
|
1067
|
+
}
|
|
1068
|
+
const pkg = nodeRequire(pkgPath);
|
|
1069
|
+
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.skills;
|
|
1070
|
+
if (!bin) {
|
|
1071
|
+
throw new Error("The bundled `skills` package declares no `skills` binary. Re-install yourskills.");
|
|
1072
|
+
}
|
|
1073
|
+
return join4(dirname3(pkgPath), bin);
|
|
1074
|
+
}
|
|
1075
|
+
function runSkills(argv) {
|
|
1076
|
+
const bin = skillsBin();
|
|
1077
|
+
return new Promise((settle) => {
|
|
1078
|
+
const child = spawn(process.execPath, [bin, ...argv], {
|
|
1079
|
+
env: childEnv(),
|
|
1080
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1081
|
+
});
|
|
1082
|
+
let stdout = "";
|
|
1083
|
+
let stderr = "";
|
|
1084
|
+
child.stdout.setEncoding("utf8");
|
|
1085
|
+
child.stderr.setEncoding("utf8");
|
|
1086
|
+
child.stdout.on("data", (chunk) => {
|
|
1087
|
+
stdout += chunk;
|
|
1088
|
+
});
|
|
1089
|
+
child.stderr.on("data", (chunk) => {
|
|
1090
|
+
stderr += chunk;
|
|
1091
|
+
});
|
|
1092
|
+
const fail = (message) => settle({ ok: false, message, detail: `${stdout}${stderr}`.trim() });
|
|
1093
|
+
child.on("error", (error) => fail(`skills ${argv.join(" ")} could not be started: ${error.message}`));
|
|
1094
|
+
child.on("close", (code, signal) => {
|
|
1095
|
+
if (code === 0) {
|
|
1096
|
+
settle({ ok: true });
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
fail(`skills ${argv.join(" ")} exited ${code ?? `on ${signal}`}`);
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
function pendingMessage(pending) {
|
|
1104
|
+
const where = `${pending.repo}/${pending.path}`;
|
|
1105
|
+
return pending.prUrl ? `${pending.skillName} not in your registry yet — opened a pull request against ${where}
|
|
1106
|
+
` + ` ${pending.prUrl}
|
|
1107
|
+
` + " Merge it, then run this command again." : `${pending.skillName} not in your registry yet — written to ${where}. ` + "Re-run once your registry has indexed it.";
|
|
1108
|
+
}
|
|
1109
|
+
function manifestEntryFor(location, opts = {}, bundle) {
|
|
1110
|
+
const path = location.path.replace(/^\/+|\/+$/g, "");
|
|
1111
|
+
return {
|
|
1112
|
+
name: location.skillName,
|
|
1113
|
+
externalId: location.externalId,
|
|
1114
|
+
ref: location.ref,
|
|
1115
|
+
source: location.source,
|
|
1116
|
+
path,
|
|
1117
|
+
...bundle ? { bundle } : {},
|
|
1118
|
+
agents: opts.agents ?? [],
|
|
1119
|
+
installedAt: new Date().toISOString()
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function record(opts, location, bundle) {
|
|
1123
|
+
const target = opts.manifest;
|
|
1124
|
+
if (!target)
|
|
1125
|
+
return;
|
|
1126
|
+
let manifest = readManifest(target.path);
|
|
1127
|
+
manifest = {
|
|
1128
|
+
...manifest,
|
|
1129
|
+
scope: target.scope,
|
|
1130
|
+
agents: opts.agents?.length ? opts.agents : manifest.agents
|
|
1131
|
+
};
|
|
1132
|
+
if (bundle && !manifest.bundles[bundle]) {
|
|
1133
|
+
manifest = recordBundle(manifest, bundle, location.ref);
|
|
1134
|
+
}
|
|
1135
|
+
manifest = recordSkill(manifest, manifestEntryFor(location, opts, bundle));
|
|
1136
|
+
writeManifest(target.path, manifest);
|
|
1137
|
+
}
|
|
1138
|
+
async function* installTargets(targets, opts, bundle) {
|
|
1139
|
+
for (const location of targets) {
|
|
1140
|
+
if (location.deprecated)
|
|
1141
|
+
yield { kind: "deprecated", location };
|
|
1142
|
+
yield { kind: "installing", location };
|
|
1143
|
+
const result = await runSkills(skillsAddArgv(location, opts));
|
|
1144
|
+
if (result.ok)
|
|
1145
|
+
record(opts, location, bundle);
|
|
1146
|
+
yield result.ok ? { kind: "installed", location } : {
|
|
1147
|
+
kind: "failed",
|
|
1148
|
+
name: location.skillName,
|
|
1149
|
+
message: result.message,
|
|
1150
|
+
detail: result.detail
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
async function* install(client, names, opts = {}) {
|
|
1155
|
+
for (const name of names)
|
|
1156
|
+
yield { kind: "resolving", name };
|
|
1157
|
+
let resolution;
|
|
1158
|
+
try {
|
|
1159
|
+
resolution = await client.resolve.skill.mutate({ names });
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1162
|
+
for (const name of names)
|
|
1163
|
+
yield { kind: "failed", name, message };
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
yield* installResolution(resolution.entries, opts);
|
|
1167
|
+
}
|
|
1168
|
+
async function* installBundle(client, name, opts = {}) {
|
|
1169
|
+
yield { kind: "resolving", name };
|
|
1170
|
+
let resolution;
|
|
1171
|
+
try {
|
|
1172
|
+
resolution = await client.resolve.bundle.mutate({ name });
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
yield {
|
|
1175
|
+
kind: "failed",
|
|
1176
|
+
name,
|
|
1177
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1178
|
+
};
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
yield* installResolution(resolution.entries, opts, name);
|
|
1182
|
+
}
|
|
1183
|
+
async function* installResolution(entries, opts, bundle) {
|
|
1184
|
+
for (const entry of entries) {
|
|
1185
|
+
if (entry.state === "failed") {
|
|
1186
|
+
yield { kind: "failed", name: entry.name, message: entry.message };
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
if (entry.state === "vendoring") {
|
|
1190
|
+
yield { kind: "pending", vendor: entry.vendor };
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
yield* installTargets(entry.locations, opts, bundle);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
// src/install/reinstall.ts
|
|
1197
|
+
function targetOf(entry) {
|
|
1198
|
+
return {
|
|
1199
|
+
skillName: entry.name,
|
|
1200
|
+
externalId: entry.externalId,
|
|
1201
|
+
source: entry.source,
|
|
1202
|
+
path: entry.path,
|
|
1203
|
+
ref: entry.ref,
|
|
1204
|
+
deprecated: false
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
async function* reinstall(manifest, opts = {}) {
|
|
1208
|
+
const entries = Object.values(manifest.skills);
|
|
1209
|
+
for (const entry of entries) {
|
|
1210
|
+
yield* installTargets([targetOf(entry)], opts, entry.bundle);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
// src/install/remove.ts
|
|
1214
|
+
function skillsRemoveArgv(name, opts = {}) {
|
|
1215
|
+
const argv = ["remove", name, "--yes"];
|
|
1216
|
+
if (opts.scope !== "project")
|
|
1217
|
+
argv.push("--global");
|
|
1218
|
+
for (const agent of opts.agents ?? [])
|
|
1219
|
+
argv.push("--agent", agent);
|
|
1220
|
+
return argv;
|
|
1221
|
+
}
|
|
1222
|
+
async function removeSkills(names, manifest, manifestPath, opts = {}) {
|
|
1223
|
+
const lines = [];
|
|
1224
|
+
const removed = [];
|
|
1225
|
+
let next = manifest;
|
|
1226
|
+
let failed = false;
|
|
1227
|
+
for (const name of names) {
|
|
1228
|
+
const entry = next.skills[name];
|
|
1229
|
+
if (!entry) {
|
|
1230
|
+
failed = true;
|
|
1231
|
+
lines.push({
|
|
1232
|
+
mark: "fail",
|
|
1233
|
+
text: name,
|
|
1234
|
+
note: "not in the manifest — nothing to remove"
|
|
1235
|
+
});
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
const result = await runSkills(skillsRemoveArgv(name, opts));
|
|
1239
|
+
if (!result.ok) {
|
|
1240
|
+
failed = true;
|
|
1241
|
+
lines.push({ mark: "fail", text: name, note: result.message });
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
next = forgetSkill(next, name);
|
|
1245
|
+
removed.push(name);
|
|
1246
|
+
lines.push({ mark: "ok", text: name, note: "removed" });
|
|
1247
|
+
}
|
|
1248
|
+
if (removed.length > 0)
|
|
1249
|
+
writeManifest(manifestPath, next);
|
|
1250
|
+
return {
|
|
1251
|
+
title: "remove",
|
|
1252
|
+
lines,
|
|
1253
|
+
json: { manifest: manifestPath, removed, failed },
|
|
1254
|
+
failed
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
// src/install/screens/installer.tsx
|
|
1258
|
+
import { Box as Box6, Text as Text8, useApp as useApp3 } from "ink";
|
|
1259
|
+
import { useEffect as useEffect4, useState as useState4 } from "react";
|
|
1260
|
+
import { jsx as jsx8, jsxs as jsxs7, Fragment as Fragment3 } from "react/jsx-runtime";
|
|
1261
|
+
function where(location) {
|
|
1262
|
+
return `${location.source}/${location.path}@${location.ref.slice(0, 8)}`;
|
|
1263
|
+
}
|
|
1264
|
+
function reduce(lines, event) {
|
|
1265
|
+
const replace = (name, next) => {
|
|
1266
|
+
const at = lines.findIndex((l) => l.name === name);
|
|
1267
|
+
if (at === -1)
|
|
1268
|
+
return [...lines, next];
|
|
1269
|
+
return lines.map((l, i) => i === at ? next : l);
|
|
1270
|
+
};
|
|
1271
|
+
switch (event.kind) {
|
|
1272
|
+
case "resolving":
|
|
1273
|
+
return replace(event.name, { state: "resolving", name: event.name });
|
|
1274
|
+
case "deprecated":
|
|
1275
|
+
return lines;
|
|
1276
|
+
case "installing":
|
|
1277
|
+
return replace(event.location.skillName, {
|
|
1278
|
+
state: "installing",
|
|
1279
|
+
name: event.location.skillName,
|
|
1280
|
+
where: where(event.location)
|
|
1281
|
+
});
|
|
1282
|
+
case "installed":
|
|
1283
|
+
return replace(event.location.skillName, {
|
|
1284
|
+
state: "installed",
|
|
1285
|
+
name: event.location.skillName,
|
|
1286
|
+
where: where(event.location),
|
|
1287
|
+
deprecated: event.location.deprecated
|
|
1288
|
+
});
|
|
1289
|
+
case "pending":
|
|
1290
|
+
return replace(event.vendor.skillName, {
|
|
1291
|
+
state: "pending",
|
|
1292
|
+
name: event.vendor.skillName,
|
|
1293
|
+
vendor: event.vendor
|
|
1294
|
+
});
|
|
1295
|
+
case "failed":
|
|
1296
|
+
return replace(event.name, {
|
|
1297
|
+
state: "failed",
|
|
1298
|
+
name: event.name,
|
|
1299
|
+
message: event.message,
|
|
1300
|
+
detail: event.detail
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
function InstallLine({ line, width }) {
|
|
1305
|
+
const names = 24;
|
|
1306
|
+
const name = clip(line.name, names).padEnd(names);
|
|
1307
|
+
switch (line.state) {
|
|
1308
|
+
case "resolving":
|
|
1309
|
+
return /* @__PURE__ */ jsxs7(Text8, {
|
|
1310
|
+
children: [
|
|
1311
|
+
/* @__PURE__ */ jsx8(Spinner, {}),
|
|
1312
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1313
|
+
children: ` ${name}`
|
|
1314
|
+
}),
|
|
1315
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1316
|
+
dimColor: true,
|
|
1317
|
+
children: "resolving..."
|
|
1318
|
+
})
|
|
1319
|
+
]
|
|
1320
|
+
});
|
|
1321
|
+
case "installing":
|
|
1322
|
+
return /* @__PURE__ */ jsxs7(Text8, {
|
|
1323
|
+
children: [
|
|
1324
|
+
/* @__PURE__ */ jsx8(Spinner, {}),
|
|
1325
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1326
|
+
children: ` ${name}`
|
|
1327
|
+
}),
|
|
1328
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1329
|
+
dimColor: true,
|
|
1330
|
+
children: clip(line.where, width - names - 3)
|
|
1331
|
+
})
|
|
1332
|
+
]
|
|
1333
|
+
});
|
|
1334
|
+
case "installed":
|
|
1335
|
+
return /* @__PURE__ */ jsxs7(Text8, {
|
|
1336
|
+
children: [
|
|
1337
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1338
|
+
color: "green",
|
|
1339
|
+
children: mark.ok
|
|
1340
|
+
}),
|
|
1341
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1342
|
+
children: ` ${name}`
|
|
1343
|
+
}),
|
|
1344
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1345
|
+
dimColor: true,
|
|
1346
|
+
children: clip(line.where, width - names - 3)
|
|
1347
|
+
}),
|
|
1348
|
+
line.deprecated ? /* @__PURE__ */ jsx8(Text8, {
|
|
1349
|
+
color: lifecycleColor("deprecated"),
|
|
1350
|
+
children: ` ${lifecycleTag("deprecated")}`
|
|
1351
|
+
}) : null
|
|
1352
|
+
]
|
|
1353
|
+
});
|
|
1354
|
+
case "pending":
|
|
1355
|
+
return /* @__PURE__ */ jsxs7(Box6, {
|
|
1356
|
+
flexDirection: "column",
|
|
1357
|
+
children: [
|
|
1358
|
+
/* @__PURE__ */ jsxs7(Text8, {
|
|
1359
|
+
children: [
|
|
1360
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1361
|
+
color: tone.notice,
|
|
1362
|
+
children: mark.pending
|
|
1363
|
+
}),
|
|
1364
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1365
|
+
children: ` ${name}`
|
|
1366
|
+
}),
|
|
1367
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1368
|
+
color: tone.notice,
|
|
1369
|
+
children: "not in your registry yet"
|
|
1370
|
+
})
|
|
1371
|
+
]
|
|
1372
|
+
}),
|
|
1373
|
+
line.vendor.prUrl ? /* @__PURE__ */ jsxs7(Text8, {
|
|
1374
|
+
children: [
|
|
1375
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1376
|
+
children: " "
|
|
1377
|
+
}),
|
|
1378
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1379
|
+
color: tone.primary,
|
|
1380
|
+
children: line.vendor.prUrl
|
|
1381
|
+
})
|
|
1382
|
+
]
|
|
1383
|
+
}) : null
|
|
1384
|
+
]
|
|
1385
|
+
});
|
|
1386
|
+
case "failed":
|
|
1387
|
+
return /* @__PURE__ */ jsxs7(Box6, {
|
|
1388
|
+
flexDirection: "column",
|
|
1389
|
+
children: [
|
|
1390
|
+
/* @__PURE__ */ jsxs7(Text8, {
|
|
1391
|
+
children: [
|
|
1392
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1393
|
+
color: tone.danger,
|
|
1394
|
+
children: mark.fail
|
|
1395
|
+
}),
|
|
1396
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1397
|
+
children: ` ${name}`
|
|
1398
|
+
}),
|
|
1399
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1400
|
+
color: tone.danger,
|
|
1401
|
+
children: clip(line.message, width - names - 3)
|
|
1402
|
+
})
|
|
1403
|
+
]
|
|
1404
|
+
}),
|
|
1405
|
+
line.detail ? line.detail.split(`
|
|
1406
|
+
`).slice(-3).map((d) => /* @__PURE__ */ jsx8(Text8, {
|
|
1407
|
+
dimColor: true,
|
|
1408
|
+
children: clip(` ${d}`, width)
|
|
1409
|
+
}, d)) : null
|
|
1410
|
+
]
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function Installer({
|
|
1415
|
+
title,
|
|
1416
|
+
stream,
|
|
1417
|
+
onDone,
|
|
1418
|
+
exitWhenDone = true
|
|
1419
|
+
}) {
|
|
1420
|
+
const { exit } = useApp3();
|
|
1421
|
+
const width = useFrameWidth();
|
|
1422
|
+
const [lines, setLines] = useState4([]);
|
|
1423
|
+
const [error, setError] = useState4(null);
|
|
1424
|
+
const [done, setDone] = useState4(false);
|
|
1425
|
+
useEffect4(() => {
|
|
1426
|
+
let live = true;
|
|
1427
|
+
(async () => {
|
|
1428
|
+
let current = [];
|
|
1429
|
+
try {
|
|
1430
|
+
for await (const event of stream()) {
|
|
1431
|
+
if (!live)
|
|
1432
|
+
return;
|
|
1433
|
+
current = reduce(current, event);
|
|
1434
|
+
setLines(current);
|
|
1435
|
+
}
|
|
1436
|
+
} catch (e) {
|
|
1437
|
+
if (live)
|
|
1438
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
1439
|
+
}
|
|
1440
|
+
if (!live)
|
|
1441
|
+
return;
|
|
1442
|
+
setDone(true);
|
|
1443
|
+
onDone?.({
|
|
1444
|
+
pending: current.filter((l) => l.state === "pending").map((l) => l.vendor),
|
|
1445
|
+
failed: current.filter((l) => l.state === "failed").length
|
|
1446
|
+
});
|
|
1447
|
+
if (exitWhenDone)
|
|
1448
|
+
exit();
|
|
1449
|
+
})();
|
|
1450
|
+
return () => {
|
|
1451
|
+
live = false;
|
|
1452
|
+
};
|
|
1453
|
+
}, [stream, onDone, exit, exitWhenDone]);
|
|
1454
|
+
const pending = lines.filter((l) => l.state === "pending");
|
|
1455
|
+
return /* @__PURE__ */ jsx8(Frame, {
|
|
1456
|
+
title,
|
|
1457
|
+
width,
|
|
1458
|
+
children: /* @__PURE__ */ jsxs7(Box6, {
|
|
1459
|
+
flexDirection: "column",
|
|
1460
|
+
children: [
|
|
1461
|
+
lines.length === 0 && !error ? /* @__PURE__ */ jsxs7(Text8, {
|
|
1462
|
+
children: [
|
|
1463
|
+
/* @__PURE__ */ jsx8(Spinner, {}),
|
|
1464
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1465
|
+
dimColor: true,
|
|
1466
|
+
children: " Resolving..."
|
|
1467
|
+
})
|
|
1468
|
+
]
|
|
1469
|
+
}) : null,
|
|
1470
|
+
lines.map((line) => /* @__PURE__ */ jsx8(InstallLine, {
|
|
1471
|
+
line,
|
|
1472
|
+
width: width - 2
|
|
1473
|
+
}, line.name)),
|
|
1474
|
+
error ? /* @__PURE__ */ jsx8(Text8, {
|
|
1475
|
+
color: tone.danger,
|
|
1476
|
+
children: `${mark.fail} ${error}`
|
|
1477
|
+
}) : null,
|
|
1478
|
+
done && pending.length > 0 ? /* @__PURE__ */ jsxs7(Fragment3, {
|
|
1479
|
+
children: [
|
|
1480
|
+
/* @__PURE__ */ jsx8(Divider, {
|
|
1481
|
+
width
|
|
1482
|
+
}),
|
|
1483
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1484
|
+
color: tone.notice,
|
|
1485
|
+
children: pending.length === 1 ? "1 skill is waiting on a pull request." : `${pending.length} skills are waiting on a pull request.`
|
|
1486
|
+
}),
|
|
1487
|
+
/* @__PURE__ */ jsx8(Text8, {
|
|
1488
|
+
dimColor: true,
|
|
1489
|
+
children: "Merge it, then run this command again."
|
|
1490
|
+
})
|
|
1491
|
+
]
|
|
1492
|
+
}) : null
|
|
1493
|
+
]
|
|
1494
|
+
})
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
async function installPlain(stream) {
|
|
1498
|
+
const pending = [];
|
|
1499
|
+
let failed = 0;
|
|
1500
|
+
for await (const event of stream) {
|
|
1501
|
+
switch (event.kind) {
|
|
1502
|
+
case "resolving":
|
|
1503
|
+
break;
|
|
1504
|
+
case "deprecated":
|
|
1505
|
+
console.warn(`warning: "${event.location.skillName}" is deprecated — installing anyway.`);
|
|
1506
|
+
break;
|
|
1507
|
+
case "installing":
|
|
1508
|
+
console.log(`${event.location.skillName} ${where(event.location)}`);
|
|
1509
|
+
break;
|
|
1510
|
+
case "installed":
|
|
1511
|
+
break;
|
|
1512
|
+
case "pending":
|
|
1513
|
+
console.log(pendingMessage(event.vendor));
|
|
1514
|
+
pending.push(event.vendor);
|
|
1515
|
+
break;
|
|
1516
|
+
case "failed":
|
|
1517
|
+
failed += 1;
|
|
1518
|
+
console.error(`error: ${event.name}: ${event.message}`);
|
|
1519
|
+
if (event.detail)
|
|
1520
|
+
console.error(event.detail);
|
|
1521
|
+
break;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
return { pending, failed };
|
|
1525
|
+
}
|
|
1526
|
+
// src/install/update.ts
|
|
1527
|
+
function update(client, bundle, opts = {}) {
|
|
1528
|
+
return installBundle(client, bundle, opts);
|
|
1529
|
+
}
|
|
1530
|
+
function looseSkills(manifest) {
|
|
1531
|
+
return Object.values(manifest.skills).filter((entry) => !entry.bundle).map((entry) => entry.name);
|
|
1532
|
+
}
|
|
1533
|
+
async function* updateTracked(client, manifest, opts = {}, all = false) {
|
|
1534
|
+
for (const bundle of Object.keys(manifest.bundles)) {
|
|
1535
|
+
yield* installBundle(client, bundle, opts);
|
|
1536
|
+
}
|
|
1537
|
+
const loose = all ? looseSkills(manifest) : [];
|
|
1538
|
+
if (loose.length > 0)
|
|
1539
|
+
yield* install(client, loose, opts);
|
|
1540
|
+
}
|
|
1541
|
+
// src/catalog/screens/browser.tsx
|
|
1542
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1543
|
+
var TABS = ["bundles", "skills"];
|
|
1544
|
+
function pageSize(rows) {
|
|
1545
|
+
return Math.max(3, Math.min(20, rows - 11));
|
|
1546
|
+
}
|
|
1547
|
+
function matches(row, query) {
|
|
1548
|
+
if (query.length === 0)
|
|
1549
|
+
return true;
|
|
1550
|
+
const q = query.toLowerCase();
|
|
1551
|
+
return row.name.toLowerCase().includes(q) || (row.description ?? "").toLowerCase().includes(q);
|
|
1552
|
+
}
|
|
1553
|
+
function TabBar({ active }) {
|
|
1554
|
+
return /* @__PURE__ */ jsx9(Box7, {
|
|
1555
|
+
children: TABS.map((tab, i) => /* @__PURE__ */ jsxs8(Text9, {
|
|
1556
|
+
children: [
|
|
1557
|
+
i > 0 ? /* @__PURE__ */ jsx9(Text9, {
|
|
1558
|
+
dimColor: true,
|
|
1559
|
+
children: " "
|
|
1560
|
+
}) : null,
|
|
1561
|
+
tab === active ? /* @__PURE__ */ jsx9(Text9, {
|
|
1562
|
+
bold: true,
|
|
1563
|
+
color: tone.primary,
|
|
1564
|
+
children: `[${tab}]`
|
|
1565
|
+
}) : /* @__PURE__ */ jsx9(Text9, {
|
|
1566
|
+
dimColor: true,
|
|
1567
|
+
children: ` ${tab} `
|
|
1568
|
+
})
|
|
1569
|
+
]
|
|
1570
|
+
}, tab))
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
function BrowserScreen({
|
|
1574
|
+
client,
|
|
1575
|
+
opts,
|
|
1576
|
+
onError,
|
|
1577
|
+
onSummary,
|
|
1578
|
+
onExit
|
|
1579
|
+
}) {
|
|
1580
|
+
const { exit } = useApp4();
|
|
1581
|
+
const width = useFrameWidth();
|
|
1582
|
+
const { rows: terminalRows } = useWindowSize2();
|
|
1583
|
+
const [catalog, setCatalog] = useState5(null);
|
|
1584
|
+
const [tab, setTab] = useState5("bundles");
|
|
1585
|
+
const [query, setQuery] = useState5("");
|
|
1586
|
+
const [cursor, setCursor] = useState5(0);
|
|
1587
|
+
const [picked, setPicked] = useState5({
|
|
1588
|
+
bundles: new Set,
|
|
1589
|
+
skills: new Set
|
|
1590
|
+
});
|
|
1591
|
+
const [running, setRunning] = useState5(null);
|
|
1592
|
+
useEffect5(() => {
|
|
1593
|
+
let live = true;
|
|
1594
|
+
Promise.all([client.skills.list.query({}), client.bundles.list.query()]).then(([skills, bundles]) => {
|
|
1595
|
+
if (!live)
|
|
1596
|
+
return;
|
|
1597
|
+
setCatalog({
|
|
1598
|
+
skills: skills.map((s) => ({
|
|
1599
|
+
name: s.name,
|
|
1600
|
+
description: s.description,
|
|
1601
|
+
status: s.status
|
|
1602
|
+
})),
|
|
1603
|
+
bundles: bundles.map((b) => ({
|
|
1604
|
+
name: b.name,
|
|
1605
|
+
description: b.description
|
|
1606
|
+
}))
|
|
1607
|
+
});
|
|
1608
|
+
}).catch((error) => {
|
|
1609
|
+
if (live)
|
|
1610
|
+
onError(error);
|
|
1611
|
+
});
|
|
1612
|
+
return () => {
|
|
1613
|
+
live = false;
|
|
1614
|
+
};
|
|
1615
|
+
}, [client, onError]);
|
|
1616
|
+
const visible = useMemo2(() => {
|
|
1617
|
+
if (!catalog)
|
|
1618
|
+
return [];
|
|
1619
|
+
return catalog[tab].filter((row) => matches(row, query));
|
|
1620
|
+
}, [catalog, tab, query]);
|
|
1621
|
+
useEffect5(() => {
|
|
1622
|
+
setCursor((c) => Math.max(0, Math.min(c, visible.length - 1)));
|
|
1623
|
+
}, [visible.length]);
|
|
1624
|
+
const selected = picked[tab];
|
|
1625
|
+
const start = useCallback2(() => {
|
|
1626
|
+
const names2 = selected.size > 0 ? [...selected] : visible[cursor] ? [visible[cursor].name] : [];
|
|
1627
|
+
if (names2.length === 0)
|
|
1628
|
+
return;
|
|
1629
|
+
setRunning({
|
|
1630
|
+
title: tab === "bundles" ? `bundle: ${names2[0]}` : "add",
|
|
1631
|
+
names: names2
|
|
1632
|
+
});
|
|
1633
|
+
}, [selected, visible, cursor, tab]);
|
|
1634
|
+
useInput2((input, key) => {
|
|
1635
|
+
if (key.upArrow) {
|
|
1636
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
1637
|
+
} else if (key.downArrow) {
|
|
1638
|
+
setCursor((c) => Math.min(visible.length - 1, c + 1));
|
|
1639
|
+
} else if (key.pageUp) {
|
|
1640
|
+
setCursor((c) => Math.max(0, c - pageSize(terminalRows)));
|
|
1641
|
+
} else if (key.pageDown) {
|
|
1642
|
+
setCursor((c) => Math.min(visible.length - 1, c + pageSize(terminalRows)));
|
|
1643
|
+
} else if (key.tab) {
|
|
1644
|
+
setTab((t) => t === "bundles" ? "skills" : "bundles");
|
|
1645
|
+
setCursor(0);
|
|
1646
|
+
} else if (key.return) {
|
|
1647
|
+
start();
|
|
1648
|
+
} else if (key.escape) {
|
|
1649
|
+
if (query.length > 0)
|
|
1650
|
+
setQuery("");
|
|
1651
|
+
else
|
|
1652
|
+
(onExit ?? exit)();
|
|
1653
|
+
} else if (key.backspace || key.delete) {
|
|
1654
|
+
setQuery((q) => q.slice(0, -1));
|
|
1655
|
+
setCursor(0);
|
|
1656
|
+
} else if (input === " ") {
|
|
1657
|
+
const row = visible[cursor];
|
|
1658
|
+
if (!row)
|
|
1659
|
+
return;
|
|
1660
|
+
setPicked((current) => {
|
|
1661
|
+
const next = new Set(current[tab]);
|
|
1662
|
+
if (tab === "bundles")
|
|
1663
|
+
next.clear();
|
|
1664
|
+
if (current[tab].has(row.name))
|
|
1665
|
+
next.delete(row.name);
|
|
1666
|
+
else
|
|
1667
|
+
next.add(row.name);
|
|
1668
|
+
return { ...current, [tab]: next };
|
|
1669
|
+
});
|
|
1670
|
+
} else if (input && !key.ctrl && !key.meta) {
|
|
1671
|
+
setQuery((q) => q + input);
|
|
1672
|
+
setCursor(0);
|
|
1673
|
+
}
|
|
1674
|
+
}, { isActive: running === null });
|
|
1675
|
+
if (running) {
|
|
1676
|
+
const names2 = running.names;
|
|
1677
|
+
const stream = tab === "bundles" ? () => installBundle(client, names2[0], opts) : () => install(client, names2, opts);
|
|
1678
|
+
return /* @__PURE__ */ jsx9(Installer, {
|
|
1679
|
+
title: running.title,
|
|
1680
|
+
stream,
|
|
1681
|
+
exitWhenDone: true,
|
|
1682
|
+
onDone: onSummary
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
if (!catalog) {
|
|
1686
|
+
return /* @__PURE__ */ jsx9(Frame, {
|
|
1687
|
+
title: "yourskills",
|
|
1688
|
+
width,
|
|
1689
|
+
children: /* @__PURE__ */ jsxs8(Text9, {
|
|
1690
|
+
children: [
|
|
1691
|
+
/* @__PURE__ */ jsx9(Spinner, {}),
|
|
1692
|
+
/* @__PURE__ */ jsx9(Text9, {
|
|
1693
|
+
dimColor: true,
|
|
1694
|
+
children: " Loading your registry..."
|
|
1695
|
+
})
|
|
1696
|
+
]
|
|
1697
|
+
})
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
const size = pageSize(terminalRows);
|
|
1701
|
+
const first = Math.max(0, Math.min(cursor - Math.floor(size / 2), visible.length - size));
|
|
1702
|
+
const page = visible.slice(Math.max(0, first), Math.max(0, first) + size);
|
|
1703
|
+
const names = nameWidth(visible.length > 0 ? visible : catalog[tab]);
|
|
1704
|
+
return /* @__PURE__ */ jsx9(Frame, {
|
|
1705
|
+
title: "yourskills",
|
|
1706
|
+
width,
|
|
1707
|
+
children: /* @__PURE__ */ jsxs8(Box7, {
|
|
1708
|
+
flexDirection: "column",
|
|
1709
|
+
children: [
|
|
1710
|
+
/* @__PURE__ */ jsx9(TabBar, {
|
|
1711
|
+
active: tab
|
|
1712
|
+
}),
|
|
1713
|
+
/* @__PURE__ */ jsx9(Divider, {
|
|
1714
|
+
width
|
|
1715
|
+
}),
|
|
1716
|
+
/* @__PURE__ */ jsxs8(Box7, {
|
|
1717
|
+
children: [
|
|
1718
|
+
/* @__PURE__ */ jsx9(Text9, {
|
|
1719
|
+
color: tone.primary,
|
|
1720
|
+
children: "search> "
|
|
1721
|
+
}),
|
|
1722
|
+
/* @__PURE__ */ jsx9(SearchInput, {
|
|
1723
|
+
value: query,
|
|
1724
|
+
placeholder: "type to filter"
|
|
1725
|
+
})
|
|
1726
|
+
]
|
|
1727
|
+
}),
|
|
1728
|
+
/* @__PURE__ */ jsx9(Divider, {
|
|
1729
|
+
width
|
|
1730
|
+
}),
|
|
1731
|
+
page.length === 0 ? /* @__PURE__ */ jsx9(Text9, {
|
|
1732
|
+
dimColor: true,
|
|
1733
|
+
children: `No ${tab} match "${query}".`
|
|
1734
|
+
}) : page.map((row, i) => /* @__PURE__ */ jsx9(SkillRow, {
|
|
1735
|
+
row,
|
|
1736
|
+
width: width - 2,
|
|
1737
|
+
names,
|
|
1738
|
+
selectable: true,
|
|
1739
|
+
cursor: Math.max(0, first) + i === cursor,
|
|
1740
|
+
selected: selected.has(row.name)
|
|
1741
|
+
}, row.name)),
|
|
1742
|
+
/* @__PURE__ */ jsx9(Divider, {
|
|
1743
|
+
width
|
|
1744
|
+
}),
|
|
1745
|
+
/* @__PURE__ */ jsxs8(Box7, {
|
|
1746
|
+
children: [
|
|
1747
|
+
/* @__PURE__ */ jsx9(Text9, {
|
|
1748
|
+
dimColor: true,
|
|
1749
|
+
children: `${visible.length > 0 ? cursor + 1 : 0}/${visible.length}`
|
|
1750
|
+
}),
|
|
1751
|
+
selected.size > 0 ? /* @__PURE__ */ jsx9(Text9, {
|
|
1752
|
+
color: tone.notice,
|
|
1753
|
+
children: ` ${selected.size} selected`
|
|
1754
|
+
}) : null,
|
|
1755
|
+
/* @__PURE__ */ jsx9(Text9, {
|
|
1756
|
+
children: " "
|
|
1757
|
+
}),
|
|
1758
|
+
/* @__PURE__ */ jsx9(Hints, {
|
|
1759
|
+
hints: [
|
|
1760
|
+
["^v", "move"],
|
|
1761
|
+
["space", "select"],
|
|
1762
|
+
["enter", "install"],
|
|
1763
|
+
["tab", tab === "bundles" ? "skills" : "bundles"],
|
|
1764
|
+
["esc", query ? "clear" : onExit ? "back" : "quit"]
|
|
1765
|
+
]
|
|
1766
|
+
})
|
|
1767
|
+
]
|
|
1768
|
+
})
|
|
1769
|
+
]
|
|
1770
|
+
})
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
// src/catalog/screens/catalog.tsx
|
|
1774
|
+
import { Box as Box8, Text as Text10, useApp as useApp5 } from "ink";
|
|
1775
|
+
import { useEffect as useEffect6, useState as useState6 } from "react";
|
|
1776
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1777
|
+
function CatalogScreen({
|
|
1778
|
+
title,
|
|
1779
|
+
load,
|
|
1780
|
+
onError
|
|
1781
|
+
}) {
|
|
1782
|
+
const { exit } = useApp5();
|
|
1783
|
+
const width = useFrameWidth();
|
|
1784
|
+
const [rows, setRows] = useState6(null);
|
|
1785
|
+
useEffect6(() => {
|
|
1786
|
+
let live = true;
|
|
1787
|
+
load().then((result) => {
|
|
1788
|
+
if (!live)
|
|
1789
|
+
return;
|
|
1790
|
+
setRows(result);
|
|
1791
|
+
exit();
|
|
1792
|
+
}).catch((error) => {
|
|
1793
|
+
if (live)
|
|
1794
|
+
onError(error);
|
|
1795
|
+
});
|
|
1796
|
+
return () => {
|
|
1797
|
+
live = false;
|
|
1798
|
+
};
|
|
1799
|
+
}, [load, exit, onError]);
|
|
1800
|
+
if (rows === null) {
|
|
1801
|
+
return /* @__PURE__ */ jsx10(Frame, {
|
|
1802
|
+
title,
|
|
1803
|
+
width,
|
|
1804
|
+
children: /* @__PURE__ */ jsxs9(Text10, {
|
|
1805
|
+
children: [
|
|
1806
|
+
/* @__PURE__ */ jsx10(Spinner, {}),
|
|
1807
|
+
/* @__PURE__ */ jsx10(Text10, {
|
|
1808
|
+
dimColor: true,
|
|
1809
|
+
children: " Loading..."
|
|
1810
|
+
})
|
|
1811
|
+
]
|
|
1812
|
+
})
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
if (rows.length === 0) {
|
|
1816
|
+
return /* @__PURE__ */ jsx10(Frame, {
|
|
1817
|
+
title,
|
|
1818
|
+
width,
|
|
1819
|
+
children: /* @__PURE__ */ jsx10(Text10, {
|
|
1820
|
+
dimColor: true,
|
|
1821
|
+
children: "No skills found."
|
|
1822
|
+
})
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
const names = nameWidth(rows);
|
|
1826
|
+
return /* @__PURE__ */ jsx10(Frame, {
|
|
1827
|
+
title,
|
|
1828
|
+
width,
|
|
1829
|
+
children: /* @__PURE__ */ jsx10(Box8, {
|
|
1830
|
+
flexDirection: "column",
|
|
1831
|
+
children: rows.map((row) => /* @__PURE__ */ jsx10(SkillRow, {
|
|
1832
|
+
row,
|
|
1833
|
+
width: width - 2,
|
|
1834
|
+
names
|
|
1835
|
+
}, row.name))
|
|
1836
|
+
})
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
// src/doctor/doctor.ts
|
|
1840
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1841
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
1842
|
+
import { homedir as homedir2 } from "node:os";
|
|
1843
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
1844
|
+
var REACH_TIMEOUT_MS = 5000;
|
|
1845
|
+
async function reachable(base) {
|
|
1846
|
+
const url = `${base.replace(/\/$/, "")}/api/auth/ok`;
|
|
1847
|
+
try {
|
|
1848
|
+
const res = await fetch(url, {
|
|
1849
|
+
signal: AbortSignal.timeout(REACH_TIMEOUT_MS)
|
|
1850
|
+
});
|
|
1851
|
+
return {
|
|
1852
|
+
name: "server reachable",
|
|
1853
|
+
ok: true,
|
|
1854
|
+
detail: `${base} (${res.status})`
|
|
1855
|
+
};
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
return {
|
|
1858
|
+
name: "server reachable",
|
|
1859
|
+
ok: false,
|
|
1860
|
+
detail: `${base}: ${error instanceof Error ? error.message : String(error)}`
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
async function session(server) {
|
|
1865
|
+
try {
|
|
1866
|
+
const who = await whoami(server);
|
|
1867
|
+
return [
|
|
1868
|
+
{ name: "session valid", ok: true, detail: who.user },
|
|
1869
|
+
{
|
|
1870
|
+
name: "active organization",
|
|
1871
|
+
ok: who.org !== null,
|
|
1872
|
+
detail: who.org ?? "none — pick one with `yourskills org <name>` or in the web UI"
|
|
1873
|
+
}
|
|
1874
|
+
];
|
|
1875
|
+
} catch (error) {
|
|
1876
|
+
return [
|
|
1877
|
+
{
|
|
1878
|
+
name: "session valid",
|
|
1879
|
+
ok: false,
|
|
1880
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
1881
|
+
},
|
|
1882
|
+
{
|
|
1883
|
+
name: "active organization",
|
|
1884
|
+
ok: false,
|
|
1885
|
+
detail: "unknown until you are logged in"
|
|
1886
|
+
}
|
|
1887
|
+
];
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
function skillsResolvable() {
|
|
1891
|
+
try {
|
|
1892
|
+
const bin = skillsBin();
|
|
1893
|
+
return existsSync2(bin) ? { name: "skills dependency", ok: true, detail: bin } : { name: "skills dependency", ok: false, detail: `missing: ${bin}` };
|
|
1894
|
+
} catch (error) {
|
|
1895
|
+
return {
|
|
1896
|
+
name: "skills dependency",
|
|
1897
|
+
ok: false,
|
|
1898
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
function detectedAgents() {
|
|
1903
|
+
let bin;
|
|
1904
|
+
try {
|
|
1905
|
+
bin = skillsBin();
|
|
1906
|
+
} catch {
|
|
1907
|
+
return Promise.resolve([]);
|
|
1908
|
+
}
|
|
1909
|
+
return new Promise((settle) => {
|
|
1910
|
+
const child = spawn2(process.execPath, [bin, "list", "--json", "--global"], {
|
|
1911
|
+
env: childEnv(),
|
|
1912
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1913
|
+
});
|
|
1914
|
+
let out = "";
|
|
1915
|
+
child.stdout.setEncoding("utf8");
|
|
1916
|
+
child.stdout.on("data", (chunk) => {
|
|
1917
|
+
out += chunk;
|
|
1918
|
+
});
|
|
1919
|
+
child.on("error", () => settle([]));
|
|
1920
|
+
child.on("close", () => {
|
|
1921
|
+
try {
|
|
1922
|
+
const found = new Set;
|
|
1923
|
+
for (const row of JSON.parse(out)) {
|
|
1924
|
+
for (const agent of row.agents ?? [])
|
|
1925
|
+
found.add(agent);
|
|
1926
|
+
}
|
|
1927
|
+
settle([...found]);
|
|
1928
|
+
} catch {
|
|
1929
|
+
settle([]);
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
async function agents() {
|
|
1935
|
+
const pinned = await resolveAgents();
|
|
1936
|
+
const found = new Set(pinned);
|
|
1937
|
+
for (const agent of await detectedAgents())
|
|
1938
|
+
found.add(agent);
|
|
1939
|
+
const all = [...found];
|
|
1940
|
+
return {
|
|
1941
|
+
name: "agents",
|
|
1942
|
+
ok: all.length > 0,
|
|
1943
|
+
detail: all.length > 0 ? all.join(", ") : "none — pin one with `yourskills config set agents claude-code`"
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
function manifestParses(nearest) {
|
|
1947
|
+
if (!existsSync2(nearest.path)) {
|
|
1948
|
+
return {
|
|
1949
|
+
name: "manifest",
|
|
1950
|
+
ok: true,
|
|
1951
|
+
detail: `${nearest.path} (not created yet)`
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
try {
|
|
1955
|
+
const raw = JSON.parse(readFileSync2(nearest.path, "utf8"));
|
|
1956
|
+
return raw && typeof raw === "object" ? {
|
|
1957
|
+
name: "manifest",
|
|
1958
|
+
ok: true,
|
|
1959
|
+
detail: `${nearest.path} (${nearest.scope})`
|
|
1960
|
+
} : {
|
|
1961
|
+
name: "manifest",
|
|
1962
|
+
ok: false,
|
|
1963
|
+
detail: `${nearest.path}: not an object`
|
|
1964
|
+
};
|
|
1965
|
+
} catch (error) {
|
|
1966
|
+
return {
|
|
1967
|
+
name: "manifest",
|
|
1968
|
+
ok: false,
|
|
1969
|
+
detail: `${nearest.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
function onDisk(nearest, name) {
|
|
1974
|
+
const root = nearest.scope === "project" ? dirname4(nearest.path) : homedir2();
|
|
1975
|
+
return existsSync2(join5(root, ".agents", "skills", name));
|
|
1976
|
+
}
|
|
1977
|
+
function manifestMatchesDisk(nearest) {
|
|
1978
|
+
const names = Object.keys(nearest.manifest.skills);
|
|
1979
|
+
const missing = names.filter((name) => !onDisk(nearest, name));
|
|
1980
|
+
return {
|
|
1981
|
+
name: "manifest matches disk",
|
|
1982
|
+
ok: missing.length === 0,
|
|
1983
|
+
detail: missing.length === 0 ? `${names.length} skill(s) recorded` : `missing: ${missing.join(", ")} — run \`yourskills install\``
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
async function doctorReport(nearest, server) {
|
|
1987
|
+
const config = await readConfig();
|
|
1988
|
+
const base = serverUrl(config, server);
|
|
1989
|
+
const checks = [
|
|
1990
|
+
await reachable(base),
|
|
1991
|
+
...await session(server),
|
|
1992
|
+
skillsResolvable(),
|
|
1993
|
+
await agents(),
|
|
1994
|
+
manifestParses(nearest),
|
|
1995
|
+
manifestMatchesDisk(nearest)
|
|
1996
|
+
];
|
|
1997
|
+
const width = Math.max(...checks.map((check) => check.name.length));
|
|
1998
|
+
const lines = checks.map((check) => ({
|
|
1999
|
+
mark: check.ok ? "ok" : "fail",
|
|
2000
|
+
text: check.name.padEnd(width),
|
|
2001
|
+
note: check.detail
|
|
2002
|
+
}));
|
|
2003
|
+
return {
|
|
2004
|
+
title: "doctor",
|
|
2005
|
+
lines,
|
|
2006
|
+
json: { manifest: nearest.path, checks },
|
|
2007
|
+
failed: checks.some((check) => !check.ok)
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
// src/hooks/adapters.ts
|
|
2011
|
+
import { homedir as homedir3 } from "node:os";
|
|
2012
|
+
import { join as join6 } from "node:path";
|
|
2013
|
+
var BINARY = "yourskills";
|
|
2014
|
+
function claudeHome() {
|
|
2015
|
+
const override = process.env.CLAUDE_CONFIG_DIR;
|
|
2016
|
+
return override && override.length > 0 ? override : join6(homedir3(), ".claude");
|
|
2017
|
+
}
|
|
2018
|
+
function opencodeHome() {
|
|
2019
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
2020
|
+
return join6(xdg && xdg.length > 0 ? xdg : join6(homedir3(), ".config"), "opencode");
|
|
2021
|
+
}
|
|
2022
|
+
var UPDATE_CHECK_ENTRY = {
|
|
2023
|
+
kind: "update-check",
|
|
2024
|
+
event: "SessionStart",
|
|
2025
|
+
matcher: "startup|resume",
|
|
2026
|
+
command: `${BINARY} hook session-start`
|
|
2027
|
+
};
|
|
2028
|
+
var USAGE_ENTRY = {
|
|
2029
|
+
kind: "usage",
|
|
2030
|
+
event: "PostToolUse",
|
|
2031
|
+
matcher: "Skill",
|
|
2032
|
+
command: `${BINARY} hook post-tool-use`
|
|
2033
|
+
};
|
|
2034
|
+
var HOOK_ADAPTERS = [
|
|
2035
|
+
{
|
|
2036
|
+
agent: "claude-code",
|
|
2037
|
+
supports: ["update-check", "usage"],
|
|
2038
|
+
home: claudeHome,
|
|
2039
|
+
settingsPath: () => join6(claudeHome(), "settings.json"),
|
|
2040
|
+
entries: [UPDATE_CHECK_ENTRY, USAGE_ENTRY]
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
agent: "opencode",
|
|
2044
|
+
supports: ["update-check"],
|
|
2045
|
+
home: opencodeHome,
|
|
2046
|
+
settingsPath: () => null,
|
|
2047
|
+
entries: [],
|
|
2048
|
+
limitation: "opencode hooks are JavaScript plugin modules, not commands in a settings file — yourskills cannot register one yet"
|
|
2049
|
+
}
|
|
2050
|
+
];
|
|
2051
|
+
function adapterFor(agent) {
|
|
2052
|
+
return HOOK_ADAPTERS.find((adapter) => adapter.agent === agent);
|
|
2053
|
+
}
|
|
2054
|
+
function agentsToReport(configured) {
|
|
2055
|
+
const names = new Set(HOOK_ADAPTERS.map((adapter) => adapter.agent));
|
|
2056
|
+
for (const agent of configured)
|
|
2057
|
+
names.add(agent);
|
|
2058
|
+
return [...names].sort();
|
|
2059
|
+
}
|
|
2060
|
+
function agentForEvent(kind, event) {
|
|
2061
|
+
const owner = HOOK_ADAPTERS.find((adapter) => adapter.entries.some((entry) => entry.kind === kind && entry.event === event));
|
|
2062
|
+
return owner?.agent ?? "unknown";
|
|
2063
|
+
}
|
|
2064
|
+
// src/hooks/ensure.ts
|
|
2065
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
2066
|
+
|
|
2067
|
+
// src/usage/queue.ts
|
|
2068
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
2069
|
+
import { appendFile, readFile as readFile3, rm, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
2070
|
+
import { join as join7 } from "node:path";
|
|
2071
|
+
|
|
2072
|
+
// src/client.ts
|
|
2073
|
+
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
2074
|
+
async function createClient(server) {
|
|
2075
|
+
const { token, base } = await requireToken(server);
|
|
2076
|
+
return createTRPCClient({
|
|
2077
|
+
links: [
|
|
2078
|
+
httpBatchLink({
|
|
2079
|
+
url: `${base.replace(/\/$/, "")}/trpc`,
|
|
2080
|
+
headers: () => ({ authorization: `Bearer ${token}` })
|
|
2081
|
+
})
|
|
2082
|
+
]
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// src/usage/queue.ts
|
|
2087
|
+
var USAGE_BATCH_MAX = 100;
|
|
2088
|
+
var QUEUE_MAX_EVENTS = 5000;
|
|
2089
|
+
var TRIM_ABOVE_BYTES = QUEUE_MAX_EVENTS * 80;
|
|
2090
|
+
function queuePath() {
|
|
2091
|
+
return join7(configDir(), "queue.jsonl");
|
|
2092
|
+
}
|
|
2093
|
+
function parseLine(line) {
|
|
2094
|
+
try {
|
|
2095
|
+
const parsed = JSON.parse(line);
|
|
2096
|
+
if (typeof parsed?.skillExternalId !== "string" || typeof parsed.ref !== "string" || typeof parsed.agent !== "string" || typeof parsed.occurredAt !== "string") {
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
return {
|
|
2100
|
+
skillExternalId: parsed.skillExternalId,
|
|
2101
|
+
ref: parsed.ref,
|
|
2102
|
+
agent: parsed.agent,
|
|
2103
|
+
occurredAt: parsed.occurredAt
|
|
2104
|
+
};
|
|
2105
|
+
} catch {
|
|
2106
|
+
return null;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
async function readRaw(path) {
|
|
2110
|
+
try {
|
|
2111
|
+
return await readFile3(path, "utf8");
|
|
2112
|
+
} catch {
|
|
2113
|
+
return "";
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
async function readQueue() {
|
|
2117
|
+
const events = [];
|
|
2118
|
+
for (const line of (await readRaw(queuePath())).split(`
|
|
2119
|
+
`)) {
|
|
2120
|
+
if (line.trim().length === 0)
|
|
2121
|
+
continue;
|
|
2122
|
+
const event = parseLine(line);
|
|
2123
|
+
if (event)
|
|
2124
|
+
events.push(event);
|
|
2125
|
+
}
|
|
2126
|
+
return events;
|
|
2127
|
+
}
|
|
2128
|
+
async function trim(path) {
|
|
2129
|
+
const events = await readQueue();
|
|
2130
|
+
if (events.length <= QUEUE_MAX_EVENTS)
|
|
2131
|
+
return;
|
|
2132
|
+
const kept = events.slice(events.length - QUEUE_MAX_EVENTS);
|
|
2133
|
+
await writeFile2(path, kept.map(line).join(""));
|
|
2134
|
+
}
|
|
2135
|
+
function line(event) {
|
|
2136
|
+
return `${JSON.stringify(event)}
|
|
2137
|
+
`;
|
|
2138
|
+
}
|
|
2139
|
+
async function appendEvent(event) {
|
|
2140
|
+
const path = queuePath();
|
|
2141
|
+
mkdirSync3(configDir(), { recursive: true, mode: 448 });
|
|
2142
|
+
await appendFile(path, line(event));
|
|
2143
|
+
try {
|
|
2144
|
+
const info = await stat(path);
|
|
2145
|
+
if (info.size > TRIM_ABOVE_BYTES)
|
|
2146
|
+
await trim(path);
|
|
2147
|
+
} catch {}
|
|
2148
|
+
}
|
|
2149
|
+
async function keepFrom(path, before, sent) {
|
|
2150
|
+
const lines = before.split(`
|
|
2151
|
+
`);
|
|
2152
|
+
let seen = 0;
|
|
2153
|
+
let cut = lines.length;
|
|
2154
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2155
|
+
const raw = lines[i];
|
|
2156
|
+
if (raw.trim().length === 0)
|
|
2157
|
+
continue;
|
|
2158
|
+
if (parseLine(raw) === null)
|
|
2159
|
+
continue;
|
|
2160
|
+
seen++;
|
|
2161
|
+
if (seen === sent) {
|
|
2162
|
+
cut = i + 1;
|
|
2163
|
+
break;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
const remainder = lines.slice(cut).join(`
|
|
2167
|
+
`);
|
|
2168
|
+
const current = await readRaw(path);
|
|
2169
|
+
const appended = current.length > before.length ? current.slice(before.length) : "";
|
|
2170
|
+
const text = `${remainder}${appended}`;
|
|
2171
|
+
if (text.trim().length === 0) {
|
|
2172
|
+
await rm(path, { force: true });
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
await writeFile2(path, text.endsWith(`
|
|
2176
|
+
`) ? text : `${text}
|
|
2177
|
+
`);
|
|
2178
|
+
}
|
|
2179
|
+
async function flush(client) {
|
|
2180
|
+
const path = queuePath();
|
|
2181
|
+
const before = await readRaw(path);
|
|
2182
|
+
const events = [];
|
|
2183
|
+
for (const raw of before.split(`
|
|
2184
|
+
`)) {
|
|
2185
|
+
if (raw.trim().length === 0)
|
|
2186
|
+
continue;
|
|
2187
|
+
const event = parseLine(raw);
|
|
2188
|
+
if (event)
|
|
2189
|
+
events.push(event);
|
|
2190
|
+
}
|
|
2191
|
+
if (events.length === 0) {
|
|
2192
|
+
if (before.trim().length > 0)
|
|
2193
|
+
await rm(path, { force: true });
|
|
2194
|
+
return { sent: 0, remaining: 0 };
|
|
2195
|
+
}
|
|
2196
|
+
let sent = 0;
|
|
2197
|
+
try {
|
|
2198
|
+
for (let i = 0;i < events.length; i += USAGE_BATCH_MAX) {
|
|
2199
|
+
const batch = events.slice(i, i + USAGE_BATCH_MAX);
|
|
2200
|
+
await client.usage.record.mutate({ events: batch });
|
|
2201
|
+
sent += batch.length;
|
|
2202
|
+
}
|
|
2203
|
+
} catch {}
|
|
2204
|
+
if (sent > 0)
|
|
2205
|
+
await keepFrom(path, before, sent);
|
|
2206
|
+
return { sent, remaining: events.length - sent };
|
|
2207
|
+
}
|
|
2208
|
+
function trackingDisabledByEnv() {
|
|
2209
|
+
const value = process.env.YOURSKILLS_NO_TRACK;
|
|
2210
|
+
return value !== undefined && value !== "" && value !== "0";
|
|
2211
|
+
}
|
|
2212
|
+
async function trackingEnabled() {
|
|
2213
|
+
if (trackingDisabledByEnv())
|
|
2214
|
+
return false;
|
|
2215
|
+
return (await readConfig()).track === true;
|
|
2216
|
+
}
|
|
2217
|
+
async function flushQuietly(server) {
|
|
2218
|
+
try {
|
|
2219
|
+
if (!await trackingEnabled())
|
|
2220
|
+
return 0;
|
|
2221
|
+
if ((await readQueue()).length === 0)
|
|
2222
|
+
return 0;
|
|
2223
|
+
const { sent } = await flush(await createClient(server));
|
|
2224
|
+
return sent;
|
|
2225
|
+
} catch {
|
|
2226
|
+
return 0;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
// src/hooks/settings.ts
|
|
2230
|
+
import { mkdirSync as mkdirSync4 } from "node:fs";
|
|
2231
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
2232
|
+
import { dirname as dirname5 } from "node:path";
|
|
2233
|
+
function binaryName(command) {
|
|
2234
|
+
const parts = command.split(/[\\/]/);
|
|
2235
|
+
return (parts[parts.length - 1] ?? "").replace(/\.(js|mjs|cjs|exe)$/i, "");
|
|
2236
|
+
}
|
|
2237
|
+
function isOurCommand(command) {
|
|
2238
|
+
if (typeof command !== "string")
|
|
2239
|
+
return false;
|
|
2240
|
+
const match = command.trim().match(/^(?:"([^"]+)"|'([^']+)'|(\S+))\s+hook\s+\S/);
|
|
2241
|
+
if (!match)
|
|
2242
|
+
return false;
|
|
2243
|
+
return binaryName(match[1] ?? match[2] ?? match[3] ?? "") === BINARY;
|
|
2244
|
+
}
|
|
2245
|
+
function groupIsOurs(group) {
|
|
2246
|
+
return (group.hooks ?? []).some((hook) => isOurCommand(hook.command));
|
|
2247
|
+
}
|
|
2248
|
+
function addHooks(settings, entries) {
|
|
2249
|
+
let next = { ...settings };
|
|
2250
|
+
for (const entry of entries) {
|
|
2251
|
+
const hooks = { ...next.hooks ?? {} };
|
|
2252
|
+
const groups = [...hooks[entry.event] ?? []];
|
|
2253
|
+
const ours = groups.findIndex((group) => group.matcher === entry.matcher && groupIsOurs(group));
|
|
2254
|
+
if (ours === -1) {
|
|
2255
|
+
groups.push({
|
|
2256
|
+
matcher: entry.matcher,
|
|
2257
|
+
hooks: [{ type: "command", command: entry.command }]
|
|
2258
|
+
});
|
|
2259
|
+
} else {
|
|
2260
|
+
const group = groups[ours];
|
|
2261
|
+
const commands = [...group.hooks ?? []];
|
|
2262
|
+
if (commands.some((hook) => hook.command === entry.command))
|
|
2263
|
+
continue;
|
|
2264
|
+
commands.push({ type: "command", command: entry.command });
|
|
2265
|
+
groups[ours] = { ...group, hooks: commands };
|
|
2266
|
+
}
|
|
2267
|
+
hooks[entry.event] = groups;
|
|
2268
|
+
next = { ...next, hooks };
|
|
2269
|
+
}
|
|
2270
|
+
return next;
|
|
2271
|
+
}
|
|
2272
|
+
function removeHooks(settings) {
|
|
2273
|
+
if (!settings.hooks || typeof settings.hooks !== "object")
|
|
2274
|
+
return { ...settings };
|
|
2275
|
+
const hooks = {};
|
|
2276
|
+
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
2277
|
+
if (!Array.isArray(groups)) {
|
|
2278
|
+
hooks[event] = groups;
|
|
2279
|
+
continue;
|
|
2280
|
+
}
|
|
2281
|
+
const kept = [];
|
|
2282
|
+
let changed = false;
|
|
2283
|
+
for (const group of groups) {
|
|
2284
|
+
const commands = group?.hooks;
|
|
2285
|
+
if (!Array.isArray(commands) || !groupIsOurs(group)) {
|
|
2286
|
+
kept.push(group);
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
changed = true;
|
|
2290
|
+
const rest = commands.filter((hook) => !isOurCommand(hook.command));
|
|
2291
|
+
if (rest.length > 0)
|
|
2292
|
+
kept.push({ ...group, hooks: rest });
|
|
2293
|
+
}
|
|
2294
|
+
if (!changed) {
|
|
2295
|
+
hooks[event] = groups;
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
if (kept.length > 0)
|
|
2299
|
+
hooks[event] = kept;
|
|
2300
|
+
}
|
|
2301
|
+
return { ...settings, hooks };
|
|
2302
|
+
}
|
|
2303
|
+
function registeredEntries(settings, entries) {
|
|
2304
|
+
return entries.filter((entry) => (settings.hooks?.[entry.event] ?? []).some((group) => group.matcher === entry.matcher && (group.hooks ?? []).some((hook) => hook.command === entry.command)));
|
|
2305
|
+
}
|
|
2306
|
+
function detectIndent(text) {
|
|
2307
|
+
const match = text.match(/\n([\t ]+)\S/);
|
|
2308
|
+
if (!match?.[1])
|
|
2309
|
+
return " ";
|
|
2310
|
+
return match[1].includes("\t") ? "\t" : match[1];
|
|
2311
|
+
}
|
|
2312
|
+
async function readSettingsFile(path) {
|
|
2313
|
+
let text;
|
|
2314
|
+
try {
|
|
2315
|
+
text = await readFile4(path, "utf8");
|
|
2316
|
+
} catch {
|
|
2317
|
+
return { ok: true, settings: {}, indent: " ", existed: false };
|
|
2318
|
+
}
|
|
2319
|
+
if (text.trim().length === 0) {
|
|
2320
|
+
return { ok: true, settings: {}, indent: " ", existed: true };
|
|
2321
|
+
}
|
|
2322
|
+
let parsed;
|
|
2323
|
+
try {
|
|
2324
|
+
parsed = JSON.parse(text);
|
|
2325
|
+
} catch (error) {
|
|
2326
|
+
return {
|
|
2327
|
+
ok: false,
|
|
2328
|
+
error: `${path}: not valid JSON (${error instanceof Error ? error.message : String(error)})`
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2332
|
+
return { ok: false, error: `${path}: not a JSON object` };
|
|
2333
|
+
}
|
|
2334
|
+
return {
|
|
2335
|
+
ok: true,
|
|
2336
|
+
settings: parsed,
|
|
2337
|
+
indent: detectIndent(text),
|
|
2338
|
+
existed: true
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
function serialiseSettings(settings, indent) {
|
|
2342
|
+
return `${JSON.stringify(settings, null, indent)}
|
|
2343
|
+
`;
|
|
2344
|
+
}
|
|
2345
|
+
async function writeSettingsFile(path, settings, indent) {
|
|
2346
|
+
mkdirSync4(dirname5(path), { recursive: true });
|
|
2347
|
+
await writeFile3(path, serialiseSettings(settings, indent));
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
// src/hooks/ensure.ts
|
|
2351
|
+
async function registerInto(adapter) {
|
|
2352
|
+
const wanted = adapter.entries.filter((entry) => entry.kind === "update-check");
|
|
2353
|
+
if (wanted.length === 0)
|
|
2354
|
+
return null;
|
|
2355
|
+
const path = adapter.settingsPath();
|
|
2356
|
+
if (!path)
|
|
2357
|
+
return null;
|
|
2358
|
+
if (!existsSync3(adapter.home()))
|
|
2359
|
+
return null;
|
|
2360
|
+
const file = await readSettingsFile(path);
|
|
2361
|
+
if (!file.ok)
|
|
2362
|
+
return null;
|
|
2363
|
+
const merged = addHooks(file.settings, wanted);
|
|
2364
|
+
if (serialiseSettings(merged, file.indent) === serialiseSettings(file.settings, file.indent)) {
|
|
2365
|
+
return null;
|
|
2366
|
+
}
|
|
2367
|
+
await writeSettingsFile(path, merged, file.indent);
|
|
2368
|
+
return { agent: adapter.agent, path, entries: wanted };
|
|
2369
|
+
}
|
|
2370
|
+
function linesFor(registered) {
|
|
2371
|
+
const lines = [
|
|
2372
|
+
"yourskills registered its update-check hook so your agent can tell you when an installed skill has moved:"
|
|
2373
|
+
];
|
|
2374
|
+
for (const { agent, path, entries } of registered) {
|
|
2375
|
+
for (const entry of entries) {
|
|
2376
|
+
lines.push(` ${agent} ${entry.event}(${entry.matcher}) ${path}`);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
lines.push(" undo any time with `yourskills hooks uninstall`");
|
|
2380
|
+
if (!trackingDisabledByEnv()) {
|
|
2381
|
+
lines.push(" usage counting stays off; `yourskills hooks install --track` turns it on");
|
|
2382
|
+
}
|
|
2383
|
+
return lines;
|
|
2384
|
+
}
|
|
2385
|
+
async function ensureHooksRegistered() {
|
|
2386
|
+
try {
|
|
2387
|
+
const config = await readConfig();
|
|
2388
|
+
if (config.hooksOffered)
|
|
2389
|
+
return [];
|
|
2390
|
+
if (!config.token)
|
|
2391
|
+
return [];
|
|
2392
|
+
const registered = [];
|
|
2393
|
+
for (const adapter of HOOK_ADAPTERS) {
|
|
2394
|
+
try {
|
|
2395
|
+
const result = await registerInto(adapter);
|
|
2396
|
+
if (result)
|
|
2397
|
+
registered.push(result);
|
|
2398
|
+
} catch {}
|
|
2399
|
+
}
|
|
2400
|
+
await writeConfig({ ...config, hooksOffered: new Date().toISOString() });
|
|
2401
|
+
return registered.length > 0 ? linesFor(registered) : [];
|
|
2402
|
+
} catch {
|
|
2403
|
+
return [];
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
// src/hooks/install.ts
|
|
2407
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
2408
|
+
var PAYLOAD_DISCLOSURE = [
|
|
2409
|
+
{ mark: "none", text: "Usage tracking sends, once per skill invocation:" },
|
|
2410
|
+
{ mark: "none", text: " skillExternalId", note: "the skill's registry id" },
|
|
2411
|
+
{ mark: "none", text: " ref", note: "the commit you installed it at" },
|
|
2412
|
+
{ mark: "none", text: " agent", note: "which agent ran it" },
|
|
2413
|
+
{ mark: "none", text: " occurredAt", note: "when it ran" },
|
|
2414
|
+
{
|
|
2415
|
+
mark: "none",
|
|
2416
|
+
text: " (nothing else)",
|
|
2417
|
+
note: "the server adds a keyed hash of you; the CLI never sends a user id"
|
|
2418
|
+
}
|
|
2419
|
+
];
|
|
2420
|
+
function trackNote(track) {
|
|
2421
|
+
if (trackingDisabledByEnv())
|
|
2422
|
+
return "off (YOURSKILLS_NO_TRACK is set)";
|
|
2423
|
+
return track ? "on" : "off — enable with `yourskills hooks install --track`";
|
|
2424
|
+
}
|
|
2425
|
+
function selectAdapters(agent) {
|
|
2426
|
+
if (agent) {
|
|
2427
|
+
const found = adapterFor(agent);
|
|
2428
|
+
return found ? { adapters: [found], unknown: [] } : { adapters: [], unknown: [agent] };
|
|
2429
|
+
}
|
|
2430
|
+
return {
|
|
2431
|
+
adapters: HOOK_ADAPTERS.filter((a) => existsSync4(a.home())),
|
|
2432
|
+
unknown: []
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
async function pinnedAdapters() {
|
|
2436
|
+
const pinned = await resolveAgents();
|
|
2437
|
+
if (pinned.length === 0)
|
|
2438
|
+
return null;
|
|
2439
|
+
const adapters = pinned.map((name) => adapterFor(name)).filter((a) => a !== undefined);
|
|
2440
|
+
return adapters.length > 0 ? adapters : null;
|
|
2441
|
+
}
|
|
2442
|
+
async function installInto(adapter, optedIn) {
|
|
2443
|
+
const path = adapter.settingsPath();
|
|
2444
|
+
const result = {
|
|
2445
|
+
agent: adapter.agent,
|
|
2446
|
+
path,
|
|
2447
|
+
installed: [],
|
|
2448
|
+
already: [],
|
|
2449
|
+
skipped: []
|
|
2450
|
+
};
|
|
2451
|
+
if (!path) {
|
|
2452
|
+
for (const kind of adapter.supports) {
|
|
2453
|
+
result.skipped.push({
|
|
2454
|
+
kind,
|
|
2455
|
+
why: adapter.limitation ?? "not supported yet"
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
return result;
|
|
2459
|
+
}
|
|
2460
|
+
const wanted = [];
|
|
2461
|
+
for (const entry of adapter.entries) {
|
|
2462
|
+
if (entry.kind === "usage" && !optedIn) {
|
|
2463
|
+
result.skipped.push({ kind: entry.kind, why: trackNote(false) });
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
wanted.push(entry);
|
|
2467
|
+
}
|
|
2468
|
+
const file = await readSettingsFile(path);
|
|
2469
|
+
if (!file.ok) {
|
|
2470
|
+
result.error = file.error;
|
|
2471
|
+
return result;
|
|
2472
|
+
}
|
|
2473
|
+
const present = new Set(registeredEntries(file.settings, wanted).map((entry) => entry.kind));
|
|
2474
|
+
for (const entry of wanted) {
|
|
2475
|
+
if (present.has(entry.kind))
|
|
2476
|
+
result.already.push(entry.kind);
|
|
2477
|
+
else
|
|
2478
|
+
result.installed.push(entry.kind);
|
|
2479
|
+
}
|
|
2480
|
+
const merged = addHooks(file.settings, wanted);
|
|
2481
|
+
if (serialiseSettings(merged, file.indent) !== serialiseSettings(file.settings, file.indent)) {
|
|
2482
|
+
await writeSettingsFile(path, merged, file.indent);
|
|
2483
|
+
}
|
|
2484
|
+
return result;
|
|
2485
|
+
}
|
|
2486
|
+
function linesFor2(result) {
|
|
2487
|
+
const lines = [];
|
|
2488
|
+
if (result.error) {
|
|
2489
|
+
lines.push({
|
|
2490
|
+
mark: "fail",
|
|
2491
|
+
text: `${result.agent}: left untouched`,
|
|
2492
|
+
note: result.error
|
|
2493
|
+
});
|
|
2494
|
+
return lines;
|
|
2495
|
+
}
|
|
2496
|
+
for (const kind of result.installed) {
|
|
2497
|
+
lines.push({
|
|
2498
|
+
mark: "ok",
|
|
2499
|
+
text: `${result.agent} ${kind}`,
|
|
2500
|
+
note: result.path ?? ""
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
for (const kind of result.already) {
|
|
2504
|
+
lines.push({
|
|
2505
|
+
mark: "ok",
|
|
2506
|
+
text: `${result.agent} ${kind}`,
|
|
2507
|
+
note: `already registered ${result.path ?? ""}`.trim()
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
for (const skip of result.skipped) {
|
|
2511
|
+
lines.push({
|
|
2512
|
+
mark: "pending",
|
|
2513
|
+
text: `${result.agent} ${skip.kind}`,
|
|
2514
|
+
note: `skipped: ${skip.why}`
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
return lines;
|
|
2518
|
+
}
|
|
2519
|
+
async function hooksInstall(opts = {}) {
|
|
2520
|
+
if (opts.track !== undefined) {
|
|
2521
|
+
const config2 = await readConfig();
|
|
2522
|
+
await writeConfig({ ...config2, track: opts.track });
|
|
2523
|
+
}
|
|
2524
|
+
const config = await readConfig();
|
|
2525
|
+
const optedIn = !trackingDisabledByEnv() && config.track === true;
|
|
2526
|
+
const { adapters: selected, unknown } = selectAdapters(opts.agent);
|
|
2527
|
+
const adapters = opts.agent === undefined ? await pinnedAdapters() ?? selected : selected;
|
|
2528
|
+
const lines = [...PAYLOAD_DISCLOSURE];
|
|
2529
|
+
lines.push({ mark: "none", text: "tracking", note: trackNote(optedIn) });
|
|
2530
|
+
for (const agent of unknown) {
|
|
2531
|
+
lines.push({
|
|
2532
|
+
mark: "fail",
|
|
2533
|
+
text: agent,
|
|
2534
|
+
note: "no yourskills hook support — see `yourskills hooks status`"
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
const results = [];
|
|
2538
|
+
for (const adapter of adapters) {
|
|
2539
|
+
const result = await installInto(adapter, optedIn);
|
|
2540
|
+
results.push(result);
|
|
2541
|
+
lines.push(...linesFor2(result));
|
|
2542
|
+
}
|
|
2543
|
+
if (adapters.length === 0 && unknown.length === 0) {
|
|
2544
|
+
lines.push({
|
|
2545
|
+
mark: "pending",
|
|
2546
|
+
text: "No agent found that can carry yourskills hooks.",
|
|
2547
|
+
note: "pin one with `yourskills config set agents claude-code`"
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
return {
|
|
2551
|
+
title: "hooks install",
|
|
2552
|
+
lines,
|
|
2553
|
+
json: { track: optedIn, agents: results, unknown },
|
|
2554
|
+
failed: unknown.length > 0 || results.some((r) => r.error !== undefined)
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
async function hooksUninstall(opts = {}) {
|
|
2558
|
+
const { adapters: selected, unknown } = selectAdapters(opts.agent);
|
|
2559
|
+
const adapters = opts.agent ? selected : HOOK_ADAPTERS;
|
|
2560
|
+
const lines = [];
|
|
2561
|
+
const results = [];
|
|
2562
|
+
for (const agent of unknown) {
|
|
2563
|
+
lines.push({ mark: "fail", text: agent, note: "unknown agent" });
|
|
2564
|
+
}
|
|
2565
|
+
for (const adapter of adapters) {
|
|
2566
|
+
const path = adapter.settingsPath();
|
|
2567
|
+
if (!path)
|
|
2568
|
+
continue;
|
|
2569
|
+
const file = await readSettingsFile(path);
|
|
2570
|
+
if (!file.ok) {
|
|
2571
|
+
results.push({
|
|
2572
|
+
agent: adapter.agent,
|
|
2573
|
+
path,
|
|
2574
|
+
removed: false,
|
|
2575
|
+
error: file.error
|
|
2576
|
+
});
|
|
2577
|
+
lines.push({
|
|
2578
|
+
mark: "fail",
|
|
2579
|
+
text: `${adapter.agent}: left untouched`,
|
|
2580
|
+
note: file.error
|
|
2581
|
+
});
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2584
|
+
const stripped = removeHooks(file.settings);
|
|
2585
|
+
const changed = serialiseSettings(stripped, file.indent) !== serialiseSettings(file.settings, file.indent);
|
|
2586
|
+
if (changed)
|
|
2587
|
+
await writeSettingsFile(path, stripped, file.indent);
|
|
2588
|
+
results.push({ agent: adapter.agent, path, removed: changed });
|
|
2589
|
+
lines.push({
|
|
2590
|
+
mark: changed ? "ok" : "none",
|
|
2591
|
+
text: `${adapter.agent} ${changed ? "removed" : "nothing registered"}`,
|
|
2592
|
+
note: path
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
return {
|
|
2596
|
+
title: "hooks uninstall",
|
|
2597
|
+
lines,
|
|
2598
|
+
json: { agents: results, unknown },
|
|
2599
|
+
failed: unknown.length > 0 || results.some((r) => r.error !== undefined)
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
async function hooksStatus() {
|
|
2603
|
+
const config = await readConfig();
|
|
2604
|
+
const optedIn = !trackingDisabledByEnv() && config.track === true;
|
|
2605
|
+
const lines = [
|
|
2606
|
+
{ mark: "none", text: "tracking", note: trackNote(optedIn) }
|
|
2607
|
+
];
|
|
2608
|
+
const agents2 = [];
|
|
2609
|
+
for (const agent of agentsToReport(config.agents ?? [])) {
|
|
2610
|
+
const adapter = adapterFor(agent);
|
|
2611
|
+
if (!adapter) {
|
|
2612
|
+
lines.push({
|
|
2613
|
+
mark: "none",
|
|
2614
|
+
text: agent,
|
|
2615
|
+
note: "no yourskills hook support"
|
|
2616
|
+
});
|
|
2617
|
+
agents2.push({ agent, supported: false });
|
|
2618
|
+
continue;
|
|
2619
|
+
}
|
|
2620
|
+
const path = adapter.settingsPath();
|
|
2621
|
+
if (!path) {
|
|
2622
|
+
lines.push({
|
|
2623
|
+
mark: "none",
|
|
2624
|
+
text: agent,
|
|
2625
|
+
note: adapter.limitation ?? "not supported yet"
|
|
2626
|
+
});
|
|
2627
|
+
agents2.push({
|
|
2628
|
+
agent,
|
|
2629
|
+
supported: false,
|
|
2630
|
+
supports: adapter.supports,
|
|
2631
|
+
limitation: adapter.limitation
|
|
2632
|
+
});
|
|
2633
|
+
continue;
|
|
2634
|
+
}
|
|
2635
|
+
const file = await readSettingsFile(path);
|
|
2636
|
+
if (!file.ok) {
|
|
2637
|
+
lines.push({ mark: "fail", text: agent, note: file.error });
|
|
2638
|
+
agents2.push({ agent, supported: true, path, error: file.error });
|
|
2639
|
+
continue;
|
|
2640
|
+
}
|
|
2641
|
+
const registered = new Set(registeredEntries(file.settings, adapter.entries).map((e) => e.kind));
|
|
2642
|
+
for (const entry of adapter.entries) {
|
|
2643
|
+
const on = registered.has(entry.kind);
|
|
2644
|
+
lines.push({
|
|
2645
|
+
mark: on ? "ok" : "pending",
|
|
2646
|
+
text: `${agent} ${entry.kind}`,
|
|
2647
|
+
note: on ? `${entry.event}(${entry.matcher}) ${path}` : "not registered — run `yourskills hooks install`"
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
agents2.push({
|
|
2651
|
+
agent,
|
|
2652
|
+
supported: true,
|
|
2653
|
+
path,
|
|
2654
|
+
registered: [...registered],
|
|
2655
|
+
supports: adapter.supports
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
return {
|
|
2659
|
+
title: "hooks status",
|
|
2660
|
+
lines,
|
|
2661
|
+
json: { track: optedIn, agents: agents2 }
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
// ../../packages/bundles/src/drift.ts
|
|
2665
|
+
var SHORT_SHA = 7;
|
|
2666
|
+
function namesSameCommit(prefix, full) {
|
|
2667
|
+
const short = prefix.trim().toLowerCase();
|
|
2668
|
+
const long = full.trim().toLowerCase();
|
|
2669
|
+
if (short === long)
|
|
2670
|
+
return true;
|
|
2671
|
+
return short.length >= SHORT_SHA && long.startsWith(short);
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// src/installed/installed.ts
|
|
2675
|
+
var shortRef = (ref) => ref.slice(0, SHORT_SHA);
|
|
2676
|
+
function installedRows(nearest) {
|
|
2677
|
+
return Object.values(nearest.manifest.skills).map((entry) => ({
|
|
2678
|
+
name: entry.name,
|
|
2679
|
+
ref: entry.ref,
|
|
2680
|
+
bundle: entry.bundle ?? null,
|
|
2681
|
+
scope: nearest.manifest.scope
|
|
2682
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
2683
|
+
}
|
|
2684
|
+
function columns(rows) {
|
|
2685
|
+
const names = Math.max(...rows.map((r) => r.name.length));
|
|
2686
|
+
const bundles = Math.max(...rows.map((r) => (r.bundle ?? "-").length));
|
|
2687
|
+
return rows.map((row) => ({
|
|
2688
|
+
mark: "none",
|
|
2689
|
+
text: `${row.name.padEnd(names)} ${shortRef(row.ref)}`,
|
|
2690
|
+
note: `${(row.bundle ?? "-").padEnd(bundles)} ${row.scope}`
|
|
2691
|
+
}));
|
|
2692
|
+
}
|
|
2693
|
+
function installedReport(nearest) {
|
|
2694
|
+
const rows = installedRows(nearest);
|
|
2695
|
+
const lines = rows.length === 0 ? [
|
|
2696
|
+
{
|
|
2697
|
+
mark: "none",
|
|
2698
|
+
text: "Nothing installed. Run `yourskills bundle <name>` or `yourskills add <skill>`.",
|
|
2699
|
+
note: nearest.path
|
|
2700
|
+
}
|
|
2701
|
+
] : columns(rows);
|
|
2702
|
+
return {
|
|
2703
|
+
title: "installed",
|
|
2704
|
+
lines,
|
|
2705
|
+
json: {
|
|
2706
|
+
manifest: nearest.path,
|
|
2707
|
+
scope: nearest.manifest.scope,
|
|
2708
|
+
skills: rows
|
|
2709
|
+
}
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
// src/installed/outdated.ts
|
|
2713
|
+
function hasMoved(installed, available) {
|
|
2714
|
+
return !(namesSameCommit(available, installed) || namesSameCommit(installed, available));
|
|
2715
|
+
}
|
|
2716
|
+
function drift(manifest, pins, catalog) {
|
|
2717
|
+
const byExternalId = new Map(catalog.map((row) => [row.externalId, row]));
|
|
2718
|
+
const byName = new Map(catalog.map((row) => [row.name, row]));
|
|
2719
|
+
const moved = [];
|
|
2720
|
+
const unknown = [];
|
|
2721
|
+
for (const entry of Object.values(manifest.skills)) {
|
|
2722
|
+
const row = byExternalId.get(entry.externalId) ?? byName.get(entry.name);
|
|
2723
|
+
if (!row) {
|
|
2724
|
+
unknown.push(entry.name);
|
|
2725
|
+
continue;
|
|
2726
|
+
}
|
|
2727
|
+
const pin = entry.bundle ? pins.find((p) => p.bundle === entry.bundle && p.skillId === row.id) : undefined;
|
|
2728
|
+
const available = pin?.pinnedSha ?? row.currentSha;
|
|
2729
|
+
if (!hasMoved(entry.ref, available))
|
|
2730
|
+
continue;
|
|
2731
|
+
moved.push({
|
|
2732
|
+
name: entry.name,
|
|
2733
|
+
bundle: entry.bundle ?? null,
|
|
2734
|
+
installed: entry.ref,
|
|
2735
|
+
available,
|
|
2736
|
+
deprecated: pin?.deprecated ?? row.status === "deprecated"
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
moved.sort((a, b) => a.name.localeCompare(b.name));
|
|
2740
|
+
return { moved, unknown };
|
|
2741
|
+
}
|
|
2742
|
+
async function outdatedReport(client, nearest, quiet = false) {
|
|
2743
|
+
let pins;
|
|
2744
|
+
let catalog;
|
|
2745
|
+
try {
|
|
2746
|
+
[pins, catalog] = await Promise.all([
|
|
2747
|
+
client.bundles.pins.query(),
|
|
2748
|
+
client.skills.list.query({})
|
|
2749
|
+
]);
|
|
2750
|
+
} catch (error) {
|
|
2751
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2752
|
+
return {
|
|
2753
|
+
title: "outdated",
|
|
2754
|
+
lines: [
|
|
2755
|
+
{ mark: "fail", text: "Could not reach the registry.", note: message }
|
|
2756
|
+
],
|
|
2757
|
+
json: { manifest: nearest.path, error: message },
|
|
2758
|
+
silent: quiet
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
const report = drift(nearest.manifest, pins, catalog);
|
|
2762
|
+
const lines = report.moved.map((row) => ({
|
|
2763
|
+
mark: "pending",
|
|
2764
|
+
text: `${row.name} ${shortRef(row.installed)} -> ${shortRef(row.available)}`,
|
|
2765
|
+
note: [row.bundle ?? "-", row.deprecated ? "[deprecated]" : ""].filter(Boolean).join(" ")
|
|
2766
|
+
}));
|
|
2767
|
+
for (const name of report.unknown) {
|
|
2768
|
+
lines.push({
|
|
2769
|
+
mark: "fail",
|
|
2770
|
+
text: name,
|
|
2771
|
+
note: "no longer in your registry"
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
const current = report.moved.length === 0 && report.unknown.length === 0;
|
|
2775
|
+
if (current && !quiet) {
|
|
2776
|
+
lines.push({
|
|
2777
|
+
mark: "ok",
|
|
2778
|
+
text: "Everything is at the ref your registry pins."
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
return {
|
|
2782
|
+
title: "outdated",
|
|
2783
|
+
lines,
|
|
2784
|
+
json: {
|
|
2785
|
+
manifest: nearest.path,
|
|
2786
|
+
outdated: report.moved,
|
|
2787
|
+
unknown: report.unknown
|
|
2788
|
+
},
|
|
2789
|
+
silent: quiet && current
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
// src/hooks/run.ts
|
|
2793
|
+
function isHookEventName(value) {
|
|
2794
|
+
return value === "session-start" || value === "post-tool-use";
|
|
2795
|
+
}
|
|
2796
|
+
var STDIN_BUDGET_MS = 2000;
|
|
2797
|
+
var SESSION_BUDGET_MS = 5000;
|
|
2798
|
+
var DAY_MS = 86400000;
|
|
2799
|
+
async function readStdin() {
|
|
2800
|
+
if (process.stdin.isTTY)
|
|
2801
|
+
return {};
|
|
2802
|
+
const text = await Promise.race([
|
|
2803
|
+
(async () => {
|
|
2804
|
+
let raw = "";
|
|
2805
|
+
process.stdin.setEncoding("utf8");
|
|
2806
|
+
for await (const chunk of process.stdin)
|
|
2807
|
+
raw += chunk;
|
|
2808
|
+
return raw;
|
|
2809
|
+
})(),
|
|
2810
|
+
new Promise((resolve3) => {
|
|
2811
|
+
const timer = setTimeout(() => resolve3(""), STDIN_BUDGET_MS);
|
|
2812
|
+
timer.unref?.();
|
|
2813
|
+
})
|
|
2814
|
+
]);
|
|
2815
|
+
if (text.trim().length === 0)
|
|
2816
|
+
return {};
|
|
2817
|
+
try {
|
|
2818
|
+
return JSON.parse(text);
|
|
2819
|
+
} catch {
|
|
2820
|
+
return {};
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
async function lookup(name, cwd) {
|
|
2824
|
+
const nearest = await nearestManifest(undefined, cwd);
|
|
2825
|
+
const found = nearest.manifest.skills[name];
|
|
2826
|
+
if (found)
|
|
2827
|
+
return found;
|
|
2828
|
+
if (nearest.scope === "global")
|
|
2829
|
+
return;
|
|
2830
|
+
const global = readManifest(manifestPathFor("global"));
|
|
2831
|
+
return global.skills[name];
|
|
2832
|
+
}
|
|
2833
|
+
async function queueInvocation(payload, now = new Date) {
|
|
2834
|
+
if (!await trackingEnabled())
|
|
2835
|
+
return null;
|
|
2836
|
+
const name = payload.tool_input?.skill;
|
|
2837
|
+
if (typeof name !== "string" || name.length === 0)
|
|
2838
|
+
return null;
|
|
2839
|
+
const entry = await lookup(name, payload.cwd);
|
|
2840
|
+
if (!entry)
|
|
2841
|
+
return null;
|
|
2842
|
+
const event = {
|
|
2843
|
+
skillExternalId: entry.externalId,
|
|
2844
|
+
ref: entry.ref,
|
|
2845
|
+
agent: agentForEvent("usage", "PostToolUse"),
|
|
2846
|
+
occurredAt: now.toISOString()
|
|
2847
|
+
};
|
|
2848
|
+
await appendEvent(event);
|
|
2849
|
+
return event;
|
|
2850
|
+
}
|
|
2851
|
+
function updateCheckDue(last, now = Date.now()) {
|
|
2852
|
+
if (!last)
|
|
2853
|
+
return true;
|
|
2854
|
+
const at = Date.parse(last);
|
|
2855
|
+
return Number.isNaN(at) || now - at >= DAY_MS;
|
|
2856
|
+
}
|
|
2857
|
+
async function sessionStart() {
|
|
2858
|
+
const config = await readConfig();
|
|
2859
|
+
if (!updateCheckDue(config.lastUpdateCheck))
|
|
2860
|
+
return;
|
|
2861
|
+
await writeConfig({ ...config, lastUpdateCheck: new Date().toISOString() });
|
|
2862
|
+
const client = await createClient();
|
|
2863
|
+
const nearest = await nearestManifest();
|
|
2864
|
+
const report = await outdatedReport(client, nearest, true);
|
|
2865
|
+
if (!report.silent && report.lines.length > 0) {
|
|
2866
|
+
const moved = report.json.outdated?.length ?? 0;
|
|
2867
|
+
if (moved > 0) {
|
|
2868
|
+
console.log(`yourskills: ${moved} skill(s) behind your registry — run \`yourskills update\``);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
if (await trackingEnabled())
|
|
2872
|
+
await flush(client);
|
|
2873
|
+
}
|
|
2874
|
+
async function runHook(event) {
|
|
2875
|
+
try {
|
|
2876
|
+
const payload = await readStdin();
|
|
2877
|
+
if (event === "post-tool-use")
|
|
2878
|
+
await queueInvocation(payload);
|
|
2879
|
+
else
|
|
2880
|
+
await withBudget(sessionStart(), SESSION_BUDGET_MS);
|
|
2881
|
+
} catch {}
|
|
2882
|
+
process.exitCode = 0;
|
|
2883
|
+
}
|
|
2884
|
+
function withBudget(work, ms) {
|
|
2885
|
+
return Promise.race([
|
|
2886
|
+
work,
|
|
2887
|
+
new Promise((resolve3) => {
|
|
2888
|
+
const timer = setTimeout(() => resolve3(undefined), ms);
|
|
2889
|
+
timer.unref?.();
|
|
2890
|
+
})
|
|
2891
|
+
]);
|
|
2892
|
+
}
|
|
2893
|
+
// src/org/org.ts
|
|
2894
|
+
var authBase2 = (base) => `${base.replace(/\/$/, "")}/api/auth`;
|
|
2895
|
+
async function activeOrganizationId(base, token) {
|
|
2896
|
+
const res = await fetch(`${authBase2(base)}/get-session`, {
|
|
2897
|
+
headers: { authorization: `Bearer ${token}` }
|
|
2898
|
+
});
|
|
2899
|
+
if (!res.ok)
|
|
2900
|
+
return null;
|
|
2901
|
+
const body = await res.json();
|
|
2902
|
+
return body?.session?.activeOrganizationId ?? null;
|
|
2903
|
+
}
|
|
2904
|
+
async function listOrganizations(base, token) {
|
|
2905
|
+
let res;
|
|
2906
|
+
try {
|
|
2907
|
+
res = await fetch(`${authBase2(base)}/organization/list`, {
|
|
2908
|
+
headers: { authorization: `Bearer ${token}` }
|
|
2909
|
+
});
|
|
2910
|
+
} catch (error) {
|
|
2911
|
+
throw new Error(`Could not reach ${base}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2912
|
+
}
|
|
2913
|
+
if (!res.ok) {
|
|
2914
|
+
throw new Error(`Could not list organizations: ${res.status} ${res.statusText}`);
|
|
2915
|
+
}
|
|
2916
|
+
return await res.json();
|
|
2917
|
+
}
|
|
2918
|
+
function rows(orgs, active) {
|
|
2919
|
+
if (orgs.length === 0) {
|
|
2920
|
+
return [
|
|
2921
|
+
{
|
|
2922
|
+
mark: "none",
|
|
2923
|
+
text: "You are not a member of any organization yet."
|
|
2924
|
+
}
|
|
2925
|
+
];
|
|
2926
|
+
}
|
|
2927
|
+
const width = Math.max(...orgs.map((o) => o.name.length));
|
|
2928
|
+
return orgs.map((org) => ({
|
|
2929
|
+
mark: org.id === active ? "ok" : "none",
|
|
2930
|
+
text: `${org.id === active ? "" : " "}${org.name.padEnd(width)}`,
|
|
2931
|
+
note: org.slug ?? org.id
|
|
2932
|
+
}));
|
|
2933
|
+
}
|
|
2934
|
+
async function orgListReport(server) {
|
|
2935
|
+
const { token, base } = await requireToken(server);
|
|
2936
|
+
const [orgs, active] = await Promise.all([
|
|
2937
|
+
listOrganizations(base, token),
|
|
2938
|
+
activeOrganizationId(base, token)
|
|
2939
|
+
]);
|
|
2940
|
+
return {
|
|
2941
|
+
title: "org",
|
|
2942
|
+
lines: rows(orgs, active),
|
|
2943
|
+
json: {
|
|
2944
|
+
active,
|
|
2945
|
+
organizations: orgs.map((o) => ({
|
|
2946
|
+
id: o.id,
|
|
2947
|
+
name: o.name,
|
|
2948
|
+
slug: o.slug ?? null,
|
|
2949
|
+
active: o.id === active
|
|
2950
|
+
}))
|
|
2951
|
+
}
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
async function orgSwitchReport(name, server) {
|
|
2955
|
+
const { token, base } = await requireToken(server);
|
|
2956
|
+
const orgs = await listOrganizations(base, token);
|
|
2957
|
+
const wanted = name.toLowerCase();
|
|
2958
|
+
const match = orgs.find((o) => o.name.toLowerCase() === wanted || o.slug?.toLowerCase() === wanted);
|
|
2959
|
+
if (!match) {
|
|
2960
|
+
return {
|
|
2961
|
+
title: "org",
|
|
2962
|
+
lines: [
|
|
2963
|
+
{
|
|
2964
|
+
mark: "fail",
|
|
2965
|
+
text: `No organization called ${name}.`,
|
|
2966
|
+
note: orgs.map((o) => o.name).join(", ") || "you are in none"
|
|
2967
|
+
}
|
|
2968
|
+
],
|
|
2969
|
+
json: { error: `unknown organization: ${name}` },
|
|
2970
|
+
failed: true
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
const res = await fetch(`${authBase2(base)}/organization/set-active`, {
|
|
2974
|
+
method: "POST",
|
|
2975
|
+
headers: {
|
|
2976
|
+
"content-type": "application/json",
|
|
2977
|
+
authorization: `Bearer ${token}`
|
|
2978
|
+
},
|
|
2979
|
+
body: JSON.stringify({ organizationId: match.id })
|
|
2980
|
+
});
|
|
2981
|
+
if (!res.ok) {
|
|
2982
|
+
const detail = (await res.text()).slice(0, 200);
|
|
2983
|
+
return {
|
|
2984
|
+
title: "org",
|
|
2985
|
+
lines: [
|
|
2986
|
+
{
|
|
2987
|
+
mark: "fail",
|
|
2988
|
+
text: `Could not switch to ${match.name}.`,
|
|
2989
|
+
note: detail || `${res.status} ${res.statusText}`
|
|
2990
|
+
}
|
|
2991
|
+
],
|
|
2992
|
+
json: { error: detail || res.statusText },
|
|
2993
|
+
failed: true
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
return {
|
|
2997
|
+
title: "org",
|
|
2998
|
+
lines: [{ mark: "ok", text: `Active organization: ${match.name}` }],
|
|
2999
|
+
json: { active: match.id, name: match.name, slug: match.slug ?? null }
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
// src/report.ts
|
|
3003
|
+
function plainLine(line2, glyph) {
|
|
3004
|
+
const head = line2.mark === "none" ? "" : `${glyph[line2.mark]} `;
|
|
3005
|
+
return `${head}${line2.text}${line2.note ? ` ${line2.note}` : ""}`;
|
|
3006
|
+
}
|
|
3007
|
+
// src/shell/commands.ts
|
|
3008
|
+
function paletteGroups() {
|
|
3009
|
+
return [
|
|
3010
|
+
{
|
|
3011
|
+
group: "Catalog",
|
|
3012
|
+
items: [
|
|
3013
|
+
{ value: "catalog.bundles", label: "browse bundles", hint: "browse" },
|
|
3014
|
+
{ value: "catalog.skills", label: "browse skills", hint: "list" },
|
|
3015
|
+
{ value: "catalog.search", label: "search", hint: "search <query>" }
|
|
3016
|
+
]
|
|
3017
|
+
},
|
|
3018
|
+
{
|
|
3019
|
+
group: "Installed",
|
|
3020
|
+
items: [
|
|
3021
|
+
{ value: "installed.list", label: "list installed", hint: "installed" },
|
|
3022
|
+
{
|
|
3023
|
+
value: "installed.update",
|
|
3024
|
+
label: "update all",
|
|
3025
|
+
hint: "update --all"
|
|
3026
|
+
},
|
|
3027
|
+
{ value: "installed.outdated", label: "outdated", hint: "outdated" },
|
|
3028
|
+
{ value: "installed.remove", label: "remove", hint: "remove <skill>" }
|
|
3029
|
+
]
|
|
3030
|
+
},
|
|
3031
|
+
{
|
|
3032
|
+
group: "Account",
|
|
3033
|
+
items: [
|
|
3034
|
+
{ value: "account.login", label: "login", hint: "login" },
|
|
3035
|
+
{ value: "account.logout", label: "logout", hint: "logout" },
|
|
3036
|
+
{ value: "account.whoami", label: "whoami", hint: "whoami" },
|
|
3037
|
+
{ value: "account.org", label: "switch org", hint: "org <name>" }
|
|
3038
|
+
]
|
|
3039
|
+
},
|
|
3040
|
+
{
|
|
3041
|
+
group: "Setup",
|
|
3042
|
+
items: [
|
|
3043
|
+
{
|
|
3044
|
+
value: "setup.scope",
|
|
3045
|
+
label: "scope",
|
|
3046
|
+
hint: "config set scope project"
|
|
3047
|
+
},
|
|
3048
|
+
{
|
|
3049
|
+
value: "setup.agents",
|
|
3050
|
+
label: "agents",
|
|
3051
|
+
hint: "config set agents <name>"
|
|
3052
|
+
},
|
|
3053
|
+
{ value: "setup.doctor", label: "doctor", hint: "doctor" }
|
|
3054
|
+
]
|
|
3055
|
+
}
|
|
3056
|
+
];
|
|
3057
|
+
}
|
|
3058
|
+
// src/shell/screens/home.tsx
|
|
3059
|
+
import { Box as Box10, Text as Text12, useApp as useApp7, useInput as useInput3 } from "ink";
|
|
3060
|
+
import { useCallback as useCallback3, useEffect as useEffect8, useMemo as useMemo3, useRef, useState as useState8 } from "react";
|
|
3061
|
+
// src/screens.tsx
|
|
3062
|
+
import { Box as Box9, Text as Text11, useApp as useApp6 } from "ink";
|
|
3063
|
+
import { useEffect as useEffect7, useState as useState7 } from "react";
|
|
3064
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3065
|
+
var MARKS = {
|
|
3066
|
+
ok: { glyph: mark.ok, color: "green" },
|
|
3067
|
+
fail: { glyph: mark.fail, color: tone.danger },
|
|
3068
|
+
pending: { glyph: mark.pending, color: tone.notice },
|
|
3069
|
+
none: { glyph: "" }
|
|
3070
|
+
};
|
|
3071
|
+
function ReportRow({ line: line2, width }) {
|
|
3072
|
+
const { glyph, color } = MARKS[line2.mark];
|
|
3073
|
+
const head = line2.mark === "none" ? "" : `${glyph} `;
|
|
3074
|
+
const room = width - FRAME_CHROME - head.length;
|
|
3075
|
+
return /* @__PURE__ */ jsxs10(Text11, {
|
|
3076
|
+
children: [
|
|
3077
|
+
head ? /* @__PURE__ */ jsx11(Text11, {
|
|
3078
|
+
color,
|
|
3079
|
+
children: head
|
|
3080
|
+
}) : null,
|
|
3081
|
+
/* @__PURE__ */ jsx11(Text11, {
|
|
3082
|
+
children: clip(line2.text, room)
|
|
3083
|
+
}),
|
|
3084
|
+
line2.note ? /* @__PURE__ */ jsx11(Text11, {
|
|
3085
|
+
dimColor: true,
|
|
3086
|
+
children: clip(` ${line2.note}`, Math.max(room - line2.text.length, 0))
|
|
3087
|
+
}) : null
|
|
3088
|
+
]
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
function ReportScreen({
|
|
3092
|
+
title,
|
|
3093
|
+
load,
|
|
3094
|
+
onError,
|
|
3095
|
+
onReport,
|
|
3096
|
+
exitWhenDone = true
|
|
3097
|
+
}) {
|
|
3098
|
+
const { exit } = useApp6();
|
|
3099
|
+
const width = useFrameWidth();
|
|
3100
|
+
const [report, setReport] = useState7(null);
|
|
3101
|
+
useEffect7(() => {
|
|
3102
|
+
let live = true;
|
|
3103
|
+
load().then((result) => {
|
|
3104
|
+
if (!live)
|
|
3105
|
+
return;
|
|
3106
|
+
setReport(result);
|
|
3107
|
+
onReport?.(result);
|
|
3108
|
+
}).catch((error) => {
|
|
3109
|
+
if (live)
|
|
3110
|
+
onError(error);
|
|
3111
|
+
});
|
|
3112
|
+
return () => {
|
|
3113
|
+
live = false;
|
|
3114
|
+
};
|
|
3115
|
+
}, [load, onError, onReport]);
|
|
3116
|
+
useEffect7(() => {
|
|
3117
|
+
if (report && exitWhenDone)
|
|
3118
|
+
exit();
|
|
3119
|
+
}, [report, exit, exitWhenDone]);
|
|
3120
|
+
return /* @__PURE__ */ jsx11(Frame, {
|
|
3121
|
+
title,
|
|
3122
|
+
width,
|
|
3123
|
+
children: report ? /* @__PURE__ */ jsx11(Box9, {
|
|
3124
|
+
flexDirection: "column",
|
|
3125
|
+
children: report.lines.map((line2) => /* @__PURE__ */ jsx11(ReportRow, {
|
|
3126
|
+
line: line2,
|
|
3127
|
+
width
|
|
3128
|
+
}, `${line2.mark}:${line2.text}`))
|
|
3129
|
+
}) : /* @__PURE__ */ jsxs10(Text11, {
|
|
3130
|
+
children: [
|
|
3131
|
+
/* @__PURE__ */ jsx11(Spinner, {}),
|
|
3132
|
+
/* @__PURE__ */ jsx11(Text11, {
|
|
3133
|
+
dimColor: true,
|
|
3134
|
+
children: " Working..."
|
|
3135
|
+
})
|
|
3136
|
+
]
|
|
3137
|
+
})
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// src/shell/screens/home.tsx
|
|
3142
|
+
import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3143
|
+
function failure(title, error) {
|
|
3144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3145
|
+
return {
|
|
3146
|
+
title,
|
|
3147
|
+
lines: [{ mark: "fail", text: message }],
|
|
3148
|
+
json: { error: message }
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
function identityReport(who) {
|
|
3152
|
+
const label = (text) => text.padEnd(7);
|
|
3153
|
+
return {
|
|
3154
|
+
title: "whoami",
|
|
3155
|
+
lines: [
|
|
3156
|
+
{ mark: "none", text: `${label("user")}${who.user}` },
|
|
3157
|
+
{
|
|
3158
|
+
mark: "none",
|
|
3159
|
+
text: `${label("org")}${who.org ?? "none — set an active organization in the web UI"}`
|
|
3160
|
+
},
|
|
3161
|
+
{ mark: "none", text: `${label("server")}${who.server}` }
|
|
3162
|
+
],
|
|
3163
|
+
json: { ...who }
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
function HomeScreen({
|
|
3167
|
+
opts,
|
|
3168
|
+
server,
|
|
3169
|
+
openClient,
|
|
3170
|
+
onError,
|
|
3171
|
+
onSummary
|
|
3172
|
+
}) {
|
|
3173
|
+
const { exit } = useApp7();
|
|
3174
|
+
const width = useFrameWidth();
|
|
3175
|
+
const groups = useMemo3(() => paletteGroups(), []);
|
|
3176
|
+
const [query, setQuery] = useState8("");
|
|
3177
|
+
const [cursor, setCursor] = useState8(0);
|
|
3178
|
+
const [route, setRoute] = useState8(null);
|
|
3179
|
+
const [client, setClient] = useState8(null);
|
|
3180
|
+
const [refused, setRefused] = useState8(null);
|
|
3181
|
+
const rows2 = useMemo3(() => flattenCommands(filterCommands(groups, query)), [groups, query]);
|
|
3182
|
+
useEffect8(() => {
|
|
3183
|
+
setCursor((c) => Math.max(0, Math.min(c, rows2.length - 1)));
|
|
3184
|
+
}, [rows2.length]);
|
|
3185
|
+
const held = useRef(null);
|
|
3186
|
+
const connect = useCallback3(async () => {
|
|
3187
|
+
held.current ??= await openClient();
|
|
3188
|
+
return held.current;
|
|
3189
|
+
}, [openClient]);
|
|
3190
|
+
const answering = useCallback3((title, load) => ({
|
|
3191
|
+
kind: "report",
|
|
3192
|
+
title,
|
|
3193
|
+
load: async () => {
|
|
3194
|
+
try {
|
|
3195
|
+
return await load();
|
|
3196
|
+
} catch (error) {
|
|
3197
|
+
return failure(title, error);
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
}), []);
|
|
3201
|
+
const routeFor = useCallback3((value) => {
|
|
3202
|
+
switch (value) {
|
|
3203
|
+
case "catalog.bundles":
|
|
3204
|
+
case "catalog.skills":
|
|
3205
|
+
case "catalog.search":
|
|
3206
|
+
return { kind: "browse" };
|
|
3207
|
+
case "installed.list":
|
|
3208
|
+
return answering("installed", async () => installedReport(await nearestManifest(opts.scope)));
|
|
3209
|
+
case "installed.update":
|
|
3210
|
+
return {
|
|
3211
|
+
kind: "install",
|
|
3212
|
+
title: "update",
|
|
3213
|
+
stream: async function* stream() {
|
|
3214
|
+
const nearest = await nearestManifest(opts.scope);
|
|
3215
|
+
yield* updateTracked(await connect(), nearest.manifest, opts, true);
|
|
3216
|
+
}
|
|
3217
|
+
};
|
|
3218
|
+
case "installed.outdated":
|
|
3219
|
+
return answering("outdated", async () => outdatedReport(await connect(), await nearestManifest(opts.scope)));
|
|
3220
|
+
case "installed.remove":
|
|
3221
|
+
return answering("remove", async () => {
|
|
3222
|
+
const report = installedReport(await nearestManifest(opts.scope));
|
|
3223
|
+
return {
|
|
3224
|
+
...report,
|
|
3225
|
+
title: "remove",
|
|
3226
|
+
lines: [
|
|
3227
|
+
{
|
|
3228
|
+
mark: "none",
|
|
3229
|
+
text: "Run `yourskills remove <skill>` to delete one."
|
|
3230
|
+
},
|
|
3231
|
+
...report.lines
|
|
3232
|
+
]
|
|
3233
|
+
};
|
|
3234
|
+
});
|
|
3235
|
+
case "account.login":
|
|
3236
|
+
return { kind: "login" };
|
|
3237
|
+
case "account.logout":
|
|
3238
|
+
return answering("logout", async () => {
|
|
3239
|
+
await logout();
|
|
3240
|
+
held.current = null;
|
|
3241
|
+
setClient(null);
|
|
3242
|
+
return {
|
|
3243
|
+
title: "logout",
|
|
3244
|
+
lines: [{ mark: "ok", text: "Logged out." }],
|
|
3245
|
+
json: { ok: true }
|
|
3246
|
+
};
|
|
3247
|
+
});
|
|
3248
|
+
case "account.whoami":
|
|
3249
|
+
return answering("whoami", async () => identityReport(await whoami(server)));
|
|
3250
|
+
case "account.org":
|
|
3251
|
+
return answering("org", () => orgListReport(server));
|
|
3252
|
+
case "setup.scope":
|
|
3253
|
+
return answering("scope", () => configGet("scope"));
|
|
3254
|
+
case "setup.agents":
|
|
3255
|
+
return answering("agents", () => configGet("agents"));
|
|
3256
|
+
case "setup.doctor":
|
|
3257
|
+
return answering("doctor", async () => doctorReport(await nearestManifest(opts.scope), server));
|
|
3258
|
+
}
|
|
3259
|
+
}, [answering, connect, opts, server]);
|
|
3260
|
+
const activate = useCallback3(() => {
|
|
3261
|
+
const item = rows2[cursor];
|
|
3262
|
+
if (!item)
|
|
3263
|
+
return;
|
|
3264
|
+
item.onSelect?.();
|
|
3265
|
+
setRoute(routeFor(item.value));
|
|
3266
|
+
}, [rows2, cursor, routeFor]);
|
|
3267
|
+
const back = useCallback3(() => {
|
|
3268
|
+
setRoute(null);
|
|
3269
|
+
setRefused(null);
|
|
3270
|
+
}, []);
|
|
3271
|
+
useEffect8(() => {
|
|
3272
|
+
if (route?.kind !== "browse" || client || refused)
|
|
3273
|
+
return;
|
|
3274
|
+
let live = true;
|
|
3275
|
+
connect().then((opened) => {
|
|
3276
|
+
if (live)
|
|
3277
|
+
setClient(opened);
|
|
3278
|
+
}).catch((error) => {
|
|
3279
|
+
if (live)
|
|
3280
|
+
setRefused(error instanceof Error ? error.message : String(error));
|
|
3281
|
+
});
|
|
3282
|
+
return () => {
|
|
3283
|
+
live = false;
|
|
3284
|
+
};
|
|
3285
|
+
}, [route, client, refused, connect]);
|
|
3286
|
+
const browsing = route?.kind === "browse" && client !== null;
|
|
3287
|
+
useInput3((input, key) => {
|
|
3288
|
+
if (key.upArrow) {
|
|
3289
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
3290
|
+
} else if (key.downArrow) {
|
|
3291
|
+
setCursor((c) => Math.min(rows2.length - 1, c + 1));
|
|
3292
|
+
} else if (key.return) {
|
|
3293
|
+
activate();
|
|
3294
|
+
} else if (key.escape) {
|
|
3295
|
+
if (query.length > 0)
|
|
3296
|
+
setQuery("");
|
|
3297
|
+
else
|
|
3298
|
+
exit();
|
|
3299
|
+
} else if (key.backspace || key.delete) {
|
|
3300
|
+
setQuery((q) => q.slice(0, -1));
|
|
3301
|
+
setCursor(0);
|
|
3302
|
+
} else if (input && !key.ctrl && !key.meta) {
|
|
3303
|
+
setQuery((q) => q + input);
|
|
3304
|
+
setCursor(0);
|
|
3305
|
+
}
|
|
3306
|
+
}, { isActive: route === null });
|
|
3307
|
+
useInput3((_input, key) => {
|
|
3308
|
+
if (key.escape)
|
|
3309
|
+
back();
|
|
3310
|
+
}, { isActive: route !== null && !browsing });
|
|
3311
|
+
if (route?.kind === "login") {
|
|
3312
|
+
return /* @__PURE__ */ jsx12(LoginScreen, {
|
|
3313
|
+
onError,
|
|
3314
|
+
onDone: back
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
if (route?.kind === "report") {
|
|
3318
|
+
return /* @__PURE__ */ jsx12(ReportScreen, {
|
|
3319
|
+
title: route.title,
|
|
3320
|
+
load: route.load,
|
|
3321
|
+
onError,
|
|
3322
|
+
exitWhenDone: false
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
if (route?.kind === "install") {
|
|
3326
|
+
return /* @__PURE__ */ jsx12(Installer, {
|
|
3327
|
+
title: route.title,
|
|
3328
|
+
stream: route.stream,
|
|
3329
|
+
exitWhenDone: false,
|
|
3330
|
+
onDone: onSummary
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
if (route?.kind === "browse") {
|
|
3334
|
+
if (refused) {
|
|
3335
|
+
return /* @__PURE__ */ jsx12(Frame, {
|
|
3336
|
+
title: "browse",
|
|
3337
|
+
width,
|
|
3338
|
+
children: /* @__PURE__ */ jsxs11(Box10, {
|
|
3339
|
+
flexDirection: "column",
|
|
3340
|
+
children: [
|
|
3341
|
+
/* @__PURE__ */ jsxs11(Text12, {
|
|
3342
|
+
children: [
|
|
3343
|
+
/* @__PURE__ */ jsx12(Text12, {
|
|
3344
|
+
color: tone.danger,
|
|
3345
|
+
children: mark.fail
|
|
3346
|
+
}),
|
|
3347
|
+
/* @__PURE__ */ jsx12(Text12, {
|
|
3348
|
+
children: ` ${refused}`
|
|
3349
|
+
})
|
|
3350
|
+
]
|
|
3351
|
+
}),
|
|
3352
|
+
/* @__PURE__ */ jsx12(Text12, {
|
|
3353
|
+
dimColor: true,
|
|
3354
|
+
children: "Pick `login` from the palette, then try again."
|
|
3355
|
+
})
|
|
3356
|
+
]
|
|
3357
|
+
})
|
|
3358
|
+
});
|
|
3359
|
+
}
|
|
3360
|
+
if (!client) {
|
|
3361
|
+
return /* @__PURE__ */ jsx12(Frame, {
|
|
3362
|
+
title: "browse",
|
|
3363
|
+
width,
|
|
3364
|
+
children: /* @__PURE__ */ jsxs11(Text12, {
|
|
3365
|
+
children: [
|
|
3366
|
+
/* @__PURE__ */ jsx12(Spinner, {}),
|
|
3367
|
+
/* @__PURE__ */ jsx12(Text12, {
|
|
3368
|
+
dimColor: true,
|
|
3369
|
+
children: " Connecting..."
|
|
3370
|
+
})
|
|
3371
|
+
]
|
|
3372
|
+
})
|
|
3373
|
+
});
|
|
3374
|
+
}
|
|
3375
|
+
return /* @__PURE__ */ jsx12(BrowserScreen, {
|
|
3376
|
+
client,
|
|
3377
|
+
opts,
|
|
3378
|
+
onError,
|
|
3379
|
+
onSummary,
|
|
3380
|
+
onExit: back
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
return /* @__PURE__ */ jsxs11(Frame, {
|
|
3384
|
+
title: "yourskills",
|
|
3385
|
+
width,
|
|
3386
|
+
children: [
|
|
3387
|
+
/* @__PURE__ */ jsx12(CommandPalette, {
|
|
3388
|
+
groups,
|
|
3389
|
+
query,
|
|
3390
|
+
cursor,
|
|
3391
|
+
width
|
|
3392
|
+
}),
|
|
3393
|
+
/* @__PURE__ */ jsx12(Divider, {
|
|
3394
|
+
width
|
|
3395
|
+
}),
|
|
3396
|
+
/* @__PURE__ */ jsx12(Hints, {
|
|
3397
|
+
hints: [
|
|
3398
|
+
["^v", "move"],
|
|
3399
|
+
["enter", "open"],
|
|
3400
|
+
["esc", query ? "clear" : "quit"]
|
|
3401
|
+
]
|
|
3402
|
+
})
|
|
3403
|
+
]
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
// src/args.ts
|
|
3407
|
+
var HELP = `yourskills — install skills from your organization's registry
|
|
3408
|
+
|
|
3409
|
+
Usage: yourskills <command> [options]
|
|
3410
|
+
|
|
3411
|
+
Commands:
|
|
3412
|
+
(none) Open the command palette (every command, one keystroke away)
|
|
3413
|
+
browse Browse and install interactively
|
|
3414
|
+
login Device-flow login (RFC 8628)
|
|
3415
|
+
logout Forget the stored session token
|
|
3416
|
+
whoami Show the current user and active organization
|
|
3417
|
+
org List your organizations, marking the active one
|
|
3418
|
+
org <name> Switch the active organization
|
|
3419
|
+
list List every skill in your organization
|
|
3420
|
+
search <query> Search skills by name and description
|
|
3421
|
+
bundle <name> Install a role bundle (the default install path)
|
|
3422
|
+
add <skill...> Install individual skills
|
|
3423
|
+
install <skill...> The same thing, spelled the other way
|
|
3424
|
+
install Re-install everything the nearest manifest records
|
|
3425
|
+
remove <skill...> Delete skills from disk and from the manifest
|
|
3426
|
+
update <bundle> Re-install a bundle at its currently pinned refs
|
|
3427
|
+
update The same, for every bundle the manifest tracks
|
|
3428
|
+
installed List what the nearest manifest records
|
|
3429
|
+
outdated Report skills whose registry pin has moved
|
|
3430
|
+
doctor Check the server, the session, the agents, the manifest
|
|
3431
|
+
config get [key] Show the stored settings, or one of them
|
|
3432
|
+
config set <key> <val> Set one: scope, agents, server, track
|
|
3433
|
+
hooks status Show which agents carry our hooks, and where
|
|
3434
|
+
hooks install Register them: an update check, and usage if opted in
|
|
3435
|
+
hooks uninstall Remove ours, leaving every other hook alone
|
|
3436
|
+
|
|
3437
|
+
Options:
|
|
3438
|
+
-g, --global Install for the user (the default)
|
|
3439
|
+
-p, --project Install into this project, recorded in yourskills.json
|
|
3440
|
+
-a, --agent <name> Target agent (repeatable); omit to auto-detect
|
|
3441
|
+
--all update: refresh loose skills too, not only bundles
|
|
3442
|
+
-q, --quiet outdated: print nothing when everything is current
|
|
3443
|
+
--track hooks install: opt in to counting skill invocations
|
|
3444
|
+
--no-track hooks install: opt out again
|
|
3445
|
+
--server <url> Registry base URL for this invocation
|
|
3446
|
+
--plain Plain, greppable output; no frames, no colour
|
|
3447
|
+
--json Machine-readable output
|
|
3448
|
+
-v, --version Show the version
|
|
3449
|
+
-h, --help Show this help
|
|
3450
|
+
|
|
3451
|
+
Environment:
|
|
3452
|
+
YOURSKILLS_URL Registry base URL (default localhost:3000)
|
|
3453
|
+
YOURSKILLS_NO_TRACK Set to 1 to disable usage counting outright`;
|
|
3454
|
+
function configCommand(rest) {
|
|
3455
|
+
const [action, key, ...value] = rest;
|
|
3456
|
+
if (action === undefined || action === "get") {
|
|
3457
|
+
return { kind: "config", action: "get", ...key ? { key } : {} };
|
|
3458
|
+
}
|
|
3459
|
+
if (action !== "set") {
|
|
3460
|
+
return {
|
|
3461
|
+
kind: "error",
|
|
3462
|
+
message: `config takes get or set, not ${action}: yourskills config get [key]`
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
if (!key) {
|
|
3466
|
+
return {
|
|
3467
|
+
kind: "error",
|
|
3468
|
+
message: "config set needs a key: yourskills config set <key> <value>"
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
if (value.length === 0) {
|
|
3472
|
+
return {
|
|
3473
|
+
kind: "error",
|
|
3474
|
+
message: `config set needs a value: yourskills config set ${key} <value>`
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
return { kind: "config", action: "set", key, value: value.join(" ") };
|
|
3478
|
+
}
|
|
3479
|
+
function hooksCommand(rest, agent, track) {
|
|
3480
|
+
const action = rest[0] ?? "status";
|
|
3481
|
+
if (action !== "install" && action !== "uninstall" && action !== "status") {
|
|
3482
|
+
return {
|
|
3483
|
+
kind: "error",
|
|
3484
|
+
message: `hooks takes install, uninstall or status, not ${action}`
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
return {
|
|
3488
|
+
kind: "hooks",
|
|
3489
|
+
action,
|
|
3490
|
+
...agent ? { agent } : {},
|
|
3491
|
+
...track === undefined ? {} : { track }
|
|
3492
|
+
};
|
|
3493
|
+
}
|
|
3494
|
+
function parseArgv(args, interactive = true) {
|
|
3495
|
+
const opts = {};
|
|
3496
|
+
const positional = [];
|
|
3497
|
+
let plain = !interactive;
|
|
3498
|
+
let json = false;
|
|
3499
|
+
let all = false;
|
|
3500
|
+
let quiet = false;
|
|
3501
|
+
let server;
|
|
3502
|
+
let track;
|
|
3503
|
+
const done = (command2) => ({
|
|
3504
|
+
command: command2,
|
|
3505
|
+
output: { plain, json },
|
|
3506
|
+
...server ? { server } : {}
|
|
3507
|
+
});
|
|
3508
|
+
for (let i = 0;i < args.length; i++) {
|
|
3509
|
+
const arg = args[i];
|
|
3510
|
+
if (arg === "-h" || arg === "--help")
|
|
3511
|
+
return done({ kind: "help" });
|
|
3512
|
+
if (arg === "-v" || arg === "--version")
|
|
3513
|
+
return done({ kind: "version" });
|
|
3514
|
+
if (arg === "--plain") {
|
|
3515
|
+
plain = true;
|
|
3516
|
+
} else if (arg === "--json") {
|
|
3517
|
+
json = true;
|
|
3518
|
+
plain = true;
|
|
3519
|
+
} else if (arg === "-g" || arg === "--global") {
|
|
3520
|
+
opts.scope = "global";
|
|
3521
|
+
} else if (arg === "-p" || arg === "--project") {
|
|
3522
|
+
opts.scope = "project";
|
|
3523
|
+
} else if (arg === "--all") {
|
|
3524
|
+
all = true;
|
|
3525
|
+
} else if (arg === "-q" || arg === "--quiet") {
|
|
3526
|
+
quiet = true;
|
|
3527
|
+
} else if (arg === "--track") {
|
|
3528
|
+
track = true;
|
|
3529
|
+
} else if (arg === "--no-track") {
|
|
3530
|
+
track = false;
|
|
3531
|
+
} else if (arg === "--server") {
|
|
3532
|
+
const value = args[++i];
|
|
3533
|
+
if (!value)
|
|
3534
|
+
return done({ kind: "error", message: "--server requires a URL" });
|
|
3535
|
+
server = value;
|
|
3536
|
+
} else if (arg === "-a" || arg === "--agent") {
|
|
3537
|
+
const value = args[++i];
|
|
3538
|
+
if (!value)
|
|
3539
|
+
return done({
|
|
3540
|
+
kind: "error",
|
|
3541
|
+
message: "--agent requires an agent name"
|
|
3542
|
+
});
|
|
3543
|
+
opts.agents = [...opts.agents ?? [], value];
|
|
3544
|
+
} else if (arg.startsWith("-")) {
|
|
3545
|
+
return done({ kind: "error", message: `Unknown option: ${arg}` });
|
|
3546
|
+
} else {
|
|
3547
|
+
positional.push(arg);
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
const [command, ...rest] = positional;
|
|
3551
|
+
switch (command) {
|
|
3552
|
+
case undefined:
|
|
3553
|
+
return done(plain ? { kind: "help" } : { kind: "home" });
|
|
3554
|
+
case "help":
|
|
3555
|
+
return done({ kind: "help" });
|
|
3556
|
+
case "version":
|
|
3557
|
+
return done({ kind: "version" });
|
|
3558
|
+
case "browse":
|
|
3559
|
+
return done(plain ? {
|
|
3560
|
+
kind: "error",
|
|
3561
|
+
message: "browse needs a terminal; use `list` or `search` instead"
|
|
3562
|
+
} : { kind: "browse" });
|
|
3563
|
+
case "login":
|
|
3564
|
+
return done({ kind: "login" });
|
|
3565
|
+
case "logout":
|
|
3566
|
+
return done({ kind: "logout" });
|
|
3567
|
+
case "whoami":
|
|
3568
|
+
return done({ kind: "whoami" });
|
|
3569
|
+
case "list":
|
|
3570
|
+
return done({ kind: "list" });
|
|
3571
|
+
case "search":
|
|
3572
|
+
return done(rest.length > 0 ? { kind: "search", query: rest.join(" ") } : {
|
|
3573
|
+
kind: "error",
|
|
3574
|
+
message: "search needs a query: yourskills search <query>"
|
|
3575
|
+
});
|
|
3576
|
+
case "add":
|
|
3577
|
+
return done(rest.length > 0 ? { kind: "add", names: rest, opts } : {
|
|
3578
|
+
kind: "error",
|
|
3579
|
+
message: "add needs at least one skill: yourskills add <skill...>"
|
|
3580
|
+
});
|
|
3581
|
+
case "install":
|
|
3582
|
+
return done(rest.length > 0 ? { kind: "add", names: rest, opts } : { kind: "install", opts });
|
|
3583
|
+
case "remove":
|
|
3584
|
+
return done(rest.length > 0 ? { kind: "remove", names: rest, opts } : {
|
|
3585
|
+
kind: "error",
|
|
3586
|
+
message: "remove needs at least one skill: yourskills remove <skill...>"
|
|
3587
|
+
});
|
|
3588
|
+
case "installed":
|
|
3589
|
+
return done({ kind: "installed" });
|
|
3590
|
+
case "outdated":
|
|
3591
|
+
return done({ kind: "outdated", ...quiet ? { quiet: true } : {} });
|
|
3592
|
+
case "doctor":
|
|
3593
|
+
return done({ kind: "doctor" });
|
|
3594
|
+
case "org":
|
|
3595
|
+
return done(rest[0] ? { kind: "org", name: rest[0] } : { kind: "org" });
|
|
3596
|
+
case "config":
|
|
3597
|
+
return done(configCommand(rest));
|
|
3598
|
+
case "hooks":
|
|
3599
|
+
return done(hooksCommand(rest, opts.agents?.[0], track));
|
|
3600
|
+
case "hook": {
|
|
3601
|
+
const event = rest[0];
|
|
3602
|
+
return done(event && isHookEventName(event) ? { kind: "hook", event } : {
|
|
3603
|
+
kind: "error",
|
|
3604
|
+
message: "hook takes session-start or post-tool-use"
|
|
3605
|
+
});
|
|
3606
|
+
}
|
|
3607
|
+
case "bundle":
|
|
3608
|
+
return done(rest[0] ? { kind: "bundle", name: rest[0], opts } : {
|
|
3609
|
+
kind: "error",
|
|
3610
|
+
message: "bundle needs a name: yourskills bundle <name>"
|
|
3611
|
+
});
|
|
3612
|
+
case "update":
|
|
3613
|
+
return done({
|
|
3614
|
+
kind: "update",
|
|
3615
|
+
...rest[0] ? { bundle: rest[0] } : {},
|
|
3616
|
+
...all ? { all: true } : {},
|
|
3617
|
+
opts
|
|
3618
|
+
});
|
|
3619
|
+
default:
|
|
3620
|
+
return done({ kind: "error", message: `Unknown command: ${command}` });
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
// src/client.ts
|
|
3625
|
+
import { createTRPCClient as createTRPCClient2, httpBatchLink as httpBatchLink2 } from "@trpc/client";
|
|
3626
|
+
async function createClient2(server) {
|
|
3627
|
+
const { token, base } = await requireToken(server);
|
|
3628
|
+
return createTRPCClient2({
|
|
3629
|
+
links: [
|
|
3630
|
+
httpBatchLink2({
|
|
3631
|
+
url: `${base.replace(/\/$/, "")}/trpc`,
|
|
3632
|
+
headers: () => ({ authorization: `Bearer ${token}` })
|
|
3633
|
+
})
|
|
3634
|
+
]
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
// src/screens.tsx
|
|
3639
|
+
import { Box as Box11, Text as Text13, useApp as useApp8 } from "ink";
|
|
3640
|
+
import { useEffect as useEffect9, useState as useState9 } from "react";
|
|
3641
|
+
import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3642
|
+
var HELP_COLUMN = 26;
|
|
3643
|
+
function WhoamiScreen({
|
|
3644
|
+
load,
|
|
3645
|
+
onError
|
|
3646
|
+
}) {
|
|
3647
|
+
const { exit } = useApp8();
|
|
3648
|
+
const width = useFrameWidth();
|
|
3649
|
+
const [who, setWho] = useState9(null);
|
|
3650
|
+
useEffect9(() => {
|
|
3651
|
+
let live = true;
|
|
3652
|
+
load().then((result) => {
|
|
3653
|
+
if (!live)
|
|
3654
|
+
return;
|
|
3655
|
+
setWho(result);
|
|
3656
|
+
exit();
|
|
3657
|
+
}).catch((error) => {
|
|
3658
|
+
if (live)
|
|
3659
|
+
onError(error);
|
|
3660
|
+
});
|
|
3661
|
+
return () => {
|
|
3662
|
+
live = false;
|
|
3663
|
+
};
|
|
3664
|
+
}, [load, exit, onError]);
|
|
3665
|
+
if (!who) {
|
|
3666
|
+
return /* @__PURE__ */ jsx13(Frame, {
|
|
3667
|
+
title: "whoami",
|
|
3668
|
+
width,
|
|
3669
|
+
children: /* @__PURE__ */ jsxs12(Text13, {
|
|
3670
|
+
children: [
|
|
3671
|
+
/* @__PURE__ */ jsx13(Spinner, {}),
|
|
3672
|
+
/* @__PURE__ */ jsx13(Text13, {
|
|
3673
|
+
dimColor: true,
|
|
3674
|
+
children: " Checking session..."
|
|
3675
|
+
})
|
|
3676
|
+
]
|
|
3677
|
+
})
|
|
3678
|
+
});
|
|
3679
|
+
}
|
|
3680
|
+
return /* @__PURE__ */ jsx13(Frame, {
|
|
3681
|
+
title: "whoami",
|
|
3682
|
+
width,
|
|
3683
|
+
children: /* @__PURE__ */ jsxs12(Box11, {
|
|
3684
|
+
flexDirection: "column",
|
|
3685
|
+
children: [
|
|
3686
|
+
/* @__PURE__ */ jsxs12(Text13, {
|
|
3687
|
+
children: [
|
|
3688
|
+
/* @__PURE__ */ jsx13(Text13, {
|
|
3689
|
+
dimColor: true,
|
|
3690
|
+
children: "user "
|
|
3691
|
+
}),
|
|
3692
|
+
/* @__PURE__ */ jsx13(Text13, {
|
|
3693
|
+
children: who.user
|
|
3694
|
+
})
|
|
3695
|
+
]
|
|
3696
|
+
}),
|
|
3697
|
+
/* @__PURE__ */ jsxs12(Text13, {
|
|
3698
|
+
children: [
|
|
3699
|
+
/* @__PURE__ */ jsx13(Text13, {
|
|
3700
|
+
dimColor: true,
|
|
3701
|
+
children: "org "
|
|
3702
|
+
}),
|
|
3703
|
+
who.org ? /* @__PURE__ */ jsx13(Text13, {
|
|
3704
|
+
children: who.org
|
|
3705
|
+
}) : /* @__PURE__ */ jsx13(Text13, {
|
|
3706
|
+
color: tone.notice,
|
|
3707
|
+
children: "none — set an active organization in the web UI"
|
|
3708
|
+
})
|
|
3709
|
+
]
|
|
3710
|
+
}),
|
|
3711
|
+
/* @__PURE__ */ jsxs12(Text13, {
|
|
3712
|
+
children: [
|
|
3713
|
+
/* @__PURE__ */ jsx13(Text13, {
|
|
3714
|
+
dimColor: true,
|
|
3715
|
+
children: "server "
|
|
3716
|
+
}),
|
|
3717
|
+
/* @__PURE__ */ jsx13(Text13, {
|
|
3718
|
+
color: tone.primary,
|
|
3719
|
+
children: who.server
|
|
3720
|
+
})
|
|
3721
|
+
]
|
|
3722
|
+
})
|
|
3723
|
+
]
|
|
3724
|
+
})
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
function NoticeScreen({
|
|
3728
|
+
title,
|
|
3729
|
+
message
|
|
3730
|
+
}) {
|
|
3731
|
+
const { exit } = useApp8();
|
|
3732
|
+
const width = useFrameWidth();
|
|
3733
|
+
useEffect9(exit, [exit]);
|
|
3734
|
+
return /* @__PURE__ */ jsx13(Frame, {
|
|
3735
|
+
title,
|
|
3736
|
+
width,
|
|
3737
|
+
children: /* @__PURE__ */ jsxs12(Text13, {
|
|
3738
|
+
children: [
|
|
3739
|
+
/* @__PURE__ */ jsx13(Text13, {
|
|
3740
|
+
color: "green",
|
|
3741
|
+
children: mark.ok
|
|
3742
|
+
}),
|
|
3743
|
+
/* @__PURE__ */ jsx13(Text13, {
|
|
3744
|
+
children: ` ${message}`
|
|
3745
|
+
})
|
|
3746
|
+
]
|
|
3747
|
+
})
|
|
3748
|
+
});
|
|
3749
|
+
}
|
|
3750
|
+
var MARKS2 = {
|
|
3751
|
+
ok: { glyph: mark.ok, color: "green" },
|
|
3752
|
+
fail: { glyph: mark.fail, color: tone.danger },
|
|
3753
|
+
pending: { glyph: mark.pending, color: tone.notice },
|
|
3754
|
+
none: { glyph: "" }
|
|
3755
|
+
};
|
|
3756
|
+
function ReportRow2({ line: line2, width }) {
|
|
3757
|
+
const { glyph, color } = MARKS2[line2.mark];
|
|
3758
|
+
const head = line2.mark === "none" ? "" : `${glyph} `;
|
|
3759
|
+
const room = width - FRAME_CHROME - head.length;
|
|
3760
|
+
return /* @__PURE__ */ jsxs12(Text13, {
|
|
3761
|
+
children: [
|
|
3762
|
+
head ? /* @__PURE__ */ jsx13(Text13, {
|
|
3763
|
+
color,
|
|
3764
|
+
children: head
|
|
3765
|
+
}) : null,
|
|
3766
|
+
/* @__PURE__ */ jsx13(Text13, {
|
|
3767
|
+
children: clip(line2.text, room)
|
|
3768
|
+
}),
|
|
3769
|
+
line2.note ? /* @__PURE__ */ jsx13(Text13, {
|
|
3770
|
+
dimColor: true,
|
|
3771
|
+
children: clip(` ${line2.note}`, Math.max(room - line2.text.length, 0))
|
|
3772
|
+
}) : null
|
|
3773
|
+
]
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
function ReportScreen2({
|
|
3777
|
+
title,
|
|
3778
|
+
load,
|
|
3779
|
+
onError,
|
|
3780
|
+
onReport,
|
|
3781
|
+
exitWhenDone = true
|
|
3782
|
+
}) {
|
|
3783
|
+
const { exit } = useApp8();
|
|
3784
|
+
const width = useFrameWidth();
|
|
3785
|
+
const [report, setReport] = useState9(null);
|
|
3786
|
+
useEffect9(() => {
|
|
3787
|
+
let live = true;
|
|
3788
|
+
load().then((result) => {
|
|
3789
|
+
if (!live)
|
|
3790
|
+
return;
|
|
3791
|
+
setReport(result);
|
|
3792
|
+
onReport?.(result);
|
|
3793
|
+
}).catch((error) => {
|
|
3794
|
+
if (live)
|
|
3795
|
+
onError(error);
|
|
3796
|
+
});
|
|
3797
|
+
return () => {
|
|
3798
|
+
live = false;
|
|
3799
|
+
};
|
|
3800
|
+
}, [load, onError, onReport]);
|
|
3801
|
+
useEffect9(() => {
|
|
3802
|
+
if (report && exitWhenDone)
|
|
3803
|
+
exit();
|
|
3804
|
+
}, [report, exit, exitWhenDone]);
|
|
3805
|
+
return /* @__PURE__ */ jsx13(Frame, {
|
|
3806
|
+
title,
|
|
3807
|
+
width,
|
|
3808
|
+
children: report ? /* @__PURE__ */ jsx13(Box11, {
|
|
3809
|
+
flexDirection: "column",
|
|
3810
|
+
children: report.lines.map((line2) => /* @__PURE__ */ jsx13(ReportRow2, {
|
|
3811
|
+
line: line2,
|
|
3812
|
+
width
|
|
3813
|
+
}, `${line2.mark}:${line2.text}`))
|
|
3814
|
+
}) : /* @__PURE__ */ jsxs12(Text13, {
|
|
3815
|
+
children: [
|
|
3816
|
+
/* @__PURE__ */ jsx13(Spinner, {}),
|
|
3817
|
+
/* @__PURE__ */ jsx13(Text13, {
|
|
3818
|
+
dimColor: true,
|
|
3819
|
+
children: " Working..."
|
|
3820
|
+
})
|
|
3821
|
+
]
|
|
3822
|
+
})
|
|
3823
|
+
});
|
|
3824
|
+
}
|
|
3825
|
+
function numberedLines(text) {
|
|
3826
|
+
return text.split(`
|
|
3827
|
+
`).map((line2, i) => ({ id: `${i}:${line2}`, line: line2 }));
|
|
3828
|
+
}
|
|
3829
|
+
function HelpScreen({ text }) {
|
|
3830
|
+
const { exit } = useApp8();
|
|
3831
|
+
const width = useFrameWidth();
|
|
3832
|
+
useEffect9(exit, [exit]);
|
|
3833
|
+
return /* @__PURE__ */ jsx13(Frame, {
|
|
3834
|
+
title: "yourskills",
|
|
3835
|
+
width,
|
|
3836
|
+
children: /* @__PURE__ */ jsx13(Box11, {
|
|
3837
|
+
flexDirection: "column",
|
|
3838
|
+
children: numberedLines(text).map(({ id, line: line2 }) => {
|
|
3839
|
+
if (line2.length === 0)
|
|
3840
|
+
return /* @__PURE__ */ jsx13(Blank, {}, id);
|
|
3841
|
+
if (!line2.startsWith(" "))
|
|
3842
|
+
return /* @__PURE__ */ jsx13(Text13, {
|
|
3843
|
+
children: clip(line2, width - FRAME_CHROME)
|
|
3844
|
+
}, id);
|
|
3845
|
+
return /* @__PURE__ */ jsxs12(Text13, {
|
|
3846
|
+
children: [
|
|
3847
|
+
/* @__PURE__ */ jsx13(Text13, {
|
|
3848
|
+
color: tone.primary,
|
|
3849
|
+
children: line2.slice(0, HELP_COLUMN)
|
|
3850
|
+
}),
|
|
3851
|
+
/* @__PURE__ */ jsx13(Text13, {
|
|
3852
|
+
dimColor: true,
|
|
3853
|
+
children: clip(line2.slice(HELP_COLUMN), width - FRAME_CHROME - HELP_COLUMN)
|
|
3854
|
+
})
|
|
3855
|
+
]
|
|
3856
|
+
}, line2);
|
|
3857
|
+
})
|
|
3858
|
+
})
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3861
|
+
// package.json
|
|
3862
|
+
var package_default = {
|
|
3863
|
+
name: "yourskills",
|
|
3864
|
+
version: "0.1.0",
|
|
3865
|
+
description: "Browse, install and keep up to date the agent skills and bundles your team publishes on yourskills.",
|
|
3866
|
+
license: "MIT",
|
|
3867
|
+
homepage: "https://github.com/xiduzo/yourskills/tree/main/apps/cli#readme",
|
|
3868
|
+
repository: {
|
|
3869
|
+
type: "git",
|
|
3870
|
+
url: "git+https://github.com/xiduzo/yourskills.git",
|
|
3871
|
+
directory: "apps/cli"
|
|
3872
|
+
},
|
|
3873
|
+
keywords: [
|
|
3874
|
+
"agent-skills",
|
|
3875
|
+
"skills",
|
|
3876
|
+
"cli",
|
|
3877
|
+
"claude-code",
|
|
3878
|
+
"codex",
|
|
3879
|
+
"cursor",
|
|
3880
|
+
"opencode",
|
|
3881
|
+
"ai"
|
|
3882
|
+
],
|
|
3883
|
+
type: "module",
|
|
3884
|
+
bin: {
|
|
3885
|
+
yourskills: "dist/main.js"
|
|
3886
|
+
},
|
|
3887
|
+
files: [
|
|
3888
|
+
"dist"
|
|
3889
|
+
],
|
|
3890
|
+
engines: {
|
|
3891
|
+
node: ">=22.20.0"
|
|
3892
|
+
},
|
|
3893
|
+
publishConfig: {
|
|
3894
|
+
access: "public"
|
|
3895
|
+
},
|
|
3896
|
+
scripts: {
|
|
3897
|
+
"check-types": "tsc --noEmit",
|
|
3898
|
+
build: "bun run build.ts",
|
|
3899
|
+
"build:binary": "bun run build.ts --binary",
|
|
3900
|
+
prepublishOnly: "bun run build"
|
|
3901
|
+
},
|
|
3902
|
+
dependencies: {
|
|
3903
|
+
"@trpc/client": "^11.18.0",
|
|
3904
|
+
"@trpc/server": "^11.18.0",
|
|
3905
|
+
ink: "^7.1.1",
|
|
3906
|
+
react: "^19.2.8",
|
|
3907
|
+
skills: "^1.5.25"
|
|
3908
|
+
},
|
|
3909
|
+
devDependencies: {
|
|
3910
|
+
"@types/bun": "catalog:",
|
|
3911
|
+
"@types/react": "catalog:",
|
|
3912
|
+
"@yourskills/api": "workspace:*",
|
|
3913
|
+
"@yourskills/bundles": "workspace:*",
|
|
3914
|
+
"@yourskills/config": "workspace:*",
|
|
3915
|
+
"@yourskills/ui": "workspace:*",
|
|
3916
|
+
"ink-testing-library": "^4.0.0",
|
|
3917
|
+
typescript: "catalog:"
|
|
3918
|
+
}
|
|
3919
|
+
};
|
|
3920
|
+
|
|
3921
|
+
// src/version.ts
|
|
3922
|
+
var VERSION = package_default.version ?? "0.0.0-dev";
|
|
3923
|
+
|
|
3924
|
+
// src/main.tsx
|
|
3925
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
3926
|
+
function applySummary(summary) {
|
|
3927
|
+
if (summary.pending.length > 0 || summary.failed > 0)
|
|
3928
|
+
process.exitCode = 1;
|
|
3929
|
+
}
|
|
3930
|
+
var PLAIN_MARKS = {
|
|
3931
|
+
ok: mark.ok,
|
|
3932
|
+
fail: mark.fail,
|
|
3933
|
+
pending: mark.pending,
|
|
3934
|
+
none: ""
|
|
3935
|
+
};
|
|
3936
|
+
function emit(report, output) {
|
|
3937
|
+
if (report.failed)
|
|
3938
|
+
process.exitCode = 1;
|
|
3939
|
+
if (output.json) {
|
|
3940
|
+
console.log(JSON.stringify(report.json));
|
|
3941
|
+
return;
|
|
3942
|
+
}
|
|
3943
|
+
if (report.silent)
|
|
3944
|
+
return;
|
|
3945
|
+
for (const line2 of report.lines)
|
|
3946
|
+
console.log(plainLine(line2, PLAIN_MARKS));
|
|
3947
|
+
}
|
|
3948
|
+
function toRows(skills) {
|
|
3949
|
+
return skills.map((s) => ({
|
|
3950
|
+
name: s.name,
|
|
3951
|
+
description: s.description,
|
|
3952
|
+
status: s.status
|
|
3953
|
+
}));
|
|
3954
|
+
}
|
|
3955
|
+
function catalogReport(title, skills) {
|
|
3956
|
+
if (skills.length === 0) {
|
|
3957
|
+
return {
|
|
3958
|
+
title,
|
|
3959
|
+
lines: [{ mark: "none", text: "No skills found." }],
|
|
3960
|
+
json: { skills: [] }
|
|
3961
|
+
};
|
|
3962
|
+
}
|
|
3963
|
+
const width = Math.max(...skills.map((s) => s.name.length));
|
|
3964
|
+
return {
|
|
3965
|
+
title,
|
|
3966
|
+
lines: skills.map((skill) => {
|
|
3967
|
+
const tag = lifecycleTag(skill.status);
|
|
3968
|
+
return {
|
|
3969
|
+
mark: "none",
|
|
3970
|
+
text: `${skill.name.padEnd(width)}${tag ? ` ${tag}` : ""} ${skill.description ?? ""}`
|
|
3971
|
+
};
|
|
3972
|
+
}),
|
|
3973
|
+
json: { skills }
|
|
3974
|
+
};
|
|
3975
|
+
}
|
|
3976
|
+
async function resolveInstallOptions(opts) {
|
|
3977
|
+
const { scope, manifestPath } = await resolveScope({ flag: opts.scope });
|
|
3978
|
+
const path = scope === "project" && manifestPath ? manifestPath : manifestPathFor(scope);
|
|
3979
|
+
return {
|
|
3980
|
+
scope,
|
|
3981
|
+
agents: await resolveAgents(opts.agents),
|
|
3982
|
+
manifest: { path, scope }
|
|
3983
|
+
};
|
|
3984
|
+
}
|
|
3985
|
+
var INVITES_HOOKS = new Set([
|
|
3986
|
+
"login",
|
|
3987
|
+
"add",
|
|
3988
|
+
"bundle",
|
|
3989
|
+
"install",
|
|
3990
|
+
"update",
|
|
3991
|
+
"remove",
|
|
3992
|
+
"browse",
|
|
3993
|
+
"home"
|
|
3994
|
+
]);
|
|
3995
|
+
async function prepare(command) {
|
|
3996
|
+
switch (command.kind) {
|
|
3997
|
+
case "add":
|
|
3998
|
+
case "bundle":
|
|
3999
|
+
case "update":
|
|
4000
|
+
case "install":
|
|
4001
|
+
case "remove":
|
|
4002
|
+
return { ...command, opts: await resolveInstallOptions(command.opts) };
|
|
4003
|
+
default:
|
|
4004
|
+
return command;
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
function nearestFor(opts) {
|
|
4008
|
+
return nearestManifest(opts.scope);
|
|
4009
|
+
}
|
|
4010
|
+
function nothingTracked(nearest, kind, all = false) {
|
|
4011
|
+
const has = kind === "install" ? Object.keys(nearest.manifest.skills).length > 0 : Object.keys(nearest.manifest.bundles).length > 0 || all && looseSkills(nearest.manifest).length > 0;
|
|
4012
|
+
if (has)
|
|
4013
|
+
return null;
|
|
4014
|
+
return {
|
|
4015
|
+
title: kind,
|
|
4016
|
+
lines: [
|
|
4017
|
+
{
|
|
4018
|
+
mark: "none",
|
|
4019
|
+
text: kind === "install" ? "Nothing recorded to install. Run `yourskills bundle <name>` first." : "No bundles tracked here. Run `yourskills bundle <name>` first.",
|
|
4020
|
+
note: nearest.path
|
|
4021
|
+
}
|
|
4022
|
+
],
|
|
4023
|
+
json: { manifest: nearest.path, skills: [], bundles: [] }
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
async function runPlain(command, output, server) {
|
|
4027
|
+
switch (command.kind) {
|
|
4028
|
+
case "help":
|
|
4029
|
+
console.log(HELP);
|
|
4030
|
+
return;
|
|
4031
|
+
case "version":
|
|
4032
|
+
console.log(VERSION);
|
|
4033
|
+
return;
|
|
4034
|
+
case "home":
|
|
4035
|
+
case "browse":
|
|
4036
|
+
case "error":
|
|
4037
|
+
throw new Error(command.kind === "error" ? command.message : `${command.kind === "home" ? "the palette" : "browse"} needs a terminal`);
|
|
4038
|
+
case "login":
|
|
4039
|
+
throw new Error("login needs a terminal. Sign in on a machine that has one; the token is then copied from ~/.config/yourskills/config.json.");
|
|
4040
|
+
case "logout":
|
|
4041
|
+
await logout();
|
|
4042
|
+
console.log("Logged out.");
|
|
4043
|
+
return;
|
|
4044
|
+
case "whoami": {
|
|
4045
|
+
const who = await whoami(server);
|
|
4046
|
+
console.log(`${who.user} (${who.server})`);
|
|
4047
|
+
console.log(`org: ${who.org ?? "none \u2014 set an active organization in the web UI"}`);
|
|
4048
|
+
return;
|
|
4049
|
+
}
|
|
4050
|
+
case "list":
|
|
4051
|
+
case "search": {
|
|
4052
|
+
const client = await createClient2(server);
|
|
4053
|
+
const skills = command.kind === "list" ? await client.skills.list.query({}) : await client.skills.search.query({ q: command.query });
|
|
4054
|
+
emit(catalogReport(command.kind === "list" ? "skills" : `search: ${command.query}`, skills), output);
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
case "add":
|
|
4058
|
+
applySummary(await installPlain(install(await createClient2(server), command.names, command.opts)));
|
|
4059
|
+
return;
|
|
4060
|
+
case "bundle":
|
|
4061
|
+
applySummary(await installPlain(installBundle(await createClient2(server), command.name, command.opts)));
|
|
4062
|
+
return;
|
|
4063
|
+
case "install": {
|
|
4064
|
+
const nearest = await nearestFor(command.opts);
|
|
4065
|
+
const empty = nothingTracked(nearest, "install");
|
|
4066
|
+
if (empty) {
|
|
4067
|
+
emit(empty, output);
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
applySummary(await installPlain(reinstall(nearest.manifest, command.opts)));
|
|
4071
|
+
return;
|
|
4072
|
+
}
|
|
4073
|
+
case "update": {
|
|
4074
|
+
if (command.bundle) {
|
|
4075
|
+
applySummary(await installPlain(update(await createClient2(server), command.bundle, command.opts)));
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
const nearest = await nearestFor(command.opts);
|
|
4079
|
+
const empty = nothingTracked(nearest, "update", command.all ?? false);
|
|
4080
|
+
if (empty) {
|
|
4081
|
+
emit(empty, output);
|
|
4082
|
+
return;
|
|
4083
|
+
}
|
|
4084
|
+
applySummary(await installPlain(updateTracked(await createClient2(server), nearest.manifest, command.opts, command.all ?? false)));
|
|
4085
|
+
return;
|
|
4086
|
+
}
|
|
4087
|
+
case "remove": {
|
|
4088
|
+
const nearest = await nearestFor(command.opts);
|
|
4089
|
+
emit(await removeSkills(command.names, nearest.manifest, nearest.path, command.opts), output);
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
case "installed":
|
|
4093
|
+
emit(installedReport(await nearestManifest()), output);
|
|
4094
|
+
return;
|
|
4095
|
+
case "outdated":
|
|
4096
|
+
emit(await outdatedReport(await createClient2(server), await nearestManifest(), command.quiet ?? false), output);
|
|
4097
|
+
return;
|
|
4098
|
+
case "doctor":
|
|
4099
|
+
emit(await doctorReport(await nearestManifest(), server), output);
|
|
4100
|
+
return;
|
|
4101
|
+
case "org":
|
|
4102
|
+
emit(command.name ? await orgSwitchReport(command.name, server) : await orgListReport(server), output);
|
|
4103
|
+
return;
|
|
4104
|
+
case "config":
|
|
4105
|
+
emit(command.action === "get" ? await configGet(command.key) : await configSet(command.key, command.value), output);
|
|
4106
|
+
return;
|
|
4107
|
+
case "hooks":
|
|
4108
|
+
emit(await hooksReport(command), output);
|
|
4109
|
+
return;
|
|
4110
|
+
case "hook":
|
|
4111
|
+
await runHook(command.event);
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
function hooksReport(command) {
|
|
4116
|
+
if (command.action === "install")
|
|
4117
|
+
return hooksInstall({
|
|
4118
|
+
...command.agent ? { agent: command.agent } : {},
|
|
4119
|
+
...command.track === undefined ? {} : { track: command.track }
|
|
4120
|
+
});
|
|
4121
|
+
if (command.action === "uninstall")
|
|
4122
|
+
return hooksUninstall(command.agent ? { agent: command.agent } : {});
|
|
4123
|
+
return hooksStatus();
|
|
4124
|
+
}
|
|
4125
|
+
async function screenFor(command, fail, summarise, report, server) {
|
|
4126
|
+
const gate = (title, build) => /* @__PURE__ */ jsx14(AuthGate, {
|
|
4127
|
+
title,
|
|
4128
|
+
build,
|
|
4129
|
+
connect: () => createClient2(server),
|
|
4130
|
+
onError: fail
|
|
4131
|
+
});
|
|
4132
|
+
const local = (title, load) => /* @__PURE__ */ jsx14(ReportScreen2, {
|
|
4133
|
+
title,
|
|
4134
|
+
load,
|
|
4135
|
+
onError: fail,
|
|
4136
|
+
onReport: report
|
|
4137
|
+
});
|
|
4138
|
+
switch (command.kind) {
|
|
4139
|
+
case "help":
|
|
4140
|
+
return /* @__PURE__ */ jsx14(HelpScreen, {
|
|
4141
|
+
text: HELP
|
|
4142
|
+
});
|
|
4143
|
+
case "version":
|
|
4144
|
+
return /* @__PURE__ */ jsx14(NoticeScreen, {
|
|
4145
|
+
title: "version",
|
|
4146
|
+
message: VERSION
|
|
4147
|
+
});
|
|
4148
|
+
case "error":
|
|
4149
|
+
throw new Error(command.message);
|
|
4150
|
+
case "login":
|
|
4151
|
+
return /* @__PURE__ */ jsx14(LoginScreen, {
|
|
4152
|
+
onError: fail
|
|
4153
|
+
});
|
|
4154
|
+
case "logout":
|
|
4155
|
+
await logout();
|
|
4156
|
+
return /* @__PURE__ */ jsx14(NoticeScreen, {
|
|
4157
|
+
title: "logout",
|
|
4158
|
+
message: "Logged out."
|
|
4159
|
+
});
|
|
4160
|
+
case "whoami":
|
|
4161
|
+
return gate("whoami", () => /* @__PURE__ */ jsx14(WhoamiScreen, {
|
|
4162
|
+
load: () => whoami(server),
|
|
4163
|
+
onError: fail
|
|
4164
|
+
}));
|
|
4165
|
+
case "list":
|
|
4166
|
+
return gate("skills", (client) => /* @__PURE__ */ jsx14(CatalogScreen, {
|
|
4167
|
+
title: "skills",
|
|
4168
|
+
load: async () => toRows(await client.skills.list.query({})),
|
|
4169
|
+
onError: fail
|
|
4170
|
+
}));
|
|
4171
|
+
case "search": {
|
|
4172
|
+
const q = command.query;
|
|
4173
|
+
return gate(`search: ${q}`, (client) => /* @__PURE__ */ jsx14(CatalogScreen, {
|
|
4174
|
+
title: `search: ${q}`,
|
|
4175
|
+
load: async () => toRows(await client.skills.search.query({ q })),
|
|
4176
|
+
onError: fail
|
|
4177
|
+
}));
|
|
4178
|
+
}
|
|
4179
|
+
case "home": {
|
|
4180
|
+
const opts = await resolveInstallOptions({});
|
|
4181
|
+
return /* @__PURE__ */ jsx14(HomeScreen, {
|
|
4182
|
+
opts,
|
|
4183
|
+
server,
|
|
4184
|
+
openClient: () => createClient2(server),
|
|
4185
|
+
onError: fail,
|
|
4186
|
+
onSummary: summarise
|
|
4187
|
+
});
|
|
4188
|
+
}
|
|
4189
|
+
case "browse": {
|
|
4190
|
+
const opts = await resolveInstallOptions({});
|
|
4191
|
+
return gate("browse", (client) => /* @__PURE__ */ jsx14(BrowserScreen, {
|
|
4192
|
+
client,
|
|
4193
|
+
opts,
|
|
4194
|
+
onError: fail,
|
|
4195
|
+
onSummary: summarise
|
|
4196
|
+
}));
|
|
4197
|
+
}
|
|
4198
|
+
case "add":
|
|
4199
|
+
return gate("add", (client) => /* @__PURE__ */ jsx14(Installer, {
|
|
4200
|
+
title: "add",
|
|
4201
|
+
stream: () => install(client, command.names, command.opts),
|
|
4202
|
+
onDone: summarise
|
|
4203
|
+
}));
|
|
4204
|
+
case "bundle":
|
|
4205
|
+
return gate(`bundle: ${command.name}`, (client) => /* @__PURE__ */ jsx14(Installer, {
|
|
4206
|
+
title: `bundle: ${command.name}`,
|
|
4207
|
+
stream: () => installBundle(client, command.name, command.opts),
|
|
4208
|
+
onDone: summarise
|
|
4209
|
+
}));
|
|
4210
|
+
case "install": {
|
|
4211
|
+
const nearest = await nearestFor(command.opts);
|
|
4212
|
+
const empty = nothingTracked(nearest, "install");
|
|
4213
|
+
if (empty)
|
|
4214
|
+
return local("install", async () => empty);
|
|
4215
|
+
return /* @__PURE__ */ jsx14(Installer, {
|
|
4216
|
+
title: "install",
|
|
4217
|
+
stream: () => reinstall(nearest.manifest, command.opts),
|
|
4218
|
+
onDone: summarise
|
|
4219
|
+
});
|
|
4220
|
+
}
|
|
4221
|
+
case "update": {
|
|
4222
|
+
if (command.bundle) {
|
|
4223
|
+
const name = command.bundle;
|
|
4224
|
+
return gate(`update: ${name}`, (client) => /* @__PURE__ */ jsx14(Installer, {
|
|
4225
|
+
title: `update: ${name}`,
|
|
4226
|
+
stream: () => update(client, name, command.opts),
|
|
4227
|
+
onDone: summarise
|
|
4228
|
+
}));
|
|
4229
|
+
}
|
|
4230
|
+
const nearest = await nearestFor(command.opts);
|
|
4231
|
+
const empty = nothingTracked(nearest, "update", command.all ?? false);
|
|
4232
|
+
if (empty)
|
|
4233
|
+
return local("update", async () => empty);
|
|
4234
|
+
return gate("update", (client) => /* @__PURE__ */ jsx14(Installer, {
|
|
4235
|
+
title: "update",
|
|
4236
|
+
stream: () => updateTracked(client, nearest.manifest, command.opts, command.all ?? false),
|
|
4237
|
+
onDone: summarise
|
|
4238
|
+
}));
|
|
4239
|
+
}
|
|
4240
|
+
case "remove": {
|
|
4241
|
+
const nearest = await nearestFor(command.opts);
|
|
4242
|
+
return local("remove", () => removeSkills(command.names, nearest.manifest, nearest.path, command.opts));
|
|
4243
|
+
}
|
|
4244
|
+
case "installed":
|
|
4245
|
+
return local("installed", async () => installedReport(await nearestManifest()));
|
|
4246
|
+
case "doctor":
|
|
4247
|
+
return local("doctor", async () => doctorReport(await nearestManifest(), server));
|
|
4248
|
+
case "config":
|
|
4249
|
+
return local("config", () => command.action === "get" ? configGet(command.key) : configSet(command.key, command.value));
|
|
4250
|
+
case "hooks":
|
|
4251
|
+
return local("hooks", () => hooksReport(command));
|
|
4252
|
+
case "hook":
|
|
4253
|
+
throw new Error("hooks are not interactive");
|
|
4254
|
+
case "outdated":
|
|
4255
|
+
return gate("outdated", (client) => /* @__PURE__ */ jsx14(ReportScreen2, {
|
|
4256
|
+
title: "outdated",
|
|
4257
|
+
load: async () => outdatedReport(client, await nearestManifest(), command.quiet),
|
|
4258
|
+
onError: fail,
|
|
4259
|
+
onReport: report
|
|
4260
|
+
}));
|
|
4261
|
+
case "org":
|
|
4262
|
+
return gate("org", () => /* @__PURE__ */ jsx14(ReportScreen2, {
|
|
4263
|
+
title: "org",
|
|
4264
|
+
load: () => command.name ? orgSwitchReport(command.name, server) : orgListReport(server),
|
|
4265
|
+
onError: fail,
|
|
4266
|
+
onReport: report
|
|
4267
|
+
}));
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
async function runInk(command, server) {
|
|
4271
|
+
let failure2 = null;
|
|
4272
|
+
const node = await screenFor(command, (error) => {
|
|
4273
|
+
failure2 = error;
|
|
4274
|
+
instance?.unmount();
|
|
4275
|
+
}, applySummary, (report) => {
|
|
4276
|
+
if (report.failed)
|
|
4277
|
+
process.exitCode = 1;
|
|
4278
|
+
}, server);
|
|
4279
|
+
const instance = render(node);
|
|
4280
|
+
await instance.waitUntilExit();
|
|
4281
|
+
if (failure2)
|
|
4282
|
+
throw failure2;
|
|
4283
|
+
}
|
|
4284
|
+
function isEntrypoint() {
|
|
4285
|
+
const entry = process.argv[1];
|
|
4286
|
+
if (!entry)
|
|
4287
|
+
return false;
|
|
4288
|
+
try {
|
|
4289
|
+
return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
|
|
4290
|
+
} catch {
|
|
4291
|
+
return false;
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
if (isEntrypoint()) {
|
|
4295
|
+
const interactive = Boolean(process.stdout.isTTY);
|
|
4296
|
+
const parsed = parseArgv(process.argv.slice(2), interactive);
|
|
4297
|
+
const { output, server } = parsed;
|
|
4298
|
+
try {
|
|
4299
|
+
const command = await prepare(parsed.command);
|
|
4300
|
+
if (command.kind === "hook") {
|
|
4301
|
+
await runHook(command.event);
|
|
4302
|
+
} else {
|
|
4303
|
+
await (output.plain ? runPlain(command, output, server) : runInk(command, server));
|
|
4304
|
+
await flushQuietly(server);
|
|
4305
|
+
if (INVITES_HOOKS.has(command.kind))
|
|
4306
|
+
for (const line2 of await ensureHooksRegistered())
|
|
4307
|
+
console.log(line2);
|
|
4308
|
+
}
|
|
4309
|
+
} catch (error) {
|
|
4310
|
+
console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
|
|
4311
|
+
process.exit(1);
|
|
4312
|
+
}
|
|
4313
|
+
}
|