yourskills 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +2016 -1596
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -61,1128 +61,1436 @@ function buildHelp(lines) {
61
61
  return `${PREAMBLE}${block}${REST}`;
62
62
  }
63
63
 
64
- // src/auth/auth.ts
65
- import { setTimeout as sleep } from "node:timers/promises";
64
+ // src/auth/screens/gate.tsx
65
+ import { Box as Box6, Text as Text8, useApp as useApp2, useInput as useInput3 } from "ink";
66
+ import {
67
+ useCallback,
68
+ useEffect as useEffect3,
69
+ useMemo,
70
+ useState as useState4
71
+ } from "react";
66
72
 
67
- // src/settings/agents.ts
68
- import { readFileSync } from "node:fs";
69
- import { homedir as homedir2 } from "node:os";
70
- import { join as join2 } from "node:path";
73
+ // src/ui/back.tsx
74
+ import { useInput } from "ink";
71
75
 
72
- // src/settings/store.ts
73
- import { chmodSync, mkdirSync, rmSync } from "node:fs";
74
- import { readFile, writeFile } from "node:fs/promises";
75
- import { homedir } from "node:os";
76
- import { join } from "node:path";
77
- var DEFAULT_URL = "https://api.yourskills.store";
78
- function configDir() {
79
- const xdg = process.env.XDG_CONFIG_HOME;
80
- return join(xdg && xdg.length > 0 ? xdg : join(homedir(), ".config"), "yourskills");
76
+ // ../../packages/ui/src/lib/ascii.ts
77
+ var DEFAULT_ASCII_CHARS = {
78
+ top: "-",
79
+ bottom: "-",
80
+ left: "|",
81
+ right: "|",
82
+ divider: "-",
83
+ junction: "+"
84
+ };
85
+ function fill(n, seq = "-") {
86
+ const count = Math.max(n, 0);
87
+ if (count === 0) {
88
+ return "";
89
+ }
90
+ const unit = seq.length > 0 ? seq : " ";
91
+ return unit.repeat(Math.ceil(count / unit.length)).slice(0, count);
81
92
  }
82
- function configPath() {
83
- return join(configDir(), "config.json");
93
+ function junctionGlyph(chars) {
94
+ return chars.junction.length > 0 ? chars.junction[0] : DEFAULT_ASCII_CHARS.junction;
84
95
  }
85
- async function readConfig() {
86
- try {
87
- return JSON.parse(await readFile(configPath(), "utf8"));
88
- } catch {
89
- return {};
96
+ function topBorder(width, title, chars = DEFAULT_ASCII_CHARS) {
97
+ const j = junctionGlyph(chars);
98
+ if (!title) {
99
+ return `${j}${fill(width - 2, chars.top)}${j}`;
90
100
  }
101
+ const label = ` ${title} `;
102
+ const left = 2;
103
+ const right = Math.max(width - 2 - left - label.length, 1);
104
+ return `${j}${fill(left, chars.top)}${label}${fill(right, chars.top)}${j}`;
91
105
  }
92
- async function updateConfig(change) {
93
- let current = {};
94
- try {
95
- current = JSON.parse(await readFile(configPath(), "utf8"));
96
- } catch (error) {
97
- if (error?.code !== "ENOENT")
98
- return null;
99
- }
100
- const next = change(current);
101
- await writeConfig(next);
102
- return next;
106
+
107
+ // src/ui/frame.tsx
108
+ import { Box, Text, useWindowSize } from "ink";
109
+
110
+ // src/ui/theme.ts
111
+ var tone = {
112
+ primary: "cyan",
113
+ notice: "yellow",
114
+ danger: "red"
115
+ };
116
+ var stateTone = {
117
+ "state-active": undefined,
118
+ "state-experimental": "yellow",
119
+ "state-deprecated": "red"
120
+ };
121
+ var mark = {
122
+ ok: "+",
123
+ warn: "!",
124
+ fail: "x",
125
+ pending: ".",
126
+ none: "",
127
+ cursor: ">",
128
+ selected: "*",
129
+ unselected: " "
130
+ };
131
+
132
+ // src/ui/frame.tsx
133
+ import { jsx, jsxs } from "react/jsx-runtime";
134
+ var MAX_WIDTH = 96;
135
+ var MIN_WIDTH = 44;
136
+ var FRAME_CHROME = 4;
137
+ function useFrameWidth() {
138
+ const { columns } = useWindowSize();
139
+ return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, columns - 2));
103
140
  }
104
- async function writeConfig(config) {
105
- mkdirSync(configDir(), { recursive: true, mode: 448 });
106
- await writeFile(configPath(), `${JSON.stringify(config, null, 2)}
107
- `);
108
- chmodSync(configPath(), 384);
141
+ var j = DEFAULT_ASCII_CHARS.junction;
142
+ var BORDER = {
143
+ topLeft: j,
144
+ top: DEFAULT_ASCII_CHARS.top,
145
+ topRight: j,
146
+ right: DEFAULT_ASCII_CHARS.right,
147
+ bottomRight: j,
148
+ bottom: DEFAULT_ASCII_CHARS.bottom,
149
+ bottomLeft: j,
150
+ left: DEFAULT_ASCII_CHARS.left
151
+ };
152
+ function Frame({
153
+ title,
154
+ width,
155
+ children
156
+ }) {
157
+ return /* @__PURE__ */ jsxs(Box, {
158
+ flexDirection: "column",
159
+ width,
160
+ children: [
161
+ /* @__PURE__ */ jsx(Text, {
162
+ color: tone.primary,
163
+ children: topBorder(width, title)
164
+ }),
165
+ /* @__PURE__ */ jsx(Box, {
166
+ flexDirection: "column",
167
+ width,
168
+ paddingLeft: 1,
169
+ paddingRight: 1,
170
+ borderStyle: BORDER,
171
+ borderColor: tone.primary,
172
+ borderTop: false,
173
+ children
174
+ })
175
+ ]
176
+ });
109
177
  }
110
- function clearConfig() {
111
- rmSync(configPath(), { force: true });
178
+ function Divider({ width }) {
179
+ return /* @__PURE__ */ jsx(Box, {
180
+ marginLeft: -1,
181
+ marginRight: -1,
182
+ children: /* @__PURE__ */ jsx(Text, {
183
+ color: tone.primary,
184
+ children: fill(width - 2, DEFAULT_ASCII_CHARS.divider)
185
+ })
186
+ });
112
187
  }
113
- function serverUrl(config = {}, override) {
114
- return override || process.env.YOURSKILLS_URL || config.serverUrl || DEFAULT_URL;
188
+ function Blank() {
189
+ return /* @__PURE__ */ jsx(Text, {
190
+ children: " "
191
+ });
115
192
  }
116
-
117
- // src/settings/agents.ts
118
- function stockLockPath() {
119
- const state = process.env.XDG_STATE_HOME;
120
- if (state && state.length > 0)
121
- return join2(state, "skills", ".skill-lock.json");
122
- const home = process.env.HOME;
123
- return join2(home && home.length > 0 ? home : homedir2(), ".agents", ".skill-lock.json");
193
+ function clip(text, width) {
194
+ if (width <= 0)
195
+ return "";
196
+ if (text.length <= width)
197
+ return text;
198
+ return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}~`;
124
199
  }
125
- var LOCK_VERSION = 3;
126
- function rememberedAgents() {
127
- let lock;
128
- try {
129
- lock = JSON.parse(readFileSync(stockLockPath(), "utf8"));
130
- } catch {
131
- return [];
200
+
201
+ // ../../packages/skills/src/lifecycle.ts
202
+ var LIFECYCLES = [
203
+ { status: "active", word: "active", glyph: "#", tone: "state-active" },
204
+ {
205
+ status: "experimental",
206
+ word: "experimental",
207
+ glyph: "?",
208
+ tone: "state-experimental"
209
+ },
210
+ {
211
+ status: "deprecated",
212
+ word: "deprecated",
213
+ glyph: "x",
214
+ tone: "state-deprecated"
132
215
  }
133
- if (!lock || typeof lock !== "object")
134
- return [];
135
- const raw = lock;
136
- if (typeof raw.version !== "number" || raw.version < LOCK_VERSION)
137
- return [];
138
- if (!raw.skills)
139
- return [];
140
- return Array.isArray(raw.lastSelectedAgents) ? raw.lastSelectedAgents.filter((a) => typeof a === "string") : [];
216
+ ];
217
+ var LIFECYCLE_ORDER = LIFECYCLES.map((lifecycle) => lifecycle.status);
218
+ var BY_STATUS = new Map(LIFECYCLES.map((lifecycle) => [lifecycle.status, lifecycle]));
219
+ function lifecycleOf(status) {
220
+ const lifecycle = BY_STATUS.get(status);
221
+ if (!lifecycle) {
222
+ throw new Error(`No lifecycle vocabulary for "${status}". Add it to LIFECYCLES.`);
223
+ }
224
+ return lifecycle;
141
225
  }
142
- async function resolveAgents(flagAgents) {
143
- if (flagAgents && flagAgents.length > 0)
144
- return flagAgents;
145
- const config = await readConfig();
146
- if (config.agents?.length)
147
- return config.agents;
148
- return rememberedAgents();
226
+ // src/ui/rows.tsx
227
+ import { Box as Box2, Text as Text2 } from "ink";
228
+ import { jsx as jsx2, jsxs as jsxs2, Fragment } from "react/jsx-runtime";
229
+ function lifecycleTag(status) {
230
+ if (!status || status === "active")
231
+ return "";
232
+ return `[${lifecycleOf(status).word}]`;
149
233
  }
150
- // src/tracking/answer.ts
151
- function trackingDisabledByEnv() {
152
- const value = process.env.YOURSKILLS_NO_TRACK;
153
- return value !== undefined && value !== "" && value !== "0";
234
+ function lifecycleColor(status) {
235
+ return stateTone[lifecycleOf(status ?? "active").tone];
154
236
  }
155
- function trackingOn(config) {
156
- return !trackingDisabledByEnv() && config.track !== false;
237
+ var GUTTER = 4;
238
+ var GAP = 2;
239
+ function nameWidth(rows, max = 28) {
240
+ return Math.min(max, Math.max(8, ...rows.map((r) => r.name.length)));
157
241
  }
158
- var ON_NOTE = "on; turn off with `yourskills config set track false`";
159
- function trackingAnswer(question) {
160
- const { track, configPath, organization } = question;
161
- if (trackingDisabledByEnv()) {
162
- return {
163
- state: "off-here",
164
- sending: false,
165
- note: "off (YOURSKILLS_NO_TRACK is set)",
166
- value: "off",
167
- from: "YOURSKILLS_NO_TRACK in the environment"
168
- };
169
- }
170
- if (track === false) {
171
- return {
172
- state: "off-here",
173
- sending: false,
174
- note: "off; turn on with `yourskills config set track true`",
175
- value: "off",
176
- from: `track in ${configPath}`
177
- };
178
- }
179
- if (organization?.enabled === false) {
180
- return {
181
- state: "off-for-organization",
182
- sending: true,
183
- note: "off; your organization turned usage tracking off for everyone",
184
- value: "off",
185
- from: "usage tracking in your organization's settings"
186
- };
187
- }
188
- return {
189
- state: "on",
190
- sending: true,
191
- note: ON_NOTE,
192
- value: "on",
193
- from: track === true ? `track in ${configPath}` : "built-in default: turn off with `config set track false`"
194
- };
242
+ function SkillRow({
243
+ row,
244
+ width,
245
+ names,
246
+ cursor,
247
+ selected,
248
+ selectable = false
249
+ }) {
250
+ const tag = lifecycleTag(row.status);
251
+ const meta = row.count !== undefined ? `${row.count} skills` : tag;
252
+ const gutter = selectable ? GUTTER : 0;
253
+ const rest = width - gutter - names - GAP - (meta ? meta.length + GAP : 0);
254
+ return /* @__PURE__ */ jsxs2(Box2, {
255
+ children: [
256
+ selectable ? /* @__PURE__ */ jsx2(Text2, {
257
+ color: cursor ? "cyan" : undefined,
258
+ children: `${cursor ? mark.cursor : " "} ${selected ? mark.selected : mark.unselected} `
259
+ }) : null,
260
+ /* @__PURE__ */ jsx2(Text2, {
261
+ bold: cursor,
262
+ color: lifecycleColor(row.status),
263
+ children: clip(row.name, names).padEnd(names)
264
+ }),
265
+ /* @__PURE__ */ jsx2(Text2, {
266
+ children: " ".repeat(GAP)
267
+ }),
268
+ /* @__PURE__ */ jsx2(Text2, {
269
+ dimColor: true,
270
+ children: clip(row.description ?? "", Math.max(rest, 0))
271
+ }),
272
+ meta ? /* @__PURE__ */ jsxs2(Fragment, {
273
+ children: [
274
+ /* @__PURE__ */ jsx2(Text2, {
275
+ children: " ".repeat(GAP)
276
+ }),
277
+ /* @__PURE__ */ jsx2(Text2, {
278
+ color: lifecycleColor(row.status),
279
+ dimColor: !tag,
280
+ children: meta
281
+ })
282
+ ]
283
+ }) : null
284
+ ]
285
+ });
195
286
  }
196
- async function trackingNow(question) {
197
- const here = trackingAnswer({
198
- track: question.track,
199
- configPath: question.configPath
287
+ function Hints({ hints }) {
288
+ return /* @__PURE__ */ jsx2(Box2, {
289
+ children: hints.map(([key, label], i) => /* @__PURE__ */ jsxs2(Text2, {
290
+ children: [
291
+ i > 0 ? /* @__PURE__ */ jsx2(Text2, {
292
+ dimColor: true,
293
+ children: " "
294
+ }) : null,
295
+ /* @__PURE__ */ jsx2(Text2, {
296
+ color: "cyan",
297
+ children: key
298
+ }),
299
+ /* @__PURE__ */ jsx2(Text2, {
300
+ dimColor: true,
301
+ children: ` ${label}`
302
+ })
303
+ ]
304
+ }, key))
200
305
  });
201
- if (!here.sending)
202
- return here;
203
- return trackingAnswer({
204
- track: question.track,
205
- configPath: question.configPath,
206
- organization: await question.askOrganization()
306
+ }
307
+
308
+ // src/ui/back.tsx
309
+ import { jsx as jsx3, jsxs as jsxs3, Fragment as Fragment2 } from "react/jsx-runtime";
310
+ function Back({
311
+ width,
312
+ onBack,
313
+ extra = []
314
+ }) {
315
+ useInput((input, key) => {
316
+ if (key.escape || input === "x")
317
+ onBack?.();
318
+ }, { isActive: Boolean(onBack) });
319
+ if (!onBack)
320
+ return null;
321
+ return /* @__PURE__ */ jsxs3(Fragment2, {
322
+ children: [
323
+ /* @__PURE__ */ jsx3(Divider, {
324
+ width
325
+ }),
326
+ /* @__PURE__ */ jsx3(Hints, {
327
+ hints: [...extra, ["esc/x", "back"]]
328
+ })
329
+ ]
207
330
  });
208
331
  }
209
- // src/settings/keys.ts
210
- var CONFIG_KEYS = [
211
- "scope",
212
- "agents",
213
- "server",
214
- "track",
215
- "autoUpdate"
216
- ];
217
- function isConfigKey(key) {
218
- return CONFIG_KEYS.includes(key);
332
+ // src/ui/command.tsx
333
+ import { Box as Box3, Text as Text4 } from "ink";
334
+
335
+ // src/ui/text-input.tsx
336
+ import { Text as Text3 } from "ink";
337
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
338
+ function SearchInput({
339
+ value,
340
+ placeholder = "",
341
+ focus = true
342
+ }) {
343
+ return /* @__PURE__ */ jsxs4(Text3, {
344
+ children: [
345
+ value.length > 0 ? /* @__PURE__ */ jsx4(Text3, {
346
+ children: value
347
+ }) : /* @__PURE__ */ jsx4(Text3, {
348
+ dimColor: true,
349
+ children: placeholder
350
+ }),
351
+ focus ? /* @__PURE__ */ jsx4(Text3, {
352
+ color: tone.primary,
353
+ children: "_"
354
+ }) : null
355
+ ]
356
+ });
219
357
  }
220
- var REDACTED = "<redacted>";
221
- function unknownAgents(asked, valid) {
222
- const unknown = asked.filter((name) => !valid.includes(name));
223
- const suggest = valid.filter((name) => unknown.some((miss) => near(miss, name)));
224
- return { unknown, suggest };
358
+
359
+ // src/ui/viewport.ts
360
+ import { useWindowSize as useWindowSize2 } from "ink";
361
+ var MIN_ROWS = 3;
362
+ var MAX_ROWS = 20;
363
+ function useViewport({
364
+ cursor,
365
+ total,
366
+ chrome
367
+ }) {
368
+ const { rows } = useWindowSize2();
369
+ const size = Math.max(MIN_ROWS, Math.min(MAX_ROWS, rows - chrome));
370
+ const first = Math.max(0, Math.min(cursor - Math.floor(size / 2), total - size));
371
+ return { first, size };
225
372
  }
226
- function near(a, b) {
227
- if (a === b)
228
- return false;
229
- if (a.includes(b) || b.includes(a))
230
- return true;
231
- if (Math.abs(a.length - b.length) > 1)
232
- return false;
233
- const [short, long] = a.length <= b.length ? [a, b] : [b, a];
234
- let i = 0;
235
- let j = 0;
236
- let slack = 1;
237
- while (i < short.length && j < long.length) {
238
- if (short[i] === long[j]) {
239
- i++;
240
- j++;
241
- continue;
242
- }
243
- if (slack-- === 0)
244
- return false;
245
- if (short.length === long.length)
246
- i++;
247
- j++;
248
- }
249
- return true;
373
+ function position(cursor, total) {
374
+ return `${total > 0 ? cursor + 1 : 0}/${total}`;
250
375
  }
251
- var UNSET = "(unset)";
252
- function configValue(config, key) {
253
- switch (key) {
254
- case "scope":
255
- return config.defaultScope ?? UNSET;
256
- case "agents":
257
- return config.agents?.length ? config.agents.join(",") : UNSET;
258
- case "server":
259
- return config.serverUrl ?? UNSET;
260
- case "track":
261
- return String(trackingOn(config));
262
- case "autoUpdate":
263
- return String(config.autoUpdate !== false);
264
- }
376
+
377
+ // src/ui/command.tsx
378
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
379
+ function filterCommands(groups, query) {
380
+ if (query.length === 0)
381
+ return groups;
382
+ const q = query.toLowerCase();
383
+ return groups.map((group) => ({
384
+ group: group.group,
385
+ items: group.items.filter((item) => item.label.toLowerCase().includes(q) || (item.hint ?? "").toLowerCase().includes(q))
386
+ })).filter((group) => group.items.length > 0);
265
387
  }
266
- function setConfigValue(config, key, value) {
267
- if (!isConfigKey(key)) {
268
- return {
269
- ok: false,
270
- message: `Unknown config key: ${key}. Known keys: ${CONFIG_KEYS.join(", ")}.`
271
- };
272
- }
273
- switch (key) {
274
- case "scope": {
275
- if (value !== "global" && value !== "project") {
276
- return {
277
- ok: false,
278
- message: "scope must be `global` or `project`."
279
- };
280
- }
281
- return {
282
- ok: true,
283
- config: { ...config, defaultScope: value },
284
- shown: value
285
- };
286
- }
287
- case "agents": {
288
- const agents = value.split(",").map((a) => a.trim()).filter((a) => a.length > 0);
289
- if (agents.length === 0) {
290
- return {
291
- ok: false,
292
- message: "agents must be a comma separated list, for example `claude-code,codex`."
293
- };
294
- }
295
- return {
296
- ok: true,
297
- config: { ...config, agents },
298
- shown: agents.join(",")
299
- };
300
- }
301
- case "server": {
302
- let url;
303
- try {
304
- url = new URL(value);
305
- } catch {
306
- return {
307
- ok: false,
308
- message: `server must be a URL, for example \`https://skills.acme.dev\`. Got: ${value}`
309
- };
310
- }
311
- if (url.protocol !== "http:" && url.protocol !== "https:") {
312
- return { ok: false, message: "server must be an http or https URL." };
313
- }
314
- const serverUrl = url.toString().replace(/\/$/, "");
315
- return { ok: true, config: { ...config, serverUrl }, shown: serverUrl };
316
- }
317
- case "track": {
318
- if (value !== "true" && value !== "false") {
319
- return { ok: false, message: "track must be `true` or `false`." };
320
- }
321
- const track = value === "true";
322
- return { ok: true, config: { ...config, track }, shown: String(track) };
323
- }
324
- case "autoUpdate": {
325
- if (value !== "true" && value !== "false") {
326
- return { ok: false, message: "autoUpdate must be `true` or `false`." };
327
- }
328
- const autoUpdate = value === "true";
329
- return {
330
- ok: true,
331
- config: { ...config, autoUpdate },
332
- shown: String(autoUpdate)
333
- };
334
- }
388
+ function flattenCommands(groups) {
389
+ return groups.flatMap((group) => group.items);
390
+ }
391
+ function windowGroups(groups, first, count) {
392
+ const out = [];
393
+ let index = 0;
394
+ for (const group of groups) {
395
+ const items = group.items.filter((_, i) => {
396
+ const row = index + i;
397
+ return row >= first && row < first + count;
398
+ });
399
+ index += group.items.length;
400
+ if (items.length > 0)
401
+ out.push({ group: group.group, items });
335
402
  }
403
+ return out;
336
404
  }
337
- function configLines(config) {
338
- const width = Math.max(...CONFIG_KEYS.map((k) => k.length), "user".length);
339
- const lines = CONFIG_KEYS.map((key) => ({
340
- mark: "none",
341
- text: `${key.padEnd(width)} ${configValue(config, key)}`
342
- }));
343
- lines.push({
344
- mark: "none",
345
- text: `${"user".padEnd(width)} ${config.userEmail ?? UNSET}`
405
+ var POINTER = 2;
406
+ var GAP2 = 2;
407
+ var LABEL_CAP = 22;
408
+ function valueColumn(items) {
409
+ return Math.min(items.reduce((wide, item) => Math.max(wide, item.label.length), 0), LABEL_CAP);
410
+ }
411
+ function CommandRow({
412
+ item,
413
+ width,
414
+ column,
415
+ active
416
+ }) {
417
+ const hint = item.hint ?? "";
418
+ const room = width - POINTER - (hint ? hint.length + GAP2 : 0);
419
+ const label = clip(item.label, Math.max(room, 0));
420
+ const shows = item.shows ?? "";
421
+ const pad = Math.max(column - label.length, 0) + GAP2;
422
+ const value = shows && label.length + pad + shows.length <= room ? shows : "";
423
+ const left = label.length + (value ? pad + value.length : 0);
424
+ const gap = Math.max(width - POINTER - left - hint.length, hint ? 1 : 0);
425
+ return /* @__PURE__ */ jsxs5(Text4, {
426
+ children: [
427
+ /* @__PURE__ */ jsx5(Text4, {
428
+ color: tone.primary,
429
+ children: `${active ? mark.cursor : " "} `
430
+ }),
431
+ /* @__PURE__ */ jsx5(Text4, {
432
+ bold: active,
433
+ color: active ? tone.primary : undefined,
434
+ children: label
435
+ }),
436
+ value ? /* @__PURE__ */ jsx5(Text4, {
437
+ children: `${" ".repeat(pad)}${value}`
438
+ }) : null,
439
+ /* @__PURE__ */ jsx5(Text4, {
440
+ dimColor: true,
441
+ children: `${" ".repeat(gap)}${hint}`
442
+ })
443
+ ]
346
444
  });
347
- lines.push({
348
- mark: "none",
349
- text: `${"token".padEnd(width)} ${config.token ? REDACTED : UNSET}`
445
+ }
446
+ function CommandPalette({
447
+ groups,
448
+ query,
449
+ cursor,
450
+ width,
451
+ chrome = 11,
452
+ placeholder = "type a command or search",
453
+ emptyMessage = "No matching commands."
454
+ }) {
455
+ const visible = filterCommands(groups, query);
456
+ const rows = flattenCommands(visible);
457
+ const current = rows[cursor];
458
+ const interior = width - FRAME_CHROME;
459
+ const column = valueColumn(rows);
460
+ const { first, size } = useViewport({
461
+ cursor,
462
+ total: rows.length,
463
+ chrome: chrome + Math.max(0, visible.length * 2 - 1)
464
+ });
465
+ const paged = windowGroups(visible, first, size);
466
+ return /* @__PURE__ */ jsxs5(Box3, {
467
+ flexDirection: "column",
468
+ children: [
469
+ /* @__PURE__ */ jsxs5(Box3, {
470
+ children: [
471
+ /* @__PURE__ */ jsx5(Text4, {
472
+ color: tone.primary,
473
+ children: "> "
474
+ }),
475
+ /* @__PURE__ */ jsx5(SearchInput, {
476
+ value: query,
477
+ placeholder
478
+ })
479
+ ]
480
+ }),
481
+ /* @__PURE__ */ jsx5(Divider, {
482
+ width
483
+ }),
484
+ rows.length === 0 ? /* @__PURE__ */ jsx5(Text4, {
485
+ dimColor: true,
486
+ children: emptyMessage
487
+ }) : paged.map((group, i) => /* @__PURE__ */ jsxs5(Box3, {
488
+ flexDirection: "column",
489
+ children: [
490
+ i > 0 ? /* @__PURE__ */ jsx5(Divider, {
491
+ width
492
+ }) : null,
493
+ /* @__PURE__ */ jsx5(Text4, {
494
+ dimColor: true,
495
+ children: group.group.toUpperCase()
496
+ }),
497
+ group.items.map((item) => /* @__PURE__ */ jsx5(CommandRow, {
498
+ item,
499
+ width: interior,
500
+ column,
501
+ active: item.value === current?.value
502
+ }, item.value))
503
+ ]
504
+ }, group.group))
505
+ ]
350
506
  });
351
- return lines;
352
507
  }
353
- function configGetReport(config, key, path) {
354
- if (key === undefined) {
355
- return {
356
- title: "config",
357
- lines: configLines(config),
358
- json: {
359
- path,
360
- ...Object.fromEntries(CONFIG_KEYS.map((k) => [k, configValue(config, k)])),
361
- user: config.userEmail ?? null,
362
- token: config.token ? REDACTED : null
363
- }
364
- };
365
- }
366
- if (!isConfigKey(key)) {
367
- return {
368
- title: "config",
369
- lines: [
370
- {
371
- mark: "fail",
372
- text: `Unknown config key: ${key}`,
373
- note: `Known keys: ${CONFIG_KEYS.join(", ")}`
374
- }
375
- ],
376
- json: { path, error: `unknown key: ${key}`, keys: CONFIG_KEYS },
377
- failed: true
378
- };
379
- }
380
- const value = configValue(config, key);
381
- return {
382
- title: "config",
383
- lines: [{ mark: "none", text: value }],
384
- json: { path, key, value }
508
+ // src/ui/confirm.tsx
509
+ import { Box as Box4, Text as Text5, useInput as useInput2 } from "ink";
510
+ import { useState } from "react";
511
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
512
+ function ConfirmScreen({
513
+ title,
514
+ headline,
515
+ lines,
516
+ notes = [],
517
+ yes,
518
+ no,
519
+ argv,
520
+ onYes,
521
+ onNo
522
+ }) {
523
+ const width = useFrameWidth();
524
+ const [choice, setChoice] = useState("yes");
525
+ useInput2((input, key) => {
526
+ if (key.upArrow || key.downArrow) {
527
+ setChoice((c) => c === "yes" ? "no" : "yes");
528
+ } else if (key.return) {
529
+ if (choice === "yes")
530
+ onYes();
531
+ else
532
+ onNo();
533
+ } else if (key.escape || input === "x") {
534
+ onNo();
535
+ }
536
+ });
537
+ const interior = width - FRAME_CHROME;
538
+ const row = (label, active, hint) => {
539
+ const room = interior - 2 - (hint ? hint.length + 2 : 0);
540
+ const text = clip(label, Math.max(room, 0));
541
+ const gap = Math.max(interior - 2 - text.length - (hint?.length ?? 0), 1);
542
+ return /* @__PURE__ */ jsxs6(Text5, {
543
+ children: [
544
+ /* @__PURE__ */ jsx6(Text5, {
545
+ color: tone.primary,
546
+ children: `${active ? mark.cursor : " "} `
547
+ }),
548
+ /* @__PURE__ */ jsx6(Text5, {
549
+ bold: active,
550
+ color: active ? tone.primary : undefined,
551
+ children: text
552
+ }),
553
+ hint ? /* @__PURE__ */ jsx6(Text5, {
554
+ dimColor: true,
555
+ children: `${" ".repeat(gap)}${hint}`
556
+ }) : null
557
+ ]
558
+ });
385
559
  };
560
+ return /* @__PURE__ */ jsxs6(Frame, {
561
+ title,
562
+ width,
563
+ children: [
564
+ /* @__PURE__ */ jsxs6(Box4, {
565
+ flexDirection: "column",
566
+ children: [
567
+ /* @__PURE__ */ jsx6(Text5, {
568
+ color: tone.notice,
569
+ children: clip(headline, interior)
570
+ }),
571
+ /* @__PURE__ */ jsx6(Text5, {
572
+ children: " "
573
+ }),
574
+ lines.map((line) => /* @__PURE__ */ jsxs6(Text5, {
575
+ children: [
576
+ /* @__PURE__ */ jsx6(Text5, {
577
+ color: line.mark === "fail" ? tone.danger : undefined,
578
+ children: `${mark[line.mark] || " "} `
579
+ }),
580
+ /* @__PURE__ */ jsx6(Text5, {
581
+ children: clip(line.text, interior - 2)
582
+ }),
583
+ line.note ? /* @__PURE__ */ jsx6(Text5, {
584
+ dimColor: true,
585
+ children: clip(` ${line.note}`, Math.max(interior - 2 - line.text.length, 0))
586
+ }) : null
587
+ ]
588
+ }, `${line.mark}:${line.text}`)),
589
+ notes.length > 0 ? /* @__PURE__ */ jsx6(Text5, {
590
+ children: " "
591
+ }) : null,
592
+ notes.map((note) => /* @__PURE__ */ jsx6(Text5, {
593
+ dimColor: true,
594
+ children: clip(note, interior)
595
+ }, note)),
596
+ /* @__PURE__ */ jsx6(Text5, {
597
+ children: " "
598
+ }),
599
+ row(yes, choice === "yes", argv),
600
+ row(no, choice === "no")
601
+ ]
602
+ }),
603
+ /* @__PURE__ */ jsx6(Divider, {
604
+ width
605
+ }),
606
+ /* @__PURE__ */ jsx6(Hints, {
607
+ hints: [
608
+ ["^v", "choose"],
609
+ ["enter", choice === "yes" ? yes : no],
610
+ ["esc", no]
611
+ ]
612
+ })
613
+ ]
614
+ });
386
615
  }
387
-
388
- // src/settings/command.ts
389
- async function configGet(key) {
390
- return configGetReport(await readConfig(), key, configPath());
616
+ // src/ui/spinner.tsx
617
+ import { Text as Text6 } from "ink";
618
+ import { useEffect, useState as useState2 } from "react";
619
+ import { jsx as jsx7 } from "react/jsx-runtime";
620
+ var FRAMES = ["-", "\\", "|", "/"];
621
+ var INTERVAL_MS = 90;
622
+ function Spinner() {
623
+ const [frame, setFrame] = useState2(0);
624
+ useEffect(() => {
625
+ const timer = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), INTERVAL_MS);
626
+ return () => clearInterval(timer);
627
+ }, []);
628
+ return /* @__PURE__ */ jsx7(Text6, {
629
+ color: tone.primary,
630
+ children: FRAMES[frame]
631
+ });
391
632
  }
392
- async function configSet(key, value, known) {
393
- const path = configPath();
394
- const config = await readConfig();
395
- const result = setConfigValue(config, key, value);
396
- if (!result.ok) {
397
- return {
398
- title: "config",
399
- lines: [{ mark: "fail", text: result.message }],
400
- json: { path, error: result.message },
401
- failed: true
402
- };
403
- }
404
- if (key === "agents") {
405
- const valid = await known();
406
- const refusal = valid && refuse(result.config.agents ?? [], valid, path);
407
- if (refusal)
408
- return refusal;
633
+ // src/auth/session.ts
634
+ import { setTimeout as sleep } from "node:timers/promises";
635
+
636
+ // src/settings/agents.ts
637
+ import { readFileSync } from "node:fs";
638
+ import { homedir as homedir2 } from "node:os";
639
+ import { join as join2 } from "node:path";
640
+
641
+ // src/settings/answer.ts
642
+ import { dirname } from "node:path";
643
+
644
+ // src/tracking/answer.ts
645
+ function trackingFrom(layer, configPath) {
646
+ switch (layer) {
647
+ case "env":
648
+ return "YOURSKILLS_NO_TRACK in the environment";
649
+ case "config":
650
+ return `track in ${configPath}`;
651
+ case "organization":
652
+ return "usage tracking in your organization's settings";
653
+ case "default":
654
+ return "built-in default: turn off with `config set track false`";
409
655
  }
410
- await writeConfig(result.config);
411
- return {
412
- title: "config",
413
- lines: [{ mark: "ok", text: `${key} = ${result.shown}`, note: path }],
414
- json: { path, key, value: result.shown }
415
- };
416
656
  }
417
- function refuse(asked, valid, path) {
418
- const { unknown, suggest } = unknownAgents(asked, valid);
419
- if (unknown.length === 0)
420
- return;
421
- const named = unknown.join(", ");
422
- return {
423
- title: "config",
424
- lines: [
425
- {
426
- mark: "fail",
427
- text: `No agent is called ${named}.`,
428
- note: suggest.length > 0 ? `did you mean ${suggest.join(", ")}?` : ""
429
- },
430
- {
431
- mark: "none",
432
- text: "The stock `skills` CLI keeps the list of agent names, and it does not have this one, so every install would stop at it."
433
- },
434
- {
435
- mark: "none",
436
- text: "Run `yourskills doctor` to see the names this machine accepts."
437
- }
438
- ],
439
- json: { path, error: `unknown agents: ${named}`, suggest },
440
- failed: true
441
- };
442
- }
443
- // src/settings/scope.ts
444
- import { existsSync } from "node:fs";
445
- import { readFile as readFile2 } from "node:fs/promises";
446
- import { dirname, join as join3, resolve } from "node:path";
447
- var PROJECT_MANIFEST = "yourskills.json";
448
- function findProjectManifest(from) {
449
- let dir = resolve(from ?? process.cwd());
450
- for (;; ) {
451
- const candidate = join3(dir, PROJECT_MANIFEST);
452
- if (existsSync(candidate))
453
- return candidate;
454
- const parent = dirname(dir);
455
- if (parent === dir)
456
- return null;
457
- dir = parent;
458
- }
657
+ function trackingDisabledByEnv() {
658
+ const value = process.env.YOURSKILLS_NO_TRACK;
659
+ return value !== undefined && value !== "" && value !== "0";
459
660
  }
460
- async function scopeInManifest(path) {
461
- try {
462
- const parsed = JSON.parse(await readFile2(path, "utf8"));
463
- return parsed.scope === "global" || parsed.scope === "project" ? parsed.scope : null;
464
- } catch {
465
- return null;
466
- }
661
+ function trackingOn(config) {
662
+ return !trackingDisabledByEnv() && config.track !== false;
467
663
  }
468
- async function resolveScope(opts = {}) {
469
- const manifestPath = findProjectManifest(opts.cwd);
470
- if (opts.flag)
471
- return { scope: opts.flag, source: "flag", manifestPath };
472
- if (manifestPath) {
473
- const declared = await scopeInManifest(manifestPath);
474
- if (declared)
475
- return { scope: declared, source: "project", manifestPath };
664
+ var ON_NOTE = "on; turn off with `yourskills config set track false`";
665
+ function trackingAnswer(question) {
666
+ const { track, configPath, organization } = question;
667
+ if (trackingDisabledByEnv()) {
668
+ return {
669
+ state: "off-here",
670
+ sending: false,
671
+ note: "off (YOURSKILLS_NO_TRACK is set)",
672
+ value: "off",
673
+ layer: "env",
674
+ from: trackingFrom("env", configPath)
675
+ };
476
676
  }
477
- const config = await readConfig();
478
- if (config.defaultScope)
479
- return { scope: config.defaultScope, source: "config", manifestPath };
480
- return { scope: "global", source: "default", manifestPath };
481
- }
482
- // src/auth/auth.ts
483
- var CLIENT_ID = "yourskills-cli";
484
- var GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
485
- var SLOW_DOWN_STEP_MS = 5000;
486
- var authBase = (base) => `${base.replace(/\/$/, "")}/api/auth`;
487
- async function post(url, body) {
488
- const res = await fetch(url, {
489
- method: "POST",
490
- headers: { "content-type": "application/json" },
491
- body: JSON.stringify(body)
492
- });
493
- const text = await res.text();
494
- let parsed;
495
- try {
496
- parsed = JSON.parse(text);
497
- } catch {
677
+ if (track === false) {
678
+ return {
679
+ state: "off-here",
680
+ sending: false,
681
+ note: "off; turn on with `yourskills config set track true`",
682
+ value: "off",
683
+ layer: "config",
684
+ from: trackingFrom("config", configPath)
685
+ };
686
+ }
687
+ if (organization?.enabled === false) {
498
688
  return {
499
- error: "invalid_response",
500
- error_description: text.slice(0, 200) || res.statusText
689
+ state: "off-for-organization",
690
+ sending: true,
691
+ note: "off; your organization turned usage tracking off for everyone",
692
+ value: "off",
693
+ layer: "organization",
694
+ from: trackingFrom("organization", configPath)
501
695
  };
502
696
  }
503
- if (res.ok)
504
- return { ok: true, data: parsed };
505
- const err = parsed;
697
+ const layer = track === true ? "config" : "default";
506
698
  return {
507
- error: err.error ?? `http_${res.status}`,
508
- error_description: err.error_description
699
+ state: "on",
700
+ sending: true,
701
+ note: ON_NOTE,
702
+ value: "on",
703
+ layer,
704
+ from: trackingFrom(layer, configPath)
509
705
  };
510
706
  }
511
- async function* login() {
512
- const config = await readConfig();
513
- const base = serverUrl(config);
514
- const start = await post(`${authBase(base)}/device/code`, {
515
- client_id: CLIENT_ID
707
+ async function trackingNow(question) {
708
+ const here = trackingAnswer({
709
+ track: question.track,
710
+ configPath: question.configPath
516
711
  });
517
- if (!("ok" in start)) {
518
- throw new Error(`Could not start device login: ${start.error_description ?? start.error}`);
519
- }
520
- const device = start.data;
521
- const deadline = Date.now() + device.expires_in * 1000;
522
- yield {
523
- kind: "prompt",
524
- userCode: device.user_code,
525
- verificationUri: device.verification_uri_complete ?? device.verification_uri,
526
- expiresAt: deadline
527
- };
528
- let intervalMs = Math.max(device.interval, 1) * 1000;
529
- while (Date.now() < deadline) {
530
- await sleep(intervalMs);
531
- const poll = await post(`${authBase(base)}/device/token`, {
532
- grant_type: GRANT_TYPE,
533
- device_code: device.device_code,
534
- client_id: CLIENT_ID
535
- });
536
- if ("ok" in poll) {
537
- const token = poll.data.access_token;
538
- const who = await fetchSession(base, token);
539
- await writeConfig({
540
- ...config,
541
- token,
542
- serverUrl: base,
543
- userEmail: who?.user?.email
544
- });
545
- yield { kind: "done", email: who?.user?.email };
546
- return;
547
- }
548
- switch (poll.error) {
549
- case "authorization_pending":
550
- yield { kind: "waiting" };
551
- break;
552
- case "slow_down":
553
- intervalMs += SLOW_DOWN_STEP_MS;
554
- yield { kind: "waiting" };
555
- break;
556
- case "access_denied":
557
- throw new Error("Login was denied.");
558
- case "expired_token":
559
- throw new Error("The device code expired. Run `yourskills login` again.");
560
- default:
561
- throw new Error(`Login failed: ${poll.error_description ?? poll.error}`);
562
- }
563
- }
564
- throw new Error("The device code expired. Run `yourskills login` again.");
565
- }
566
- async function logout() {
567
- const config = await readConfig();
568
- if (config.token) {
569
- await fetch(`${authBase(serverUrl(config))}/sign-out`, {
570
- method: "POST",
571
- headers: { authorization: `Bearer ${config.token}` }
572
- }).catch(() => {
573
- return;
574
- });
575
- }
576
- clearConfig();
577
- }
578
- async function fetchSession(base, token) {
579
- const res = await fetch(`${authBase(base)}/get-session`, {
580
- headers: { authorization: `Bearer ${token}` }
712
+ if (!here.sending)
713
+ return here;
714
+ return trackingAnswer({
715
+ track: question.track,
716
+ configPath: question.configPath,
717
+ organization: await question.askOrganization()
581
718
  });
582
- if (!res.ok)
583
- return null;
584
- return await res.json();
585
719
  }
586
- async function whoami(server) {
587
- const config = await readConfig();
588
- if (!config.token)
589
- throw new Error("Not logged in. Run `yourskills login`.");
590
- const base = serverUrl(config, server);
591
- const session = await fetchSession(base, config.token);
592
- if (!session?.user)
593
- throw new Error("Session is no longer valid. Run `yourskills login`.");
594
- return {
595
- user: session.user.email ?? session.user.id,
596
- org: session.session?.activeOrganizationId ?? null,
597
- server: base
598
- };
720
+ // src/settings/keys.ts
721
+ var CONFIG_KEYS = [
722
+ "scope",
723
+ "agents",
724
+ "server",
725
+ "track",
726
+ "autoUpdate"
727
+ ];
728
+ function isConfigKey(key) {
729
+ return CONFIG_KEYS.includes(key);
599
730
  }
600
- async function requireToken(server) {
601
- const config = await readConfig();
602
- if (!config.token)
603
- throw new Error("Not logged in. Run `yourskills login`.");
604
- return { token: config.token, base: serverUrl(config, server) };
731
+ var REDACTED = "<redacted>";
732
+ function unknownAgents(asked, valid) {
733
+ const unknown = asked.filter((name) => !valid.includes(name));
734
+ const suggest = valid.filter((name) => unknown.some((miss) => near(miss, name)));
735
+ return { unknown, suggest };
605
736
  }
606
- async function hasSession() {
607
- return Boolean((await readConfig()).token);
737
+ function near(a, b) {
738
+ if (a === b)
739
+ return false;
740
+ if (a.includes(b) || b.includes(a))
741
+ return true;
742
+ if (Math.abs(a.length - b.length) > 1)
743
+ return false;
744
+ const [short, long] = a.length <= b.length ? [a, b] : [b, a];
745
+ let i = 0;
746
+ let j = 0;
747
+ let slack = 1;
748
+ while (i < short.length && j < long.length) {
749
+ if (short[i] === long[j]) {
750
+ i++;
751
+ j++;
752
+ continue;
753
+ }
754
+ if (slack-- === 0)
755
+ return false;
756
+ if (short.length === long.length)
757
+ i++;
758
+ j++;
759
+ }
760
+ return true;
608
761
  }
609
- async function localAccount(server) {
610
- const config = await readConfig();
611
- return {
612
- signedIn: Boolean(config.token),
613
- email: config.userEmail ?? null,
614
- organization: config.orgName ?? null,
615
- server: serverUrl(config, server)
616
- };
762
+ var UNSET = "(unset)";
763
+ function setConfigValue(config, key, value) {
764
+ if (!isConfigKey(key)) {
765
+ return {
766
+ ok: false,
767
+ message: `Unknown config key: ${key}. Known keys: ${CONFIG_KEYS.join(", ")}.`
768
+ };
769
+ }
770
+ switch (key) {
771
+ case "scope": {
772
+ if (value !== "global" && value !== "project") {
773
+ return {
774
+ ok: false,
775
+ message: "scope must be `global` or `project`."
776
+ };
777
+ }
778
+ return {
779
+ ok: true,
780
+ config: { ...config, defaultScope: value },
781
+ shown: value
782
+ };
783
+ }
784
+ case "agents": {
785
+ const agents = value.split(",").map((a) => a.trim()).filter((a) => a.length > 0);
786
+ if (agents.length === 0) {
787
+ return {
788
+ ok: false,
789
+ message: "agents must be a comma separated list, for example `claude-code,codex`."
790
+ };
791
+ }
792
+ return {
793
+ ok: true,
794
+ config: { ...config, agents },
795
+ shown: agents.join(",")
796
+ };
797
+ }
798
+ case "server": {
799
+ let url;
800
+ try {
801
+ url = new URL(value);
802
+ } catch {
803
+ return {
804
+ ok: false,
805
+ message: `server must be a URL, for example \`https://skills.acme.dev\`. Got: ${value}`
806
+ };
807
+ }
808
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
809
+ return { ok: false, message: "server must be an http or https URL." };
810
+ }
811
+ const serverUrl = url.toString().replace(/\/$/, "");
812
+ return { ok: true, config: { ...config, serverUrl }, shown: serverUrl };
813
+ }
814
+ case "track": {
815
+ if (value !== "true" && value !== "false") {
816
+ return { ok: false, message: "track must be `true` or `false`." };
817
+ }
818
+ const track = value === "true";
819
+ return { ok: true, config: { ...config, track }, shown: String(track) };
820
+ }
821
+ case "autoUpdate": {
822
+ if (value !== "true" && value !== "false") {
823
+ return { ok: false, message: "autoUpdate must be `true` or `false`." };
824
+ }
825
+ const autoUpdate = value === "true";
826
+ return {
827
+ ok: true,
828
+ config: { ...config, autoUpdate },
829
+ shown: String(autoUpdate)
830
+ };
831
+ }
832
+ }
617
833
  }
618
- // src/auth/screens/gate.tsx
619
- import { Box as Box6, Text as Text8, useApp as useApp2, useInput as useInput3 } from "ink";
620
- import {
621
- useCallback,
622
- useEffect as useEffect3,
623
- useMemo,
624
- useState as useState4
625
- } from "react";
626
-
627
- // src/ui/back.tsx
628
- import { useInput } from "ink";
629
834
 
630
- // ../../packages/ui/src/lib/ascii.ts
631
- var DEFAULT_ASCII_CHARS = {
632
- top: "-",
633
- bottom: "-",
634
- left: "|",
635
- right: "|",
636
- divider: "-",
637
- junction: "+"
638
- };
639
- function fill(n, seq = "-") {
640
- const count = Math.max(n, 0);
641
- if (count === 0) {
642
- return "";
835
+ // src/settings/store.ts
836
+ import { chmodSync, mkdirSync, rmSync } from "node:fs";
837
+ import { readFile, writeFile } from "node:fs/promises";
838
+ import { homedir } from "node:os";
839
+ import { join } from "node:path";
840
+ function configDir() {
841
+ const xdg = process.env.XDG_CONFIG_HOME;
842
+ return join(xdg && xdg.length > 0 ? xdg : join(homedir(), ".config"), "yourskills");
843
+ }
844
+ function configPath() {
845
+ return join(configDir(), "config.json");
846
+ }
847
+ async function readConfig() {
848
+ try {
849
+ return JSON.parse(await readFile(configPath(), "utf8"));
850
+ } catch {
851
+ return {};
852
+ }
853
+ }
854
+ async function updateConfig(change) {
855
+ let current = {};
856
+ try {
857
+ current = JSON.parse(await readFile(configPath(), "utf8"));
858
+ } catch (error) {
859
+ if (error?.code !== "ENOENT")
860
+ return null;
643
861
  }
644
- const unit = seq.length > 0 ? seq : " ";
645
- return unit.repeat(Math.ceil(count / unit.length)).slice(0, count);
862
+ const next = change(current);
863
+ await writeConfig(next);
864
+ return next;
646
865
  }
647
- function junctionGlyph(chars) {
648
- return chars.junction.length > 0 ? chars.junction[0] : DEFAULT_ASCII_CHARS.junction;
866
+ async function writeConfig(config) {
867
+ mkdirSync(configDir(), { recursive: true, mode: 448 });
868
+ await writeFile(configPath(), `${JSON.stringify(config, null, 2)}
869
+ `);
870
+ chmodSync(configPath(), 384);
649
871
  }
650
- function topBorder(width, title, chars = DEFAULT_ASCII_CHARS) {
651
- const j = junctionGlyph(chars);
652
- if (!title) {
653
- return `${j}${fill(width - 2, chars.top)}${j}`;
654
- }
655
- const label = ` ${title} `;
656
- const left = 2;
657
- const right = Math.max(width - 2 - left - label.length, 1);
658
- return `${j}${fill(left, chars.top)}${label}${fill(right, chars.top)}${j}`;
872
+ function clearConfig() {
873
+ rmSync(configPath(), { force: true });
659
874
  }
660
875
 
661
- // src/ui/frame.tsx
662
- import { Box, Text, useWindowSize } from "ink";
663
-
664
- // src/ui/theme.ts
665
- var tone = {
666
- primary: "cyan",
667
- notice: "yellow",
668
- danger: "red"
669
- };
670
- var stateTone = {
671
- "state-active": undefined,
672
- "state-experimental": "yellow",
673
- "state-deprecated": "red"
674
- };
675
- var mark = {
676
- ok: "+",
677
- fail: "x",
678
- pending: ".",
679
- none: "",
680
- cursor: ">",
681
- selected: "*",
682
- unselected: " "
683
- };
684
-
685
- // src/ui/frame.tsx
686
- import { jsx, jsxs } from "react/jsx-runtime";
687
- var MAX_WIDTH = 96;
688
- var MIN_WIDTH = 44;
689
- var FRAME_CHROME = 4;
690
- function useFrameWidth() {
691
- const { columns } = useWindowSize();
692
- return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, columns - 2));
876
+ // src/settings/answer.ts
877
+ function decide(consulted, last) {
878
+ const layers = [...consulted, last];
879
+ const spoke = consulted.find((layer) => layer.value !== null);
880
+ if (spoke && spoke.value !== null) {
881
+ return {
882
+ value: spoke.value,
883
+ layer: spoke.layer,
884
+ from: spoke.from,
885
+ layers,
886
+ decided: true
887
+ };
888
+ }
889
+ return {
890
+ value: last.value,
891
+ layer: last.layer,
892
+ from: last.from,
893
+ layers,
894
+ decided: false
895
+ };
693
896
  }
694
- var j = DEFAULT_ASCII_CHARS.junction;
695
- var BORDER = {
696
- topLeft: j,
697
- top: DEFAULT_ASCII_CHARS.top,
698
- topRight: j,
699
- right: DEFAULT_ASCII_CHARS.right,
700
- bottomRight: j,
701
- bottom: DEFAULT_ASCII_CHARS.bottom,
702
- bottomLeft: j,
703
- left: DEFAULT_ASCII_CHARS.left
704
- };
705
- function Frame({
706
- title,
707
- width,
708
- children
709
- }) {
710
- return /* @__PURE__ */ jsxs(Box, {
711
- flexDirection: "column",
712
- width,
713
- children: [
714
- /* @__PURE__ */ jsx(Text, {
715
- color: tone.primary,
716
- children: topBorder(width, title)
717
- }),
718
- /* @__PURE__ */ jsx(Box, {
719
- flexDirection: "column",
720
- width,
721
- paddingLeft: 1,
722
- paddingRight: 1,
723
- borderStyle: BORDER,
724
- borderColor: tone.primary,
725
- borderTop: false,
726
- children
727
- })
728
- ]
729
- });
897
+ function scopeAnswer(held) {
898
+ const { project } = held;
899
+ return decide([
900
+ {
901
+ layer: "flag",
902
+ value: held.flags?.scope ?? null,
903
+ from: "-g or -p on the command line"
904
+ },
905
+ project ? {
906
+ layer: "project",
907
+ value: project.scope,
908
+ from: `yourskills.json at ${dirname(project.path)}${project.scope ? "" : " (declares no scope)"}`
909
+ } : {
910
+ layer: "project",
911
+ value: null,
912
+ from: "no yourskills.json above here"
913
+ },
914
+ {
915
+ layer: "config",
916
+ value: held.config.defaultScope ?? null,
917
+ from: `defaultScope in ${held.configPath}`
918
+ }
919
+ ], { layer: "default", value: "global", from: "built-in default" });
730
920
  }
731
- function Divider({ width }) {
732
- return /* @__PURE__ */ jsx(Box, {
733
- marginLeft: -1,
734
- marginRight: -1,
735
- children: /* @__PURE__ */ jsx(Text, {
736
- color: tone.primary,
737
- children: fill(width - 2, DEFAULT_ASCII_CHARS.divider)
738
- })
921
+ function agentsAnswer(held) {
922
+ const some = (agents) => agents && agents.length > 0 ? agents : null;
923
+ return decide([
924
+ {
925
+ layer: "flag",
926
+ value: some(held.flags?.agents),
927
+ from: "-a on the command line"
928
+ },
929
+ {
930
+ layer: "config",
931
+ value: some(held.config.agents),
932
+ from: `agents in ${held.configPath}`
933
+ },
934
+ {
935
+ layer: "stock",
936
+ value: some(held.stock?.agents),
937
+ from: held.stock ? `lastSelectedAgents in ${held.stock.path}` : "what the stock `skills` CLI remembers (not read)"
938
+ }
939
+ ], {
940
+ layer: "default",
941
+ value: [],
942
+ from: "nothing pinned; the stock `skills` CLI looks at what is installed"
739
943
  });
740
944
  }
741
- function Blank() {
742
- return /* @__PURE__ */ jsx(Text, {
743
- children: " "
744
- });
945
+ var DEFAULT_URL = "https://api.yourskills.store";
946
+ function serverAnswer(held) {
947
+ return decide([
948
+ {
949
+ layer: "flag",
950
+ value: held.flags?.server || null,
951
+ from: "--server on the command line"
952
+ },
953
+ {
954
+ layer: "env",
955
+ value: process.env.YOURSKILLS_URL || null,
956
+ from: "YOURSKILLS_URL in the environment"
957
+ },
958
+ {
959
+ layer: "config",
960
+ value: held.config.serverUrl || null,
961
+ from: `server in ${held.configPath}`
962
+ }
963
+ ], { layer: "default", value: DEFAULT_URL, from: "built-in default" });
745
964
  }
746
- function clip(text, width) {
747
- if (width <= 0)
748
- return "";
749
- if (text.length <= width)
750
- return text;
751
- return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}~`;
965
+ function serverUrl(config = {}, override) {
966
+ return serverAnswer({
967
+ config,
968
+ configPath: configPath(),
969
+ flags: { server: override }
970
+ }).value;
752
971
  }
753
-
754
- // ../../packages/skills/src/lifecycle.ts
755
- var LIFECYCLES = [
756
- { status: "active", word: "active", glyph: "#", tone: "state-active" },
757
- {
758
- status: "experimental",
759
- word: "experimental",
760
- glyph: "?",
761
- tone: "state-experimental"
762
- },
763
- {
764
- status: "deprecated",
765
- word: "deprecated",
766
- glyph: "x",
767
- tone: "state-deprecated"
768
- }
769
- ];
770
- var LIFECYCLE_ORDER = LIFECYCLES.map((lifecycle) => lifecycle.status);
771
- var BY_STATUS = new Map(LIFECYCLES.map((lifecycle) => [lifecycle.status, lifecycle]));
772
- function lifecycleOf(status) {
773
- const lifecycle = BY_STATUS.get(status);
774
- if (!lifecycle) {
775
- throw new Error(`No lifecycle vocabulary for "${status}". Add it to LIFECYCLES.`);
776
- }
777
- return lifecycle;
972
+ function trackingOf(held) {
973
+ return trackingAnswer({
974
+ track: held.config.track,
975
+ configPath: held.configPath,
976
+ organization: held.organization
977
+ });
778
978
  }
779
- // src/ui/rows.tsx
780
- import { Box as Box2, Text as Text2 } from "ink";
781
- import { jsx as jsx2, jsxs as jsxs2, Fragment } from "react/jsx-runtime";
782
- function lifecycleTag(status) {
783
- if (!status || status === "active")
784
- return "";
785
- return `[${lifecycleOf(status).word}]`;
979
+ function trackAnswer(held) {
980
+ const tracking = trackingOf(held);
981
+ const { configPath, organization } = held;
982
+ const layers = [
983
+ {
984
+ layer: "env",
985
+ value: trackingDisabledByEnv() ? false : null,
986
+ from: trackingFrom("env", configPath)
987
+ },
988
+ {
989
+ layer: "config",
990
+ value: held.config.track ?? null,
991
+ from: trackingFrom("config", configPath)
992
+ },
993
+ {
994
+ layer: "organization",
995
+ value: organization ? organization.enabled : null,
996
+ from: trackingFrom("organization", configPath)
997
+ },
998
+ {
999
+ layer: "default",
1000
+ value: true,
1001
+ from: trackingFrom("default", configPath)
1002
+ }
1003
+ ];
1004
+ return {
1005
+ value: tracking.value === "on",
1006
+ layer: tracking.layer,
1007
+ from: tracking.from,
1008
+ layers,
1009
+ decided: tracking.layer !== "default"
1010
+ };
786
1011
  }
787
- function lifecycleColor(status) {
788
- return stateTone[lifecycleOf(status ?? "active").tone];
1012
+ function autoUpdateAnswer(held) {
1013
+ return decide([
1014
+ {
1015
+ layer: "config",
1016
+ value: held.config.autoUpdate ?? null,
1017
+ from: `autoUpdate in ${held.configPath}`
1018
+ }
1019
+ ], { layer: "default", value: true, from: "built-in default" });
789
1020
  }
790
- var GUTTER = 4;
791
- var GAP = 2;
792
- function nameWidth(rows, max = 28) {
793
- return Math.min(max, Math.max(8, ...rows.map((r) => r.name.length)));
1021
+ function spell(key, answer, as) {
1022
+ return {
1023
+ key,
1024
+ json: answer.value,
1025
+ value: as(answer.value),
1026
+ layer: answer.layer,
1027
+ from: answer.from,
1028
+ decided: answer.decided,
1029
+ layers: answer.layers.map((layer) => ({
1030
+ layer: layer.layer,
1031
+ from: layer.from,
1032
+ value: layer.value === null ? null : as(layer.value)
1033
+ }))
1034
+ };
794
1035
  }
795
- function SkillRow({
796
- row,
797
- width,
798
- names,
799
- cursor,
800
- selected,
801
- selectable = false
802
- }) {
803
- const tag = lifecycleTag(row.status);
804
- const meta = row.count !== undefined ? `${row.count} skills` : tag;
805
- const gutter = selectable ? GUTTER : 0;
806
- const rest = width - gutter - names - GAP - (meta ? meta.length + GAP : 0);
807
- return /* @__PURE__ */ jsxs2(Box2, {
808
- children: [
809
- selectable ? /* @__PURE__ */ jsx2(Text2, {
810
- color: cursor ? "cyan" : undefined,
811
- children: `${cursor ? mark.cursor : " "} ${selected ? mark.selected : mark.unselected} `
812
- }) : null,
813
- /* @__PURE__ */ jsx2(Text2, {
814
- bold: cursor,
815
- color: lifecycleColor(row.status),
816
- children: clip(row.name, names).padEnd(names)
817
- }),
818
- /* @__PURE__ */ jsx2(Text2, {
819
- children: " ".repeat(GAP)
820
- }),
821
- /* @__PURE__ */ jsx2(Text2, {
822
- dimColor: true,
823
- children: clip(row.description ?? "", Math.max(rest, 0))
824
- }),
825
- meta ? /* @__PURE__ */ jsxs2(Fragment, {
826
- children: [
827
- /* @__PURE__ */ jsx2(Text2, {
828
- children: " ".repeat(GAP)
829
- }),
830
- /* @__PURE__ */ jsx2(Text2, {
831
- color: lifecycleColor(row.status),
832
- dimColor: !tag,
833
- children: meta
834
- })
835
- ]
836
- }) : null
837
- ]
838
- });
1036
+ function shown(key, held) {
1037
+ switch (key) {
1038
+ case "scope":
1039
+ return spell(key, scopeAnswer(held), (scope) => scope);
1040
+ case "agents":
1041
+ return spell(key, agentsAnswer(held), (agents) => agents.length > 0 ? agents.join(",") : UNSET);
1042
+ case "server":
1043
+ return spell(key, serverAnswer(held), (server) => server);
1044
+ case "track":
1045
+ return spell(key, trackAnswer(held), String);
1046
+ case "autoUpdate":
1047
+ return spell(key, autoUpdateAnswer(held), String);
1048
+ }
839
1049
  }
840
- function Hints({ hints }) {
841
- return /* @__PURE__ */ jsx2(Box2, {
842
- children: hints.map(([key, label], i) => /* @__PURE__ */ jsxs2(Text2, {
843
- children: [
844
- i > 0 ? /* @__PURE__ */ jsx2(Text2, {
845
- dimColor: true,
846
- children: " "
847
- }) : null,
848
- /* @__PURE__ */ jsx2(Text2, {
849
- color: "cyan",
850
- children: key
851
- }),
852
- /* @__PURE__ */ jsx2(Text2, {
853
- dimColor: true,
854
- children: ` ${label}`
855
- })
856
- ]
857
- }, key))
858
- });
1050
+ function settingsShown(held) {
1051
+ return CONFIG_KEYS.map((key) => shown(key, held));
859
1052
  }
860
1053
 
861
- // src/ui/back.tsx
862
- import { jsx as jsx3, jsxs as jsxs3, Fragment as Fragment2 } from "react/jsx-runtime";
863
- function Back({
864
- width,
865
- onBack,
866
- extra = []
867
- }) {
868
- useInput((input, key) => {
869
- if (key.escape || input === "x")
870
- onBack?.();
871
- }, { isActive: Boolean(onBack) });
872
- if (!onBack)
1054
+ // src/settings/agents.ts
1055
+ function stockLockPath() {
1056
+ const state = process.env.XDG_STATE_HOME;
1057
+ if (state && state.length > 0)
1058
+ return join2(state, "skills", ".skill-lock.json");
1059
+ const home = process.env.HOME;
1060
+ return join2(home && home.length > 0 ? home : homedir2(), ".agents", ".skill-lock.json");
1061
+ }
1062
+ var LOCK_VERSION = 3;
1063
+ function rememberedAgents() {
1064
+ let lock;
1065
+ try {
1066
+ lock = JSON.parse(readFileSync(stockLockPath(), "utf8"));
1067
+ } catch {
1068
+ return [];
1069
+ }
1070
+ if (!lock || typeof lock !== "object")
1071
+ return [];
1072
+ const raw = lock;
1073
+ if (typeof raw.version !== "number" || raw.version < LOCK_VERSION)
1074
+ return [];
1075
+ if (!raw.skills)
1076
+ return [];
1077
+ return Array.isArray(raw.lastSelectedAgents) ? raw.lastSelectedAgents.filter((a) => typeof a === "string") : [];
1078
+ }
1079
+ async function resolveAgents(flagAgents) {
1080
+ const config = await readConfig();
1081
+ return agentsAnswer({
1082
+ config,
1083
+ configPath: configPath(),
1084
+ flags: { agents: flagAgents },
1085
+ stock: { path: stockLockPath(), agents: rememberedAgents() }
1086
+ }).value;
1087
+ }
1088
+ // src/settings/scope.ts
1089
+ import { existsSync } from "node:fs";
1090
+ import { readFile as readFile2 } from "node:fs/promises";
1091
+ import { dirname as dirname2, join as join3, resolve } from "node:path";
1092
+ var PROJECT_MANIFEST = "yourskills.json";
1093
+ function findProjectManifest(from) {
1094
+ let dir = resolve(from ?? process.cwd());
1095
+ for (;; ) {
1096
+ const candidate = join3(dir, PROJECT_MANIFEST);
1097
+ if (existsSync(candidate))
1098
+ return candidate;
1099
+ const parent = dirname2(dir);
1100
+ if (parent === dir)
1101
+ return null;
1102
+ dir = parent;
1103
+ }
1104
+ }
1105
+ async function scopeInManifest(path) {
1106
+ try {
1107
+ const parsed = JSON.parse(await readFile2(path, "utf8"));
1108
+ return parsed.scope === "global" || parsed.scope === "project" ? parsed.scope : null;
1109
+ } catch {
873
1110
  return null;
874
- return /* @__PURE__ */ jsxs3(Fragment2, {
875
- children: [
876
- /* @__PURE__ */ jsx3(Divider, {
877
- width
878
- }),
879
- /* @__PURE__ */ jsx3(Hints, {
880
- hints: [...extra, ["esc/x", "back"]]
881
- })
882
- ]
1111
+ }
1112
+ }
1113
+ async function nearestProject(cwd) {
1114
+ const path = findProjectManifest(cwd);
1115
+ return path ? { path, scope: await scopeInManifest(path) } : null;
1116
+ }
1117
+ async function resolveScope(opts = {}) {
1118
+ const [config, project] = await Promise.all([
1119
+ readConfig(),
1120
+ nearestProject(opts.cwd)
1121
+ ]);
1122
+ const answer = scopeAnswer({
1123
+ config,
1124
+ configPath: configPath(),
1125
+ flags: { scope: opts.flag },
1126
+ project
883
1127
  });
1128
+ return {
1129
+ scope: answer.value,
1130
+ source: answer.layer,
1131
+ manifestPath: project?.path ?? null
1132
+ };
884
1133
  }
885
- // src/ui/command.tsx
886
- import { Box as Box3, Text as Text4 } from "ink";
887
1134
 
888
- // src/ui/text-input.tsx
889
- import { Text as Text3 } from "ink";
890
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
891
- function SearchInput({
892
- value,
893
- placeholder = "",
894
- focus = true
895
- }) {
896
- return /* @__PURE__ */ jsxs4(Text3, {
897
- children: [
898
- value.length > 0 ? /* @__PURE__ */ jsx4(Text3, {
899
- children: value
900
- }) : /* @__PURE__ */ jsx4(Text3, {
901
- dimColor: true,
902
- children: placeholder
903
- }),
904
- focus ? /* @__PURE__ */ jsx4(Text3, {
905
- color: tone.primary,
906
- children: "_"
907
- }) : null
908
- ]
909
- });
1135
+ // src/settings/held.ts
1136
+ async function holdSettings(ask = {}) {
1137
+ const [config, project] = await Promise.all([
1138
+ readConfig(),
1139
+ nearestProject(ask.cwd)
1140
+ ]);
1141
+ const organization = ask.askOrganization && trackingOn(config) ? await ask.askOrganization() : undefined;
1142
+ return {
1143
+ config,
1144
+ configPath: configPath(),
1145
+ flags: ask.flags,
1146
+ project,
1147
+ stock: { path: stockLockPath(), agents: rememberedAgents() },
1148
+ organization
1149
+ };
910
1150
  }
911
1151
 
912
- // src/ui/viewport.ts
913
- import { useWindowSize as useWindowSize2 } from "ink";
914
- var MIN_ROWS = 3;
915
- var MAX_ROWS = 20;
916
- function useViewport({
917
- cursor,
918
- total,
919
- chrome
920
- }) {
921
- const { rows } = useWindowSize2();
922
- const size = Math.max(MIN_ROWS, Math.min(MAX_ROWS, rows - chrome));
923
- const first = Math.max(0, Math.min(cursor - Math.floor(size / 2), total - size));
924
- return { first, size };
1152
+ // src/settings/command.ts
1153
+ async function configGet(key) {
1154
+ return configGetReport(await holdSettings(), key);
925
1155
  }
926
- function position(cursor, total) {
927
- return `${total > 0 ? cursor + 1 : 0}/${total}`;
1156
+ function configLines(held) {
1157
+ const width = Math.max(...CONFIG_KEYS.map((k) => k.length), "user".length);
1158
+ const lines = settingsShown(held).map((setting) => ({
1159
+ mark: "none",
1160
+ text: `${setting.key.padEnd(width)} ${setting.value}`
1161
+ }));
1162
+ lines.push({
1163
+ mark: "none",
1164
+ text: `${"user".padEnd(width)} ${held.config.userEmail ?? UNSET}`
1165
+ });
1166
+ lines.push({
1167
+ mark: "none",
1168
+ text: `${"token".padEnd(width)} ${held.config.token ? REDACTED : UNSET}`
1169
+ });
1170
+ return lines;
928
1171
  }
929
-
930
- // src/ui/command.tsx
931
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
932
- function filterCommands(groups, query) {
933
- if (query.length === 0)
934
- return groups;
935
- const q = query.toLowerCase();
936
- return groups.map((group) => ({
937
- group: group.group,
938
- items: group.items.filter((item) => item.label.toLowerCase().includes(q) || (item.hint ?? "").toLowerCase().includes(q))
939
- })).filter((group) => group.items.length > 0);
1172
+ function configGetReport(held, key) {
1173
+ const path = held.configPath;
1174
+ if (key === undefined) {
1175
+ return {
1176
+ title: "config",
1177
+ lines: configLines(held),
1178
+ json: {
1179
+ path,
1180
+ ...Object.fromEntries(settingsShown(held).map((setting) => [setting.key, setting.json])),
1181
+ user: held.config.userEmail ?? null,
1182
+ token: held.config.token ? REDACTED : null
1183
+ }
1184
+ };
1185
+ }
1186
+ if (!isConfigKey(key)) {
1187
+ return {
1188
+ title: "config",
1189
+ lines: [
1190
+ {
1191
+ mark: "fail",
1192
+ text: `Unknown config key: ${key}`,
1193
+ note: `Known keys: ${CONFIG_KEYS.join(", ")}`
1194
+ }
1195
+ ],
1196
+ json: { path, error: `unknown key: ${key}`, keys: CONFIG_KEYS },
1197
+ failed: true
1198
+ };
1199
+ }
1200
+ const setting = shown(key, held);
1201
+ return {
1202
+ title: "config",
1203
+ lines: [{ mark: "none", text: setting.value }],
1204
+ json: { path, key, value: setting.json }
1205
+ };
940
1206
  }
941
- function flattenCommands(groups) {
942
- return groups.flatMap((group) => group.items);
1207
+ async function configSet(key, value, known) {
1208
+ const path = configPath();
1209
+ const config = await readConfig();
1210
+ const result = setConfigValue(config, key, value);
1211
+ if (!result.ok) {
1212
+ return {
1213
+ title: "config",
1214
+ lines: [{ mark: "fail", text: result.message }],
1215
+ json: { path, error: result.message },
1216
+ failed: true
1217
+ };
1218
+ }
1219
+ if (key === "agents") {
1220
+ const valid = await known();
1221
+ const refusal = valid && refuse(result.config.agents ?? [], valid, path);
1222
+ if (refusal)
1223
+ return refusal;
1224
+ }
1225
+ await writeConfig(result.config);
1226
+ return {
1227
+ title: "config",
1228
+ lines: [{ mark: "ok", text: `${key} = ${result.shown}`, note: path }],
1229
+ json: { path, key, value: result.shown }
1230
+ };
943
1231
  }
944
- function windowGroups(groups, first, count) {
945
- const out = [];
946
- let index = 0;
947
- for (const group of groups) {
948
- const items = group.items.filter((_, i) => {
949
- const row = index + i;
950
- return row >= first && row < first + count;
951
- });
952
- index += group.items.length;
953
- if (items.length > 0)
954
- out.push({ group: group.group, items });
1232
+ function refuse(asked, valid, path) {
1233
+ const { unknown, suggest } = unknownAgents(asked, valid);
1234
+ if (unknown.length === 0)
1235
+ return;
1236
+ const named = unknown.join(", ");
1237
+ return {
1238
+ title: "config",
1239
+ lines: [
1240
+ {
1241
+ mark: "fail",
1242
+ text: `No agent is called ${named}.`,
1243
+ note: suggest.length > 0 ? `did you mean ${suggest.join(", ")}?` : ""
1244
+ },
1245
+ {
1246
+ mark: "none",
1247
+ text: "The stock `skills` CLI keeps the list of agent names, and it does not have this one, so every install would stop at it."
1248
+ },
1249
+ {
1250
+ mark: "none",
1251
+ text: "Run `yourskills doctor` to see the names this machine accepts."
1252
+ }
1253
+ ],
1254
+ json: { path, error: `unknown agents: ${named}`, suggest },
1255
+ failed: true
1256
+ };
1257
+ }
1258
+ // src/auth/session.ts
1259
+ var CLIENT_ID = "yourskills-cli";
1260
+ var GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
1261
+ var SLOW_DOWN_STEP_MS = 5000;
1262
+ var LOGIN = "yourskills login";
1263
+ var EXPIRED = `The device code expired. Run \`${LOGIN}\` again.`;
1264
+ var authBase = (base) => `${base.replace(/\/$/, "")}/api/auth`;
1265
+ var bearer = (token) => ({ authorization: `Bearer ${token}` });
1266
+ var REFUSED = {
1267
+ "no-token": { said: "Not logged in.", fact: "no token on this machine" },
1268
+ stale: {
1269
+ said: "Session is no longer valid.",
1270
+ fact: "the registry no longer accepts the token on this machine"
1271
+ }
1272
+ };
1273
+
1274
+ class SessionRefusal extends Error {
1275
+ reason;
1276
+ fact;
1277
+ next = LOGIN;
1278
+ constructor(reason) {
1279
+ super(`${REFUSED[reason].said} Run \`${LOGIN}\`.`);
1280
+ this.name = "SessionRefusal";
1281
+ this.reason = reason;
1282
+ this.fact = REFUSED[reason].fact;
955
1283
  }
956
- return out;
957
1284
  }
958
- var POINTER = 2;
959
- var GAP2 = 2;
960
- var LABEL_CAP = 22;
961
- function valueColumn(items) {
962
- return Math.min(items.reduce((wide, item) => Math.max(wide, item.label.length), 0), LABEL_CAP);
1285
+ function isSessionRefusal(error) {
1286
+ return error instanceof Error && error.name === "SessionRefusal";
963
1287
  }
964
- function CommandRow({
965
- item,
966
- width,
967
- column,
968
- active
969
- }) {
970
- const hint = item.hint ?? "";
971
- const room = width - POINTER - (hint ? hint.length + GAP2 : 0);
972
- const label = clip(item.label, Math.max(room, 0));
973
- const shows = item.shows ?? "";
974
- const pad = Math.max(column - label.length, 0) + GAP2;
975
- const value = shows && label.length + pad + shows.length <= room ? shows : "";
976
- const left = label.length + (value ? pad + value.length : 0);
977
- const gap = Math.max(width - POINTER - left - hint.length, hint ? 1 : 0);
978
- return /* @__PURE__ */ jsxs5(Text4, {
979
- children: [
980
- /* @__PURE__ */ jsx5(Text4, {
981
- color: tone.primary,
982
- children: `${active ? mark.cursor : " "} `
983
- }),
984
- /* @__PURE__ */ jsx5(Text4, {
985
- bold: active,
986
- color: active ? tone.primary : undefined,
987
- children: label
988
- }),
989
- value ? /* @__PURE__ */ jsx5(Text4, {
990
- children: `${" ".repeat(pad)}${value}`
991
- }) : null,
992
- /* @__PURE__ */ jsx5(Text4, {
993
- dimColor: true,
994
- children: `${" ".repeat(gap)}${hint}`
995
- })
996
- ]
997
- });
1288
+ async function requireToken(server) {
1289
+ const config = await readConfig();
1290
+ if (!config.token)
1291
+ throw new SessionRefusal("no-token");
1292
+ return { token: config.token, base: serverUrl(config, server) };
998
1293
  }
999
- function CommandPalette({
1000
- groups,
1001
- query,
1002
- cursor,
1003
- width,
1004
- chrome = 11,
1005
- placeholder = "type a command or search",
1006
- emptyMessage = "No matching commands."
1007
- }) {
1008
- const visible = filterCommands(groups, query);
1009
- const rows = flattenCommands(visible);
1010
- const current = rows[cursor];
1011
- const interior = width - FRAME_CHROME;
1012
- const column = valueColumn(rows);
1013
- const { first, size } = useViewport({
1014
- cursor,
1015
- total: rows.length,
1016
- chrome: chrome + Math.max(0, visible.length * 2 - 1)
1294
+ async function hasSession() {
1295
+ return Boolean((await readConfig()).token);
1296
+ }
1297
+ async function localAccount(server) {
1298
+ const config = await readConfig();
1299
+ return {
1300
+ signedIn: Boolean(config.token),
1301
+ email: config.userEmail ?? null,
1302
+ organization: config.orgName ?? null,
1303
+ server: serverUrl(config, server)
1304
+ };
1305
+ }
1306
+ async function post(talk, url, body) {
1307
+ const res = await talk(url, {
1308
+ method: "POST",
1309
+ headers: { "content-type": "application/json" },
1310
+ body: JSON.stringify(body)
1017
1311
  });
1018
- const paged = windowGroups(visible, first, size);
1019
- return /* @__PURE__ */ jsxs5(Box3, {
1020
- flexDirection: "column",
1021
- children: [
1022
- /* @__PURE__ */ jsxs5(Box3, {
1023
- children: [
1024
- /* @__PURE__ */ jsx5(Text4, {
1025
- color: tone.primary,
1026
- children: "> "
1027
- }),
1028
- /* @__PURE__ */ jsx5(SearchInput, {
1029
- value: query,
1030
- placeholder
1031
- })
1032
- ]
1033
- }),
1034
- /* @__PURE__ */ jsx5(Divider, {
1035
- width
1036
- }),
1037
- rows.length === 0 ? /* @__PURE__ */ jsx5(Text4, {
1038
- dimColor: true,
1039
- children: emptyMessage
1040
- }) : paged.map((group, i) => /* @__PURE__ */ jsxs5(Box3, {
1041
- flexDirection: "column",
1042
- children: [
1043
- i > 0 ? /* @__PURE__ */ jsx5(Divider, {
1044
- width
1045
- }) : null,
1046
- /* @__PURE__ */ jsx5(Text4, {
1047
- dimColor: true,
1048
- children: group.group.toUpperCase()
1049
- }),
1050
- group.items.map((item) => /* @__PURE__ */ jsx5(CommandRow, {
1051
- item,
1052
- width: interior,
1053
- column,
1054
- active: item.value === current?.value
1055
- }, item.value))
1056
- ]
1057
- }, group.group))
1058
- ]
1312
+ const text = await res.text();
1313
+ let parsed;
1314
+ try {
1315
+ parsed = JSON.parse(text);
1316
+ } catch {
1317
+ return {
1318
+ error: "invalid_response",
1319
+ error_description: text.slice(0, 200) || res.statusText
1320
+ };
1321
+ }
1322
+ if (res.ok)
1323
+ return { ok: true, data: parsed };
1324
+ const err = parsed;
1325
+ return {
1326
+ error: err.error ?? `http_${res.status}`,
1327
+ error_description: err.error_description
1328
+ };
1329
+ }
1330
+ async function getSession(talk, base, token) {
1331
+ const res = await talk(`${authBase(base)}/get-session`, {
1332
+ headers: bearer(token)
1059
1333
  });
1334
+ if (!res.ok)
1335
+ return null;
1336
+ return await res.json();
1060
1337
  }
1061
- // src/ui/confirm.tsx
1062
- import { Box as Box4, Text as Text5, useInput as useInput2 } from "ink";
1063
- import { useState } from "react";
1064
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1065
- function ConfirmScreen({
1066
- title,
1067
- headline,
1068
- lines,
1069
- notes = [],
1070
- yes,
1071
- no,
1072
- argv,
1073
- onYes,
1074
- onNo
1075
- }) {
1076
- const width = useFrameWidth();
1077
- const [choice, setChoice] = useState("yes");
1078
- useInput2((input, key) => {
1079
- if (key.upArrow || key.downArrow) {
1080
- setChoice((c) => c === "yes" ? "no" : "yes");
1081
- } else if (key.return) {
1082
- if (choice === "yes")
1083
- onYes();
1084
- else
1085
- onNo();
1086
- } else if (key.escape || input === "x") {
1087
- onNo();
1338
+ async function sessionOf(talk, base, token) {
1339
+ const answer = await getSession(talk, base, token);
1340
+ if (!answer?.user)
1341
+ throw new SessionRefusal("stale");
1342
+ return {
1343
+ user: answer.user.email ?? answer.user.id,
1344
+ organizationId: answer.session?.activeOrganizationId ?? null,
1345
+ server: base
1346
+ };
1347
+ }
1348
+ async function listOrganizations(talk, base, token) {
1349
+ let res;
1350
+ try {
1351
+ res = await talk(`${authBase(base)}/organization/list`, {
1352
+ headers: bearer(token)
1353
+ });
1354
+ } catch (error) {
1355
+ throw new Error(`Could not reach ${base}: ${error instanceof Error ? error.message : String(error)}`);
1356
+ }
1357
+ if (!res.ok) {
1358
+ throw new Error(`Could not list organizations: ${res.status} ${res.statusText}`);
1359
+ }
1360
+ return await res.json();
1361
+ }
1362
+ async function* login(reach = {}) {
1363
+ const talk = reach.talk ?? fetch;
1364
+ const wait = reach.wait ?? ((ms) => sleep(ms));
1365
+ const config = await readConfig();
1366
+ const base = serverUrl(config, reach.server);
1367
+ const start = await post(talk, `${authBase(base)}/device/code`, { client_id: CLIENT_ID });
1368
+ if (!("ok" in start)) {
1369
+ throw new Error(`Could not start device login: ${start.error_description ?? start.error}`);
1370
+ }
1371
+ const device = start.data;
1372
+ const deadline = Date.now() + device.expires_in * 1000;
1373
+ yield {
1374
+ kind: "prompt",
1375
+ userCode: device.user_code,
1376
+ verificationUri: device.verification_uri_complete ?? device.verification_uri,
1377
+ expiresAt: deadline
1378
+ };
1379
+ let intervalMs = Math.max(device.interval, 1) * 1000;
1380
+ while (Date.now() < deadline) {
1381
+ await wait(intervalMs);
1382
+ const poll = await post(talk, `${authBase(base)}/device/token`, {
1383
+ grant_type: GRANT_TYPE,
1384
+ device_code: device.device_code,
1385
+ client_id: CLIENT_ID
1386
+ });
1387
+ if ("ok" in poll) {
1388
+ const token = poll.data.access_token;
1389
+ const who = await getSession(talk, base, token);
1390
+ await writeConfig({
1391
+ ...config,
1392
+ token,
1393
+ serverUrl: base,
1394
+ userEmail: who?.user?.email
1395
+ });
1396
+ yield { kind: "done", email: who?.user?.email };
1397
+ return;
1088
1398
  }
1089
- });
1090
- const interior = width - FRAME_CHROME;
1091
- const row = (label, active, hint) => {
1092
- const room = interior - 2 - (hint ? hint.length + 2 : 0);
1093
- const text = clip(label, Math.max(room, 0));
1094
- const gap = Math.max(interior - 2 - text.length - (hint?.length ?? 0), 1);
1095
- return /* @__PURE__ */ jsxs6(Text5, {
1096
- children: [
1097
- /* @__PURE__ */ jsx6(Text5, {
1098
- color: tone.primary,
1099
- children: `${active ? mark.cursor : " "} `
1100
- }),
1101
- /* @__PURE__ */ jsx6(Text5, {
1102
- bold: active,
1103
- color: active ? tone.primary : undefined,
1104
- children: text
1105
- }),
1106
- hint ? /* @__PURE__ */ jsx6(Text5, {
1107
- dimColor: true,
1108
- children: `${" ".repeat(gap)}${hint}`
1109
- }) : null
1110
- ]
1399
+ switch (poll.error) {
1400
+ case "authorization_pending":
1401
+ yield { kind: "waiting" };
1402
+ break;
1403
+ case "slow_down":
1404
+ intervalMs += SLOW_DOWN_STEP_MS;
1405
+ yield { kind: "waiting" };
1406
+ break;
1407
+ case "access_denied":
1408
+ throw new Error("Login was denied.");
1409
+ case "expired_token":
1410
+ throw new Error(EXPIRED);
1411
+ default:
1412
+ throw new Error(`Login failed: ${poll.error_description ?? poll.error}`);
1413
+ }
1414
+ }
1415
+ throw new Error(EXPIRED);
1416
+ }
1417
+ async function logout(reach = {}) {
1418
+ const config = await readConfig();
1419
+ if (config.token) {
1420
+ await (reach.talk ?? fetch)(`${authBase(serverUrl(config, reach.server))}/sign-out`, { method: "POST", headers: bearer(config.token) }).catch(() => {
1421
+ return;
1111
1422
  });
1112
- };
1113
- return /* @__PURE__ */ jsxs6(Frame, {
1114
- title,
1115
- width,
1116
- children: [
1117
- /* @__PURE__ */ jsxs6(Box4, {
1118
- flexDirection: "column",
1119
- children: [
1120
- /* @__PURE__ */ jsx6(Text5, {
1121
- color: tone.notice,
1122
- children: clip(headline, interior)
1123
- }),
1124
- /* @__PURE__ */ jsx6(Text5, {
1125
- children: " "
1126
- }),
1127
- lines.map((line) => /* @__PURE__ */ jsxs6(Text5, {
1128
- children: [
1129
- /* @__PURE__ */ jsx6(Text5, {
1130
- color: line.mark === "fail" ? tone.danger : undefined,
1131
- children: `${mark[line.mark] || " "} `
1132
- }),
1133
- /* @__PURE__ */ jsx6(Text5, {
1134
- children: clip(line.text, interior - 2)
1135
- }),
1136
- line.note ? /* @__PURE__ */ jsx6(Text5, {
1137
- dimColor: true,
1138
- children: clip(` ${line.note}`, Math.max(interior - 2 - line.text.length, 0))
1139
- }) : null
1140
- ]
1141
- }, `${line.mark}:${line.text}`)),
1142
- notes.length > 0 ? /* @__PURE__ */ jsx6(Text5, {
1143
- children: " "
1144
- }) : null,
1145
- notes.map((note) => /* @__PURE__ */ jsx6(Text5, {
1146
- dimColor: true,
1147
- children: clip(note, interior)
1148
- }, note)),
1149
- /* @__PURE__ */ jsx6(Text5, {
1150
- children: " "
1151
- }),
1152
- row(yes, choice === "yes", argv),
1153
- row(no, choice === "no")
1154
- ]
1155
- }),
1156
- /* @__PURE__ */ jsx6(Divider, {
1157
- width
1158
- }),
1159
- /* @__PURE__ */ jsx6(Hints, {
1160
- hints: [
1161
- ["^v", "choose"],
1162
- ["enter", choice === "yes" ? yes : no],
1163
- ["esc", no]
1164
- ]
1165
- })
1166
- ]
1167
- });
1423
+ }
1424
+ clearConfig();
1168
1425
  }
1169
- // src/ui/spinner.tsx
1170
- import { Text as Text6 } from "ink";
1171
- import { useEffect, useState as useState2 } from "react";
1172
- import { jsx as jsx7 } from "react/jsx-runtime";
1173
- var FRAMES = ["-", "\\", "|", "/"];
1174
- var INTERVAL_MS = 90;
1175
- function Spinner() {
1176
- const [frame, setFrame] = useState2(0);
1177
- useEffect(() => {
1178
- const timer = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), INTERVAL_MS);
1179
- return () => clearInterval(timer);
1180
- }, []);
1181
- return /* @__PURE__ */ jsx7(Text6, {
1182
- color: tone.primary,
1183
- children: FRAMES[frame]
1426
+ async function readSession(reach = {}) {
1427
+ const { token, base } = await requireToken(reach.server);
1428
+ return sessionOf(reach.talk ?? fetch, base, token);
1429
+ }
1430
+ var REACH_TIMEOUT_MS = 5000;
1431
+ async function reachable(reach = {}, timeoutMs = REACH_TIMEOUT_MS) {
1432
+ const base = serverUrl(await readConfig(), reach.server);
1433
+ try {
1434
+ const res = await (reach.talk ?? fetch)(`${authBase(base)}/ok`, {
1435
+ signal: AbortSignal.timeout(timeoutMs)
1436
+ });
1437
+ return { base, status: res.status };
1438
+ } catch (error) {
1439
+ return {
1440
+ base,
1441
+ status: null,
1442
+ said: error instanceof Error ? error.message : String(error)
1443
+ };
1444
+ }
1445
+ }
1446
+ async function rememberOrganization(name) {
1447
+ try {
1448
+ const config = await readConfig();
1449
+ if ((config.orgName ?? null) === name)
1450
+ return;
1451
+ await writeConfig(name === null ? { ...config, orgName: undefined } : { ...config, orgName: name });
1452
+ } catch {}
1453
+ }
1454
+ async function organizations(reach = {}) {
1455
+ const { token, base } = await requireToken(reach.server);
1456
+ const talk = reach.talk ?? fetch;
1457
+ const [who, list] = await Promise.allSettled([
1458
+ sessionOf(talk, base, token),
1459
+ listOrganizations(talk, base, token)
1460
+ ]);
1461
+ if (who.status === "rejected")
1462
+ throw who.reason;
1463
+ if (list.status === "rejected")
1464
+ throw list.reason;
1465
+ const active = list.value.find((o) => o.id === who.value.organizationId) ?? null;
1466
+ await rememberOrganization(active?.name ?? null);
1467
+ return { all: list.value, active };
1468
+ }
1469
+ async function switchOrganization(name, reach = {}) {
1470
+ const { token, base } = await requireToken(reach.server);
1471
+ const talk = reach.talk ?? fetch;
1472
+ const all = await listOrganizations(talk, base, token);
1473
+ const wanted = name.toLowerCase();
1474
+ const match = all.find((o) => o.name.toLowerCase() === wanted || o.slug?.toLowerCase() === wanted);
1475
+ if (!match)
1476
+ return { kind: "unknown", offered: all };
1477
+ const res = await talk(`${authBase(base)}/organization/set-active`, {
1478
+ method: "POST",
1479
+ headers: { "content-type": "application/json", ...bearer(token) },
1480
+ body: JSON.stringify({ organizationId: match.id })
1184
1481
  });
1482
+ if (!res.ok) {
1483
+ const detail = (await res.text()).slice(0, 200);
1484
+ return {
1485
+ kind: "refused",
1486
+ organization: match,
1487
+ detail: detail || `${res.status} ${res.statusText}`
1488
+ };
1489
+ }
1490
+ await rememberOrganization(match.name);
1491
+ return { kind: "switched", organization: match };
1185
1492
  }
1493
+
1186
1494
  // src/auth/screens/login.tsx
1187
1495
  import { Box as Box5, Text as Text7, useApp } from "ink";
1188
1496
  import { useEffect as useEffect2, useState as useState3 } from "react";
@@ -1533,7 +1841,7 @@ import { useCallback as useCallback2, useEffect as useEffect6, useMemo as useMem
1533
1841
 
1534
1842
  // src/manifest/manifest.ts
1535
1843
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
1536
- import { dirname as dirname2, join as join4, resolve as resolve2 } from "node:path";
1844
+ import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
1537
1845
  function emptyManifest(scope) {
1538
1846
  return { version: 1, scope, agents: [], bundles: {}, skills: {} };
1539
1847
  }
@@ -1561,7 +1869,7 @@ function readManifest(path) {
1561
1869
  };
1562
1870
  }
1563
1871
  function writeManifest(path, m) {
1564
- mkdirSync2(dirname2(path), { recursive: true });
1872
+ mkdirSync2(dirname3(path), { recursive: true });
1565
1873
  writeFileSync(path, `${JSON.stringify(m, null, 2)}
1566
1874
  `);
1567
1875
  }
@@ -1637,7 +1945,7 @@ async function nearestManifest(flag, cwd) {
1637
1945
  // src/install/install.ts
1638
1946
  import { spawn } from "node:child_process";
1639
1947
  import { createRequire } from "node:module";
1640
- import { dirname as dirname3, join as join5 } from "node:path";
1948
+ import { dirname as dirname4, join as join5 } from "node:path";
1641
1949
  function skillsAddArgv(location, opts = {}) {
1642
1950
  const path = location.path.replace(/^\/+|\/+$/g, "");
1643
1951
  const at = location.ref ? `#${location.ref}` : "";
@@ -1671,7 +1979,7 @@ function skillsBin() {
1671
1979
  if (!bin) {
1672
1980
  throw new Error("The bundled `skills` package declares no `skills` binary. Re-install yourskills.");
1673
1981
  }
1674
- return join5(dirname3(pkgPath), bin);
1982
+ return join5(dirname4(pkgPath), bin);
1675
1983
  }
1676
1984
  function runSkills(argv) {
1677
1985
  const bin = skillsBin();
@@ -1703,9 +2011,9 @@ function runSkills(argv) {
1703
2011
  }
1704
2012
  function pendingMessage(pending) {
1705
2013
  const where = `${pending.repo}/${pending.path}`;
1706
- return pending.prUrl ? `${pending.skillName} not in your catalog yet — opened a pull request against ${where}
2014
+ return `${pending.skillName} not in your catalog yet — opened a pull request against ${where}
1707
2015
  ` + ` ${pending.prUrl}
1708
- ` + " Merge it, then run this command again." : `${pending.skillName} not in your catalog yet — written to ${where}. ` + "Re-run once it has been indexed.";
2016
+ ` + " Merge it, then run this command again.";
1709
2017
  }
1710
2018
  function licenceQuestionMessage(name, licence) {
1711
2019
  const repos = [...new Set(licence.upstreams.map((one) => one.repo))];
@@ -2185,7 +2493,7 @@ function InstallLine({ line, width }) {
2185
2493
  })
2186
2494
  ]
2187
2495
  }),
2188
- line.vendor.prUrl ? /* @__PURE__ */ jsxs9(Text9, {
2496
+ /* @__PURE__ */ jsxs9(Text9, {
2189
2497
  children: [
2190
2498
  /* @__PURE__ */ jsx10(Text9, {
2191
2499
  children: " "
@@ -2195,7 +2503,7 @@ function InstallLine({ line, width }) {
2195
2503
  children: line.vendor.prUrl
2196
2504
  })
2197
2505
  ]
2198
- }) : null
2506
+ })
2199
2507
  ]
2200
2508
  });
2201
2509
  case "question":
@@ -2420,6 +2728,7 @@ function NoticeScreen({
2420
2728
  }
2421
2729
  var COLOURS = {
2422
2730
  ok: "green",
2731
+ warn: tone.notice,
2423
2732
  fail: tone.danger,
2424
2733
  pending: tone.notice,
2425
2734
  none: undefined
@@ -3298,7 +3607,7 @@ function shellFromEnv(shell) {
3298
3607
  // src/doctor/doctor.ts
3299
3608
  import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
3300
3609
  import { homedir as homedir4 } from "node:os";
3301
- import { dirname as dirname5, join as join8 } from "node:path";
3610
+ import { dirname as dirname6, join as join8 } from "node:path";
3302
3611
 
3303
3612
  // ../../node_modules/.bun/@trpc+client@11.18.0+acc6907a906fa511/node_modules/@trpc/client/dist/objectSpread2-BvkFp-_Y.mjs
3304
3613
  var __create = Object.create;
@@ -4728,7 +5037,7 @@ var EXIT_CODES = [
4728
5037
  [EXIT.usage, "the command line was wrong; the last line says the spelling"],
4729
5038
  [EXIT.session, "needs a session: run `yourskills login`"]
4730
5039
  ];
4731
- var LOGIN = "yourskills login";
5040
+ var LOGIN2 = "yourskills login";
4732
5041
  function needsTerminal(kind) {
4733
5042
  if (kind === "login")
4734
5043
  return new CliError("login needs a terminal", {
@@ -4752,7 +5061,7 @@ function describeError(error) {
4752
5061
  const message = error instanceof Error ? error.message : String(error);
4753
5062
  if (isUnauthorized(error)) {
4754
5063
  return {
4755
- lines: shape("you are not signed in", message.startsWith("Session") ? "the registry no longer accepts the token on this machine" : "the catalog needs a session, and there is no token on this machine", LOGIN),
5064
+ lines: shape("you are not signed in", message.startsWith("Session") ? "the registry no longer accepts the token on this machine" : "the catalog needs a session, and there is no token on this machine", LOGIN2),
4756
5065
  code: EXIT.session
4757
5066
  };
4758
5067
  }
@@ -4871,7 +5180,7 @@ import { existsSync as existsSync2 } from "node:fs";
4871
5180
  // src/hooks/settings.ts
4872
5181
  import { mkdirSync as mkdirSync3 } from "node:fs";
4873
5182
  import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
4874
- import { dirname as dirname4 } from "node:path";
5183
+ import { dirname as dirname5 } from "node:path";
4875
5184
  function binaryName(command) {
4876
5185
  const parts = command.split(/[\\/]/);
4877
5186
  return (parts[parts.length - 1] ?? "").replace(/\.(js|mjs|cjs|exe)$/i, "");
@@ -4993,7 +5302,7 @@ function serialiseSettings(settings, indent) {
4993
5302
  `;
4994
5303
  }
4995
5304
  async function writeSettingsFile(path, settings, indent) {
4996
- mkdirSync3(dirname4(path), { recursive: true });
5305
+ mkdirSync3(dirname5(path), { recursive: true });
4997
5306
  await writeFile2(path, serialiseSettings(settings, indent));
4998
5307
  }
4999
5308
  async function applyTo(path, change) {
@@ -5778,7 +6087,7 @@ async function hooksStatus() {
5778
6087
  { mark: "none", text: "tracking", note: tracking.note }
5779
6088
  ];
5780
6089
  const agents = [];
5781
- for (const agent of agentsToReport(config.agents ?? [])) {
6090
+ for (const agent of agentsToReport(await resolveAgents())) {
5782
6091
  const adapter = adapterFor(agent);
5783
6092
  if (!adapter) {
5784
6093
  lines.push({
@@ -5843,7 +6152,7 @@ import { realpathSync } from "node:fs";
5843
6152
  // package.json
5844
6153
  var package_default = {
5845
6154
  name: "yourskills",
5846
- version: "0.4.4",
6155
+ version: "0.5.0",
5847
6156
  description: "Browse, install and keep up to date the agent skills and bundles your team publishes on yourskills.",
5848
6157
  license: "UNLICENSED",
5849
6158
  homepage: "https://yourskills.store",
@@ -6288,40 +6597,6 @@ async function runSessionStart() {
6288
6597
  const outcome = await sessionStart();
6289
6598
  emit(outcome, await upgradeCheck());
6290
6599
  }
6291
- // src/org/wire.ts
6292
- var authBase2 = (base) => `${base.replace(/\/$/, "")}/api/auth`;
6293
- async function activeOrganizationId(base, token) {
6294
- const res = await fetch(`${authBase2(base)}/get-session`, {
6295
- headers: { authorization: `Bearer ${token}` }
6296
- });
6297
- if (!res.ok)
6298
- return null;
6299
- const body = await res.json();
6300
- return body?.session?.activeOrganizationId ?? null;
6301
- }
6302
- async function listOrganizations(base, token) {
6303
- let res;
6304
- try {
6305
- res = await fetch(`${authBase2(base)}/organization/list`, {
6306
- headers: { authorization: `Bearer ${token}` }
6307
- });
6308
- } catch (error) {
6309
- throw new Error(`Could not reach ${base}: ${error instanceof Error ? error.message : String(error)}`);
6310
- }
6311
- if (!res.ok) {
6312
- throw new Error(`Could not list organizations: ${res.status} ${res.statusText}`);
6313
- }
6314
- return await res.json();
6315
- }
6316
- async function rememberOrganization(name) {
6317
- try {
6318
- const config = await readConfig();
6319
- if ((config.orgName ?? null) === name)
6320
- return;
6321
- await writeConfig(name === null ? { ...config, orgName: undefined } : { ...config, orgName: name });
6322
- } catch {}
6323
- }
6324
-
6325
6600
  // src/org/org.ts
6326
6601
  var NO_ACTIVE_ORGANIZATION = "none: choose one with `yourskills org <name>`, or in the web app";
6327
6602
  async function namedOrganization(server) {
@@ -6330,8 +6605,8 @@ async function namedOrganization(server) {
6330
6605
  return view.organization;
6331
6606
  }
6332
6607
  async function whoamiReport(server) {
6333
- const who = await whoami(server);
6334
- const organization = who.org ? await namedOrganization(server) : null;
6608
+ const who = await readSession({ server });
6609
+ const organization = who.organizationId ? await namedOrganization(server) : null;
6335
6610
  await rememberOrganization(organization?.name ?? null);
6336
6611
  return drawWhoami({ user: who.user, server: who.server, organization });
6337
6612
  }
@@ -6369,152 +6644,167 @@ function rows(orgs, active) {
6369
6644
  }
6370
6645
  async function learnActiveOrganization(server) {
6371
6646
  try {
6372
- const { token, base } = await requireToken(server);
6373
- const [orgs, active] = await Promise.all([
6374
- listOrganizations(base, token),
6375
- activeOrganizationId(base, token)
6376
- ]);
6377
- const name = orgs.find((o) => o.id === active)?.name ?? null;
6378
- await rememberOrganization(name);
6379
- return name;
6647
+ return (await organizations({ server })).active?.name ?? null;
6380
6648
  } catch {
6381
6649
  return null;
6382
6650
  }
6383
6651
  }
6384
6652
  async function organizationChoices(server) {
6385
- const { token, base } = await requireToken(server);
6386
- const [orgs, active] = await Promise.all([
6387
- listOrganizations(base, token),
6388
- activeOrganizationId(base, token)
6389
- ]);
6390
- await rememberOrganization(orgs.find((o) => o.id === active)?.name ?? null);
6391
- return orgs.map((org) => ({
6653
+ const { all, active } = await organizations({ server });
6654
+ return all.map((org) => ({
6392
6655
  name: org.name,
6393
6656
  slug: org.slug ?? null,
6394
- active: org.id === active
6657
+ active: org.id === active?.id
6395
6658
  }));
6396
6659
  }
6397
6660
  async function orgListReport(server) {
6398
- const { token, base } = await requireToken(server);
6399
- const [orgs, active] = await Promise.all([
6400
- listOrganizations(base, token),
6401
- activeOrganizationId(base, token)
6402
- ]);
6403
- await rememberOrganization(orgs.find((o) => o.id === active)?.name ?? null);
6661
+ const { all, active } = await organizations({ server });
6404
6662
  return {
6405
6663
  title: "org",
6406
- lines: rows(orgs, active),
6664
+ lines: rows(all, active?.id ?? null),
6407
6665
  json: {
6408
- active,
6409
- organizations: orgs.map((o) => ({
6666
+ active: active?.id ?? null,
6667
+ organizations: all.map((o) => ({
6410
6668
  id: o.id,
6411
6669
  name: o.name,
6412
6670
  slug: o.slug ?? null,
6413
- active: o.id === active
6671
+ active: o.id === active?.id
6414
6672
  }))
6415
6673
  }
6416
6674
  };
6417
6675
  }
6418
- async function orgSwitchReport(name, server) {
6419
- const { token, base } = await requireToken(server);
6420
- const orgs = await listOrganizations(base, token);
6421
- const wanted = name.toLowerCase();
6422
- const match = orgs.find((o) => o.name.toLowerCase() === wanted || o.slug?.toLowerCase() === wanted);
6423
- if (!match) {
6424
- return {
6425
- title: "org",
6426
- lines: [
6427
- {
6428
- mark: "fail",
6429
- text: `No organization called ${name}.`,
6430
- note: orgs.map((o) => o.name).join(", ") || "you are in none"
6676
+ function drawSwitch(name, outcome) {
6677
+ switch (outcome.kind) {
6678
+ case "unknown":
6679
+ return {
6680
+ title: "org",
6681
+ lines: [
6682
+ {
6683
+ mark: "fail",
6684
+ text: `No organization called ${name}.`,
6685
+ note: outcome.offered.map((o) => o.name).join(", ") || "you are in none"
6686
+ }
6687
+ ],
6688
+ json: { error: `unknown organization: ${name}` },
6689
+ failed: true
6690
+ };
6691
+ case "refused":
6692
+ return {
6693
+ title: "org",
6694
+ lines: [
6695
+ {
6696
+ mark: "fail",
6697
+ text: `Could not switch to ${outcome.organization.name}.`,
6698
+ note: outcome.detail
6699
+ }
6700
+ ],
6701
+ json: { error: outcome.detail },
6702
+ failed: true
6703
+ };
6704
+ case "switched": {
6705
+ const { organization } = outcome;
6706
+ return {
6707
+ title: "org",
6708
+ lines: [
6709
+ { mark: "ok", text: `Active organization: ${organization.name}` }
6710
+ ],
6711
+ json: {
6712
+ active: organization.id,
6713
+ name: organization.name,
6714
+ slug: organization.slug ?? null
6431
6715
  }
6432
- ],
6433
- json: { error: `unknown organization: ${name}` },
6434
- failed: true
6435
- };
6716
+ };
6717
+ }
6436
6718
  }
6437
- const res = await fetch(`${authBase2(base)}/organization/set-active`, {
6438
- method: "POST",
6439
- headers: {
6440
- "content-type": "application/json",
6441
- authorization: `Bearer ${token}`
6442
- },
6443
- body: JSON.stringify({ organizationId: match.id })
6444
- });
6445
- if (!res.ok) {
6446
- const detail = (await res.text()).slice(0, 200);
6719
+ }
6720
+ async function orgSwitchReport(name, server) {
6721
+ return drawSwitch(name, await switchOrganization(name, { server }));
6722
+ }
6723
+ // src/doctor/check.ts
6724
+ function fixable(checks) {
6725
+ return checks.filter((check) => check.state === "fail" && check.fix?.apply !== undefined);
6726
+ }
6727
+ var MARK = {
6728
+ pass: "ok",
6729
+ warn: "warn",
6730
+ fail: "fail"
6731
+ };
6732
+ function markOf(state) {
6733
+ return MARK[state];
6734
+ }
6735
+ function blocks(checks) {
6736
+ return checks.some((check) => check.state === "fail");
6737
+ }
6738
+ function nextFor2(checks, fixing) {
6739
+ const fixes = fixable(checks);
6740
+ if (!fixing && fixes.length > 0)
6447
6741
  return {
6448
- title: "org",
6449
- lines: [
6450
- {
6451
- mark: "fail",
6452
- text: `Could not switch to ${match.name}.`,
6453
- note: detail || `${res.status} ${res.statusText}`
6454
- }
6455
- ],
6456
- json: { error: detail || res.statusText },
6457
- failed: true
6742
+ command: "yourskills doctor --fix",
6743
+ why: `repair ${fixes.length === 1 ? "it" : "them"}`
6458
6744
  };
6459
- }
6460
- await rememberOrganization(match.name);
6461
- return {
6462
- title: "org",
6463
- lines: [{ mark: "ok", text: `Active organization: ${match.name}` }],
6464
- json: { active: match.id, name: match.name, slug: match.slug ?? null }
6465
- };
6745
+ const names = (name) => checks.some((check) => check.fix?.name === name);
6746
+ if (names("login"))
6747
+ return { command: "yourskills login", why: "start a session" };
6748
+ if (names("upgrade"))
6749
+ return { command: "yourskills upgrade", why: "get the newer yourskills" };
6750
+ return;
6466
6751
  }
6752
+
6467
6753
  // src/doctor/doctor.ts
6468
- var REACH_TIMEOUT_MS = 5000;
6469
6754
  var liveDiagnosis = async (server) => (await createClient(server)).downstream.diagnosis.query();
6470
- function withLogin(message) {
6471
- return message.includes("yourskills login") ? message : `${message} — run \`yourskills login\``;
6472
- }
6473
6755
  function home() {
6474
6756
  const env = process.env.HOME;
6475
6757
  return env && env.length > 0 ? env : homedir4();
6476
6758
  }
6477
- async function reachable(base) {
6478
- const url = `${base.replace(/\/$/, "")}/api/auth/ok`;
6479
- try {
6480
- const res = await fetch(url, {
6481
- signal: AbortSignal.timeout(REACH_TIMEOUT_MS)
6482
- });
6483
- return {
6484
- name: "server reachable",
6485
- ok: true,
6486
- detail: `${base} (${res.status})`
6487
- };
6488
- } catch (error) {
6489
- return {
6490
- name: "server reachable",
6491
- ok: false,
6492
- detail: `${base}: ${error instanceof Error ? error.message : String(error)}`
6493
- };
6494
- }
6759
+ function said(error) {
6760
+ return error instanceof Error ? error.message : String(error);
6761
+ }
6762
+ async function reached(server) {
6763
+ const answer = await reachable({ server });
6764
+ return answer.status === null ? {
6765
+ name: "server reachable",
6766
+ state: "fail",
6767
+ summary: `${answer.base}: ${answer.said}`,
6768
+ fix: null
6769
+ } : {
6770
+ name: "server reachable",
6771
+ state: "pass",
6772
+ summary: `${answer.base} (${answer.status})`,
6773
+ fix: null
6774
+ };
6495
6775
  }
6496
6776
  async function session(ask, server) {
6497
6777
  try {
6498
- const who = await whoami(server);
6778
+ const who = await readSession({ server });
6779
+ const id = who.organizationId;
6499
6780
  return [
6500
- { name: "session valid", ok: true, detail: who.user },
6781
+ { name: "session valid", state: "pass", summary: who.user, fix: null },
6501
6782
  {
6502
6783
  name: "active organization",
6503
- ok: who.org !== null,
6504
- detail: who.org ? (await ask(server).catch(() => null))?.name ?? who.org : NO_ACTIVE_ORGANIZATION
6784
+ state: id ? "pass" : "fail",
6785
+ summary: id ? (await ask(server).catch(() => null))?.name ?? id : NO_ACTIVE_ORGANIZATION,
6786
+ fix: null
6505
6787
  }
6506
6788
  ];
6507
6789
  } catch (error) {
6790
+ const refused = isSessionRefusal(error);
6508
6791
  return [
6509
- {
6792
+ refused ? {
6793
+ name: "session valid",
6794
+ state: "fail",
6795
+ summary: error.fact,
6796
+ fix: { name: "login", sentence: `run \`${error.next}\`` }
6797
+ } : {
6510
6798
  name: "session valid",
6511
- ok: false,
6512
- detail: withLogin(error instanceof Error ? error.message : String(error))
6799
+ state: "fail",
6800
+ summary: said(error),
6801
+ fix: null
6513
6802
  },
6514
6803
  {
6515
6804
  name: "active organization",
6516
- ok: false,
6517
- detail: "unknown until you are logged in"
6805
+ state: "fail",
6806
+ summary: refused ? "unknown until you are logged in" : "not asked: the session could not be read",
6807
+ fix: null
6518
6808
  }
6519
6809
  ];
6520
6810
  }
@@ -6522,27 +6812,42 @@ async function session(ask, server) {
6522
6812
  function configParses() {
6523
6813
  const path = configPath();
6524
6814
  if (!existsSync5(path))
6525
- return { name: "config", ok: true, detail: `${path} (not created yet)` };
6815
+ return {
6816
+ name: "config",
6817
+ state: "pass",
6818
+ summary: `${path} (not created yet)`,
6819
+ fix: null
6820
+ };
6526
6821
  try {
6527
6822
  JSON.parse(readFileSync3(path, "utf8"));
6528
- return { name: "config", ok: true, detail: path };
6823
+ return { name: "config", state: "pass", summary: path, fix: null };
6529
6824
  } catch (error) {
6530
6825
  return {
6531
6826
  name: "config",
6532
- ok: false,
6533
- detail: `${path}: ${error instanceof Error ? error.message : String(error)} — fix it by hand, or move it aside and log in again`
6827
+ state: "fail",
6828
+ summary: `${path}: ${said(error)}`,
6829
+ fix: {
6830
+ name: "by-hand",
6831
+ sentence: "fix it by hand, or move it aside and log in again"
6832
+ }
6534
6833
  };
6535
6834
  }
6536
6835
  }
6537
6836
  function skillsResolvable() {
6538
6837
  try {
6539
6838
  const bin = skillsBin();
6540
- return existsSync5(bin) ? { name: "skills dependency", ok: true, detail: bin } : { name: "skills dependency", ok: false, detail: `missing: ${bin}` };
6839
+ return existsSync5(bin) ? { name: "skills dependency", state: "pass", summary: bin, fix: null } : {
6840
+ name: "skills dependency",
6841
+ state: "fail",
6842
+ summary: `missing: ${bin}`,
6843
+ fix: null
6844
+ };
6541
6845
  } catch (error) {
6542
6846
  return {
6543
6847
  name: "skills dependency",
6544
- ok: false,
6545
- detail: error instanceof Error ? error.message : String(error)
6848
+ state: "fail",
6849
+ summary: said(error),
6850
+ fix: null
6546
6851
  };
6547
6852
  }
6548
6853
  }
@@ -6551,7 +6856,7 @@ async function agents(nearest, local) {
6551
6856
  const found = local ? [] : [
6552
6857
  ...new Set((await listInstalled(nearest.scope)).flatMap((row) => row.agents))
6553
6858
  ];
6554
- const said = [
6859
+ const sentences = [
6555
6860
  pinned.length > 0 ? `pinned: ${pinned.join(", ")}` : "",
6556
6861
  found.length > 0 ? `found: ${found.join(", ")} (the stock CLI's labels)` : ""
6557
6862
  ].filter(Boolean);
@@ -6562,15 +6867,25 @@ async function agents(nearest, local) {
6562
6867
  const hint = suggest.length > 0 ? `did you mean ${suggest.join(", ")}?` : "run `yourskills config set agents <name>` with a name the stock CLI knows";
6563
6868
  return {
6564
6869
  name: "agents",
6565
- ok: false,
6566
- detail: `no agent is called ${unknown.join(", ")} — every install would stop at it; ${hint}`
6870
+ state: "fail",
6871
+ summary: `no agent is called ${unknown.join(", ")}: every install would stop at it`,
6872
+ fix: { name: "pin-agent", sentence: hint }
6567
6873
  };
6568
6874
  }
6569
6875
  }
6570
- return {
6876
+ return sentences.length > 0 ? {
6877
+ name: "agents",
6878
+ state: "pass",
6879
+ summary: sentences.join(" "),
6880
+ fix: null
6881
+ } : {
6571
6882
  name: "agents",
6572
- ok: said.length > 0,
6573
- detail: said.length > 0 ? said.join(" ") : "none — pin one with `yourskills config set agents claude-code`"
6883
+ state: "fail",
6884
+ summary: "none",
6885
+ fix: {
6886
+ name: "pin-agent",
6887
+ sentence: "pin one with `yourskills config set agents claude-code`"
6888
+ }
6574
6889
  };
6575
6890
  }
6576
6891
  function hookCheck(gap) {
@@ -6578,22 +6893,28 @@ function hookCheck(gap) {
6578
6893
  if (gap.error)
6579
6894
  return {
6580
6895
  name,
6581
- ok: false,
6582
- detail: `${gap.path}: ${gap.error} — left untouched; fix the JSON by hand`
6896
+ state: "fail",
6897
+ summary: `${gap.path}: ${gap.error}`,
6898
+ fix: {
6899
+ name: "by-hand",
6900
+ sentence: "left untouched; fix the JSON by hand"
6901
+ }
6583
6902
  };
6584
6903
  if (gap.missing.length === 0)
6585
6904
  return {
6586
6905
  name,
6587
- ok: true,
6588
- detail: `${gap.registered.join(" + ")} ${gap.path}`
6906
+ state: "pass",
6907
+ summary: `${gap.registered.join(" + ")} ${gap.path}`,
6908
+ fix: null
6589
6909
  };
6590
6910
  const kinds = gap.missing.join(" + ");
6591
6911
  return {
6592
6912
  name,
6593
- ok: false,
6594
- detail: `${kinds} not registered ${gap.path}`,
6913
+ state: "fail",
6914
+ summary: `${kinds} not registered`,
6595
6915
  fix: {
6596
- what: `register ${kinds} in ${gap.path}`,
6916
+ name: "register-hooks",
6917
+ sentence: `register ${kinds} in ${gap.path}`,
6597
6918
  apply: () => registerMissing(gap)
6598
6919
  }
6599
6920
  };
@@ -6602,26 +6923,30 @@ function manifestParses(nearest) {
6602
6923
  if (!existsSync5(nearest.path)) {
6603
6924
  return {
6604
6925
  name: "manifest",
6605
- ok: true,
6606
- detail: `${nearest.path} (not created yet)`
6926
+ state: "pass",
6927
+ summary: `${nearest.path} (not created yet)`,
6928
+ fix: null
6607
6929
  };
6608
6930
  }
6609
6931
  try {
6610
6932
  const raw = JSON.parse(readFileSync3(nearest.path, "utf8"));
6611
6933
  return raw && typeof raw === "object" ? {
6612
6934
  name: "manifest",
6613
- ok: true,
6614
- detail: `${nearest.path} (${nearest.scope})`
6935
+ state: "pass",
6936
+ summary: `${nearest.path} (${nearest.scope})`,
6937
+ fix: null
6615
6938
  } : {
6616
6939
  name: "manifest",
6617
- ok: false,
6618
- detail: `${nearest.path}: not an object`
6940
+ state: "fail",
6941
+ summary: `${nearest.path}: not an object`,
6942
+ fix: null
6619
6943
  };
6620
6944
  } catch (error) {
6621
6945
  return {
6622
6946
  name: "manifest",
6623
- ok: false,
6624
- detail: `${nearest.path}: ${error instanceof Error ? error.message : String(error)}`
6947
+ state: "fail",
6948
+ summary: `${nearest.path}: ${said(error)}`,
6949
+ fix: null
6625
6950
  };
6626
6951
  }
6627
6952
  }
@@ -6629,7 +6954,7 @@ function onDisk(nearest, name) {
6629
6954
  const entry = nearest.manifest.skills[name];
6630
6955
  if (entry?.unpinned)
6631
6956
  return existsSync5(entry.path);
6632
- const root = nearest.scope === "project" ? dirname5(nearest.path) : home();
6957
+ const root = nearest.scope === "project" ? dirname6(nearest.path) : home();
6633
6958
  return existsSync5(join8(root, ".agents", "skills", name));
6634
6959
  }
6635
6960
  function manifestMatchesDisk(nearest) {
@@ -6638,16 +6963,18 @@ function manifestMatchesDisk(nearest) {
6638
6963
  if (missing.length === 0)
6639
6964
  return {
6640
6965
  name: "manifest matches disk",
6641
- ok: true,
6642
- detail: `${names.length} skill(s) recorded`
6966
+ state: "pass",
6967
+ summary: `${names.length} skill(s) recorded`,
6968
+ fix: null
6643
6969
  };
6644
6970
  const list = missing.join(", ");
6645
6971
  return {
6646
6972
  name: "manifest matches disk",
6647
- ok: false,
6648
- detail: `missing on disk: ${list} — run \`yourskills install\` to restore, or \`doctor --fix\` to forget`,
6973
+ state: "fail",
6974
+ summary: `missing on disk: ${list}`,
6649
6975
  fix: {
6650
- what: `forget ${list} in ${nearest.path}`,
6976
+ name: "forget",
6977
+ sentence: `forget ${list} in ${nearest.path}, or \`yourskills install\` to restore ${missing.length === 1 ? "it" : "them"}`,
6651
6978
  apply: async () => {
6652
6979
  openManifest({ path: nearest.path, scope: nearest.scope }).forget(missing);
6653
6980
  return `forgot ${list}; \`yourskills add <name>\` brings one back`;
@@ -6661,8 +6988,9 @@ function unpinned(nearest) {
6661
6988
  return null;
6662
6989
  return {
6663
6990
  name: "unpinned",
6664
- ok: true,
6665
- detail: `${names.join(", ")} — on this machine, not in your catalog; nothing keeps ${names.length === 1 ? "it" : "them"} up to date`
6991
+ state: "warn",
6992
+ summary: `${names.join(", ")}: on this machine, not in your catalog; nothing keeps ${names.length === 1 ? "it" : "them"} up to date`,
6993
+ fix: null
6666
6994
  };
6667
6995
  }
6668
6996
  async function downstreamChecks(ask, server) {
@@ -6673,8 +7001,9 @@ async function downstreamChecks(ask, server) {
6673
7001
  return [
6674
7002
  {
6675
7003
  name: "downstream",
6676
- ok: false,
6677
- detail: `could not ask the registry: ${error instanceof Error ? error.message : String(error)}`
7004
+ state: "fail",
7005
+ summary: `could not ask the registry: ${said(error)}`,
7006
+ fix: null
6678
7007
  }
6679
7008
  ];
6680
7009
  }
@@ -6682,21 +7011,24 @@ async function downstreamChecks(ask, server) {
6682
7011
  return [
6683
7012
  {
6684
7013
  name: "downstream",
6685
- ok: false,
6686
- detail: `${answer.headline} — nothing you install can be vendored until one is set`
7014
+ state: "fail",
7015
+ summary: `${answer.headline}: nothing you install can be vendored until one is set`,
7016
+ fix: null
6687
7017
  }
6688
7018
  ];
6689
7019
  }
6690
7020
  return [
6691
7021
  {
6692
7022
  name: "downstream",
6693
- ok: answer.tone !== "fail",
6694
- detail: `${answer.repo.repo} — ${answer.headline}`
7023
+ state: answer.tone,
7024
+ summary: `${answer.repo.repo}: ${answer.headline}`,
7025
+ fix: null
6695
7026
  },
6696
7027
  ...answer.checks.map((check) => ({
6697
7028
  name: `downstream: ${check.key}`,
6698
- ok: check.state !== "fail",
6699
- detail: check.fix ? `${check.summary} — ${check.fix.sentence}` : check.summary
7029
+ state: check.state,
7030
+ summary: check.summary,
7031
+ fix: check.fix
6700
7032
  }))
6701
7033
  ];
6702
7034
  }
@@ -6709,16 +7041,18 @@ async function unwatchedCheck(nearest, ask, server) {
6709
7041
  } catch (error) {
6710
7042
  return {
6711
7043
  name,
6712
- ok: false,
6713
- detail: `could not ask the registry: ${error instanceof Error ? error.message : String(error)}`
7044
+ state: "fail",
7045
+ summary: `could not ask the registry: ${said(error)}`,
7046
+ fix: null
6714
7047
  };
6715
7048
  }
6716
7049
  const orphans = drift.unknown.filter((row) => row.reason === "orphaned");
6717
7050
  if (orphans.length === 0)
6718
7051
  return {
6719
7052
  name,
6720
- ok: true,
6721
- detail: "every installed skill came from a source this organization still connects"
7053
+ state: "pass",
7054
+ summary: "every installed skill came from a source this organization still connects",
7055
+ fix: null
6722
7056
  };
6723
7057
  const names = orphans.map((row) => row.name);
6724
7058
  const repos = [
@@ -6727,8 +7061,12 @@ async function unwatchedCheck(nearest, ask, server) {
6727
7061
  const one = names.length === 1;
6728
7062
  return {
6729
7063
  name,
6730
- ok: true,
6731
- detail: `${names.join(", ")}: installed from ${repos.join(", ")}, which this ` + "organization does not connect any more, so nothing is watching " + `${repos.length === 1 ? "it" : "them"} for changes. The ` + `${one ? "copy on this machine goes" : "copies on this machine go"} on ` + "working. A team member can reconnect the source or vendor the skill into " + "your catalog, and then `yourskills update --all` moves this machine onto it."
7064
+ state: "warn",
7065
+ summary: `${names.join(", ")}: installed from ${repos.join(", ")}, which this ` + "organization does not connect any more, so nothing is watching " + `${repos.length === 1 ? "it" : "them"} for changes. The ` + `${one ? "copy on this machine goes" : "copies on this machine go"} on ` + "working.",
7066
+ fix: {
7067
+ name: "unwatched",
7068
+ sentence: "A team member can reconnect the source or vendor the skill into your " + "catalog, and then `yourskills update --all` moves this machine onto it."
7069
+ }
6732
7070
  };
6733
7071
  }
6734
7072
  function version(config) {
@@ -6736,20 +7074,23 @@ function version(config) {
6736
7074
  if (latest && isNewer(latest, VERSION))
6737
7075
  return {
6738
7076
  name: "version",
6739
- ok: true,
6740
- detail: `${VERSION} — ${latest} is out; run \`yourskills upgrade\``
7077
+ state: "warn",
7078
+ summary: VERSION,
7079
+ fix: {
7080
+ name: "upgrade",
7081
+ sentence: `${latest} is out; run \`yourskills upgrade\``
7082
+ }
6741
7083
  };
6742
- return { name: "version", ok: true, detail: VERSION };
7084
+ return { name: "version", state: "pass", summary: VERSION, fix: null };
6743
7085
  }
6744
7086
  async function diagnose(nearest, options = {}) {
6745
7087
  const config = await readConfig();
6746
- const base = serverUrl(config, options.server);
6747
7088
  const local = options.local ?? false;
6748
7089
  const checks = [configParses()];
6749
7090
  if (!local) {
6750
7091
  const account = await session(options.askOrganization ?? namedOrganization, options.server);
6751
- checks.push(await reachable(base), ...account);
6752
- if (account[0].ok) {
7092
+ checks.push(await reached(options.server), ...account);
7093
+ if (account[0].state !== "fail") {
6753
7094
  checks.push(...await downstreamChecks(options.ask ?? liveDiagnosis, options.server), await unwatchedCheck(nearest, options.askDrift ?? liveDrift, options.server));
6754
7095
  }
6755
7096
  }
@@ -6762,53 +7103,41 @@ async function diagnose(nearest, options = {}) {
6762
7103
  checks.push(version(config));
6763
7104
  return checks;
6764
7105
  }
6765
- function fixable(checks) {
6766
- return checks.filter((check) => !check.ok && check.fix !== undefined);
6767
- }
6768
7106
  async function applyFixes(checks) {
6769
7107
  const out = [];
6770
7108
  for (const check of checks) {
6771
- if (check.ok || !check.fix) {
7109
+ const [own] = fixable([check]);
7110
+ if (!own) {
6772
7111
  out.push(check);
6773
7112
  continue;
6774
7113
  }
6775
- try {
6776
- const detail = await check.fix.apply();
6777
- out.push({ ...check, ok: true, fixed: { ok: true, detail } });
6778
- } catch (error) {
6779
- out.push({
6780
- ...check,
6781
- fixed: {
6782
- ok: false,
6783
- detail: error instanceof Error ? error.message : String(error)
6784
- }
6785
- });
6786
- }
7114
+ out.push(await applied(own));
6787
7115
  }
6788
7116
  return out;
6789
7117
  }
6790
- function nextFor2(checks, fixing) {
6791
- if (!fixing && fixable(checks).length > 0)
6792
- return {
6793
- command: "yourskills doctor --fix",
6794
- why: `repair ${fixable(checks).length === 1 ? "it" : "them"}`
6795
- };
6796
- if (checks.some((check) => check.name === "session valid" && !check.ok))
6797
- return { command: "yourskills login", why: "start a session" };
6798
- if (checks.some((check) => check.name === "version" && check.detail.includes("is out")))
6799
- return { command: "yourskills upgrade", why: "get the newer yourskills" };
6800
- return;
7118
+ async function applied(check) {
7119
+ try {
7120
+ const detail = await check.fix.apply();
7121
+ return { ...check, state: "pass", fixed: { ok: true, detail } };
7122
+ } catch (error) {
7123
+ return { ...check, fixed: { ok: false, detail: said(error) } };
7124
+ }
7125
+ }
7126
+ function noteFor(check, fixing) {
7127
+ if (check.fixed)
7128
+ return check.fixed.ok ? `fixed: ${check.fixed.detail}` : `${check.summary} could not fix: ${check.fixed.detail}`;
7129
+ if (!check.fix)
7130
+ return check.summary;
7131
+ const own = check.fix.apply && !fixing ? " [fixable]" : "";
7132
+ return `${check.summary} ${check.fix.sentence}${own}`;
6801
7133
  }
6802
7134
  function doctorLines(checks, fixing = false) {
6803
- const width = Math.max(...checks.map((check) => check.name.length));
6804
- const lines = checks.map((check) => {
6805
- const note = check.fixed ? check.fixed.ok ? `fixed: ${check.fixed.detail}` : `${check.detail} — could not fix: ${check.fixed.detail}` : check.fix && !fixing ? `${check.detail} [fixable]` : check.detail;
6806
- return {
6807
- mark: check.ok ? "ok" : "fail",
6808
- text: check.name.padEnd(width),
6809
- note
6810
- };
6811
- });
7135
+ const width = Math.max(...checks.map((check) => check.name.length));
7136
+ const lines = checks.map((check) => ({
7137
+ mark: markOf(check.state),
7138
+ text: check.name.padEnd(width),
7139
+ note: noteFor(check, fixing)
7140
+ }));
6812
7141
  const next = nextFor2(checks, fixing);
6813
7142
  return {
6814
7143
  title: fixing ? "doctor --fix" : "doctor",
@@ -6816,11 +7145,12 @@ function doctorLines(checks, fixing = false) {
6816
7145
  json: {
6817
7146
  checks: checks.map(({ fix, ...rest }) => ({
6818
7147
  ...rest,
6819
- fixable: fix !== undefined
7148
+ fix: fix ? { name: fix.name, sentence: fix.sentence } : null,
7149
+ fixable: fix?.apply !== undefined
6820
7150
  })),
6821
7151
  fixed: checks.filter((check) => check.fixed?.ok).map((check) => check.fixed?.detail)
6822
7152
  },
6823
- failed: checks.some((check) => !check.ok),
7153
+ failed: blocks(checks),
6824
7154
  ...next ? { next } : {}
6825
7155
  };
6826
7156
  }
@@ -6832,38 +7162,6 @@ async function doctorReport(nearest, options = {}) {
6832
7162
  return { ...report, json: { ...report.json, manifest: nearest.path } };
6833
7163
  }
6834
7164
  // src/doctor/explain.ts
6835
- import { existsSync as existsSync6 } from "node:fs";
6836
- import { dirname as dirname6 } from "node:path";
6837
- function scopeLayers(resolved, manifestScope, config) {
6838
- const project = resolved.manifestPath ? {
6839
- layer: "project",
6840
- value: manifestScope,
6841
- from: `yourskills.json at ${dirname6(resolved.manifestPath)}${manifestScope ? "" : " (declares no scope)"}`
6842
- } : { layer: "project", value: null, from: "no yourskills.json above here" };
6843
- return [
6844
- {
6845
- layer: "flag",
6846
- value: resolved.source === "flag" ? "given" : null,
6847
- from: "-g or -p on the command line"
6848
- },
6849
- project,
6850
- {
6851
- layer: "config",
6852
- value: config.defaultScope ?? null,
6853
- from: `defaultScope in ${configPath()}`
6854
- },
6855
- { layer: "default", value: "global", from: "built-in default" }
6856
- ];
6857
- }
6858
- function serverFrom(config, override) {
6859
- if (override)
6860
- return "--server on the command line";
6861
- if (process.env.YOURSKILLS_URL)
6862
- return "YOURSKILLS_URL in the environment";
6863
- if (config.serverUrl)
6864
- return `server in ${configPath()}`;
6865
- return "built-in default";
6866
- }
6867
7165
  var LABEL = 12;
6868
7166
  function row(label, value, from) {
6869
7167
  return {
@@ -6872,36 +7170,32 @@ function row(label, value, from) {
6872
7170
  note: `<- ${from}`
6873
7171
  };
6874
7172
  }
6875
- function losers(layers, winner) {
6876
- return layers.filter((layer) => layer.layer !== winner).map((layer) => ({
7173
+ var LOSER = LABEL + 22;
7174
+ function losers(setting) {
7175
+ return setting.layers.filter((layer) => layer.layer !== setting.layer).map((layer) => ({
6877
7176
  mark: "none",
6878
- text: `${"".padEnd(LABEL)}(${layer.layer}: ${layer.value ?? "unset"}`.padEnd(LABEL + 18),
7177
+ text: `${"".padEnd(LABEL)}(${layer.layer}: ${layer.value ?? "unset"}`.padEnd(LOSER),
6879
7178
  note: `${layer.from})`
6880
7179
  }));
6881
7180
  }
7181
+ function settingRows(settings) {
7182
+ return settings.flatMap((setting) => [
7183
+ row(setting.key, setting.value, setting.from),
7184
+ ...losers(setting)
7185
+ ]);
7186
+ }
6882
7187
  async function explainReport(server) {
6883
- const [config, resolved, nearest, queue] = await Promise.all([
6884
- readConfig(),
6885
- resolveScope({}),
7188
+ const [held, nearest, queue] = await Promise.all([
7189
+ holdSettings({
7190
+ flags: { server },
7191
+ askOrganization: () => organizationTracking(server)
7192
+ }),
6886
7193
  nearestManifest(),
6887
7194
  readQueue()
6888
7195
  ]);
6889
- const track = await trackingNow({
6890
- track: config.track,
6891
- configPath: configPath(),
6892
- askOrganization: () => organizationTracking(server)
6893
- });
6894
- const manifestScope = resolved.manifestPath && existsSync6(resolved.manifestPath) ? resolved.source === "project" ? resolved.scope : null : null;
6895
- const layers = scopeLayers(resolved, manifestScope, config);
6896
- const winner = layers.find((layer) => layer.layer === resolved.source);
6897
- const lines = [
6898
- row("scope", resolved.scope, winner?.from ?? resolved.source),
6899
- ...losers(layers, resolved.source),
6900
- row("agents", config.agents?.length ? config.agents.join(", ") : "(the stock CLI detects)", config.agents?.length ? `agents in ${configPath()}` : "nothing pinned; `skills` looks at what is installed"),
6901
- row("server", serverUrl(config, server), serverFrom(config, server)),
6902
- row("track", track.value, track.from),
6903
- row("autoUpdate", config.autoUpdate === false ? "off" : "on", config.autoUpdate === undefined ? "built-in default" : `autoUpdate in ${configPath()}`)
6904
- ];
7196
+ const settings = settingsShown(held);
7197
+ const tracking = trackingOf(held);
7198
+ const lines = settingRows(settings);
6905
7199
  const skills = Object.keys(nearest.manifest.skills).length;
6906
7200
  const bundles = Object.keys(nearest.manifest.bundles).length;
6907
7201
  const loose = looseSkills(nearest.manifest).length;
@@ -6938,16 +7232,19 @@ async function explainReport(server) {
6938
7232
  title: "config --explain",
6939
7233
  lines,
6940
7234
  json: {
6941
- path: configPath(),
6942
- scope: { value: resolved.scope, source: resolved.source, layers },
6943
- agents: config.agents ?? [],
6944
- server: {
6945
- value: serverUrl(config, server),
6946
- source: serverFrom(config, server)
6947
- },
6948
- track: track.value === "on",
6949
- trackingState: track.state,
6950
- autoUpdate: config.autoUpdate !== false,
7235
+ path: held.configPath,
7236
+ settings: Object.fromEntries(settings.map((setting) => [
7237
+ setting.key,
7238
+ {
7239
+ value: setting.json,
7240
+ layer: setting.layer,
7241
+ from: setting.from,
7242
+ layers: setting.layers,
7243
+ decided: setting.decided
7244
+ }
7245
+ ])),
7246
+ track: tracking.value === "on",
7247
+ trackingState: tracking.state,
6951
7248
  manifest: { path: nearest.path, skills, bundles, loose },
6952
7249
  queue: { path: queuePath(), events: queue.length },
6953
7250
  hooks: hooksJson
@@ -7225,7 +7522,25 @@ var TABLE = {
7225
7522
  hint: "org <name>",
7226
7523
  shows: (facts) => facts.feed.organizations === undefined ? "" : facts.feed.organizations <= 1 ? "only this one" : count2(facts.feed.organizations - 1, "other"),
7227
7524
  when: (facts) => facts.signedIn,
7228
- says: (facts) => facts.organization ? `switch this machine out of ${facts.organization}` : "choose which organization this machine installs from"
7525
+ says: (facts) => facts.organization ? `switch this machine out of ${facts.organization}` : "choose which organization this machine installs from",
7526
+ asks: {
7527
+ title: "org",
7528
+ verb: "switch",
7529
+ pick: "one",
7530
+ empty: "You are not a member of any organization yet.",
7531
+ choices: async (ctx, _facts, ask = organizationChoices) => {
7532
+ const orgs = await ask(ctx.server);
7533
+ const active = orgs.find((org) => org.active)?.name;
7534
+ return {
7535
+ rows: orgs.map((org) => ({
7536
+ name: org.name,
7537
+ description: org.slug ?? ""
7538
+ })),
7539
+ chosen: active ? [active] : []
7540
+ };
7541
+ },
7542
+ opens: (picked) => ({ kind: "org", name: picked[0] ?? "" })
7543
+ }
7229
7544
  }
7230
7545
  ],
7231
7546
  invitesHooks: false,
@@ -7298,6 +7613,7 @@ var TABLE = {
7298
7613
  label: "search",
7299
7614
  hint: "search <query>",
7300
7615
  when: (facts) => facts.signedIn,
7616
+ opens: { kind: "search", query: "" },
7301
7617
  says: () => "filter the same catalog by name and description"
7302
7618
  }
7303
7619
  ],
@@ -7367,7 +7683,21 @@ var TABLE = {
7367
7683
  label: "remove",
7368
7684
  hint: "remove <name...>",
7369
7685
  when: (facts) => facts.skills > 0,
7370
- says: (facts) => `pick which of the ${count2(facts.skills, "skill")} to delete from this machine; it asks before it does`
7686
+ says: (facts) => `pick which of the ${count2(facts.skills, "skill")} to delete from this machine; it asks before it does`,
7687
+ asks: {
7688
+ title: "remove",
7689
+ verb: "remove",
7690
+ pick: "many",
7691
+ empty: "Nothing installed here.",
7692
+ choices: async (ctx) => ({
7693
+ rows: installedRows(ctx.where).map((row) => ({
7694
+ name: row.name,
7695
+ description: `${row.bundle ?? "loose"} ${shortRef(row.ref)}`
7696
+ })),
7697
+ chosen: []
7698
+ }),
7699
+ opens: (picked) => ({ kind: "remove", names: [...picked] })
7700
+ }
7371
7701
  }
7372
7702
  ],
7373
7703
  invitesHooks: true,
@@ -7643,7 +7973,7 @@ var TABLE = {
7643
7973
  lines: fixes.map((check) => ({
7644
7974
  mark: "fail",
7645
7975
  text: check.name,
7646
- note: check.fix?.what ?? ""
7976
+ note: check.fix.sentence
7647
7977
  })),
7648
7978
  notes: [
7649
7979
  "Every other check stays as it is; a session or an upgrade is yours to run."
@@ -7689,14 +8019,62 @@ var TABLE = {
7689
8019
  label: "scope",
7690
8020
  hint: "config set scope project",
7691
8021
  shows: (facts) => facts.scope,
7692
- says: (facts) => `where installs land: ${facts.scope} right now`
8022
+ says: (facts) => `where installs land: ${facts.scope} right now`,
8023
+ asks: {
8024
+ title: "scope",
8025
+ verb: "set",
8026
+ pick: "one",
8027
+ empty: "",
8028
+ choices: async (_ctx, facts) => ({
8029
+ rows: [
8030
+ {
8031
+ name: "global",
8032
+ description: "installs land in your home directory, where every project sees them"
8033
+ },
8034
+ {
8035
+ name: "project",
8036
+ description: "installs land in this project's yourskills.json, and travel with the repo"
8037
+ }
8038
+ ],
8039
+ chosen: [facts.scope]
8040
+ }),
8041
+ opens: (picked) => ({
8042
+ kind: "config",
8043
+ action: "set",
8044
+ key: "scope",
8045
+ value: picked[0] ?? ""
8046
+ })
8047
+ }
7693
8048
  },
7694
8049
  {
7695
8050
  value: "setup.agents",
7696
8051
  label: "agents",
7697
8052
  hint: "config set agents <name>",
7698
8053
  shows: (facts) => facts.agents.length > 0 ? count2(facts.agents.length, "agent") : "unset",
7699
- says: (facts) => facts.agents.length > 0 ? `which agents an install writes for: ${facts.agents.join(", ")}` : "which agents an install writes for; nothing pinned, so `skills` looks"
8054
+ says: (facts) => facts.agents.length > 0 ? `which agents an install writes for: ${facts.agents.join(", ")}` : "which agents an install writes for; nothing pinned, so `skills` looks",
8055
+ asks: {
8056
+ title: "agents",
8057
+ verb: "set",
8058
+ pick: "many",
8059
+ empty: "Could not ask the `skills` CLI which agent names it accepts. Run `yourskills doctor`.",
8060
+ choices: async (_ctx, facts, ask = knownAgents) => {
8061
+ const valid = await ask() ?? [];
8062
+ const onDisk = new Set(facts.agentsOnDisk);
8063
+ return {
8064
+ rows: valid.map((name) => ({
8065
+ name,
8066
+ description: onDisk.has(name) ? "on this machine" : ""
8067
+ })),
8068
+ chosen: facts.agents
8069
+ };
8070
+ },
8071
+ opens: (picked) => ({
8072
+ kind: "config",
8073
+ action: "set",
8074
+ key: "agents",
8075
+ value: picked.join(",")
8076
+ })
8077
+ }
7700
8078
  },
7701
8079
  {
7702
8080
  value: "setup.explain",
@@ -8199,14 +8577,13 @@ function paletteGroups(facts) {
8199
8577
  }
8200
8578
  return groups;
8201
8579
  }
8202
- // src/shell/facts.ts
8203
- import { existsSync as existsSync7 } from "node:fs";
8580
+ // src/shell/machine.ts
8581
+ import { existsSync as existsSync6 } from "node:fs";
8204
8582
  async function paletteFacts(scope, server) {
8205
- const [account, nearest, config, agents] = await Promise.all([
8583
+ const [account, nearest, held] = await Promise.all([
8206
8584
  localAccount(server),
8207
8585
  nearestManifest(scope),
8208
- readConfig(),
8209
- resolveAgents()
8586
+ holdSettings({ flags: { server } })
8210
8587
  ]);
8211
8588
  const skills = Object.keys(nearest.manifest.skills).length;
8212
8589
  return {
@@ -8214,21 +8591,252 @@ async function paletteFacts(scope, server) {
8214
8591
  email: account.email,
8215
8592
  organization: account.organization,
8216
8593
  server: account.server,
8217
- firstRun: !account.signedIn && skills === 0 && !config.welcomed,
8218
- agentsOnDisk: HOOK_ADAPTERS.filter((adapter) => existsSync7(adapter.home())).map((adapter) => adapter.agent),
8219
- agents,
8594
+ firstRun: !account.signedIn && skills === 0 && !held.config.welcomed,
8595
+ agentsOnDisk: HOOK_ADAPTERS.filter((adapter) => existsSync6(adapter.home())).map((adapter) => adapter.agent),
8596
+ agents: agentsAnswer(held).value,
8220
8597
  scope: nearest.scope,
8221
8598
  manifest: nearest.path,
8222
8599
  skills,
8223
8600
  bundles: Object.keys(nearest.manifest.bundles).length,
8224
8601
  loose: looseSkills(nearest.manifest).length,
8225
- settings: CONFIG_KEYS.filter((key) => configValue(config, key) !== UNSET).length,
8602
+ settings: settingsShown(held).filter((setting) => setting.decided).length,
8226
8603
  feed: NO_FEED
8227
8604
  };
8228
8605
  }
8606
+ async function feedFacts(client, where, server, choices = organizationChoices) {
8607
+ const installed = Object.keys(where.manifest.skills).length > 0;
8608
+ const [skills, bundles, passes, drift, orgs] = await Promise.allSettled([
8609
+ client.skills.list.query({}),
8610
+ client.bundles.list.query({}),
8611
+ client.mirror.passes.query(),
8612
+ installed ? driftNow(client, where) : Promise.resolve(null),
8613
+ choices(server)
8614
+ ]);
8615
+ return {
8616
+ ...skills.status === "fulfilled" ? { skills: skills.value.length } : {},
8617
+ ...bundles.status === "fulfilled" ? { bundles: bundles.value.length } : {},
8618
+ ...passes.status === "fulfilled" ? { pending: passes.value.length } : {},
8619
+ ...drift.status === "fulfilled" && drift.value ? { outdated: drift.value.moved.length } : {},
8620
+ ...orgs.status === "fulfilled" ? { organizations: orgs.value.length } : {}
8621
+ };
8622
+ }
8623
+ var importProbe = async (where) => {
8624
+ const found = await listInstalled(where.scope);
8625
+ const recorded = new Set(Object.keys(where.manifest.skills));
8626
+ const news = found.filter((skill) => !recorded.has(skill.name)).length;
8627
+ if (found.length === 0)
8628
+ return "nothing on this machine to adopt";
8629
+ return news === 0 ? `${count2(found.length, "skill")} on this machine, all recorded here already` : `${count2(found.length, "skill")} on this machine, ${news} not recorded here`;
8630
+ };
8631
+ var doctorProbe = async (where, server) => {
8632
+ const checks = await diagnose(where, {
8633
+ local: true,
8634
+ ...server ? { server } : {}
8635
+ });
8636
+ const bad = checks.filter((check) => check.state === "fail");
8637
+ return bad.length === 0 ? `${count2(checks.length, "local check")}, all passing` : `${bad.length} of ${checks.length} local checks failing: ${bad.map((check) => check.name).join(", ")}`;
8638
+ };
8639
+ var PROBES = {
8640
+ "setup.import": importProbe,
8641
+ "setup.doctor": doctorProbe
8642
+ };
8643
+ function probeFor(value) {
8644
+ return value ? PROBES[value] : undefined;
8645
+ }
8646
+ var REST_MS = 250;
8647
+ var noop3 = () => {
8648
+ return;
8649
+ };
8650
+ function knowMachine(from, talkers = {}) {
8651
+ const { server } = from;
8652
+ const ask = {
8653
+ upgrade: talkers.upgrade ?? (() => upgradeCheck()),
8654
+ feed: talkers.feed ?? feedFacts,
8655
+ probes: talkers.probes ?? probeFor,
8656
+ learnOrganization: talkers.learnOrganization ?? learnActiveOrganization
8657
+ };
8658
+ const restMs = talkers.restMs ?? REST_MS;
8659
+ let where = from.where;
8660
+ let local = from.facts;
8661
+ let feed = from.facts.feed;
8662
+ let upgrade;
8663
+ let probes = {};
8664
+ let probing = null;
8665
+ let dead = false;
8666
+ let epoch = 0;
8667
+ const listeners = new Set;
8668
+ const pending = new Set;
8669
+ function fold() {
8670
+ return {
8671
+ where,
8672
+ facts: {
8673
+ ...local,
8674
+ feed: {
8675
+ ...feed,
8676
+ ...upgrade === undefined ? {} : { upgrade: upgrade ? upgrade.latest : null }
8677
+ }
8678
+ },
8679
+ upgrade,
8680
+ probes,
8681
+ probing
8682
+ };
8683
+ }
8684
+ let state = fold();
8685
+ function set() {
8686
+ state = fold();
8687
+ for (const listener of listeners)
8688
+ listener();
8689
+ }
8690
+ function track(work) {
8691
+ const done = work.then(noop3, noop3).then(() => {
8692
+ pending.delete(done);
8693
+ });
8694
+ pending.add(done);
8695
+ return done;
8696
+ }
8697
+ let held = null;
8698
+ async function client() {
8699
+ held ??= await from.client();
8700
+ return held;
8701
+ }
8702
+ function askUpgrade() {
8703
+ track(ask.upgrade().then((found) => {
8704
+ if (dead)
8705
+ return;
8706
+ upgrade = found;
8707
+ set();
8708
+ }));
8709
+ }
8710
+ function askFeed() {
8711
+ if (dead || !local.signedIn)
8712
+ return;
8713
+ const mine = epoch;
8714
+ track(client().then((live) => ask.feed(live, where, server)).then((found) => {
8715
+ if (dead || mine !== epoch)
8716
+ return;
8717
+ feed = found;
8718
+ set();
8719
+ }));
8720
+ }
8721
+ let resting;
8722
+ let timer = null;
8723
+ let wake = null;
8724
+ function unrest() {
8725
+ if (timer)
8726
+ clearTimeout(timer);
8727
+ timer = null;
8728
+ wake?.();
8729
+ wake = null;
8730
+ }
8731
+ function scan(value, probe) {
8732
+ const mine = epoch;
8733
+ probing = value;
8734
+ set();
8735
+ track(probe(where, server).then((text) => {
8736
+ if (dead || mine !== epoch)
8737
+ return;
8738
+ probes = { ...probes, [value]: text };
8739
+ }).catch(noop3).finally(() => {
8740
+ if (dead || mine !== epoch)
8741
+ return;
8742
+ if (probing === value)
8743
+ probing = null;
8744
+ set();
8745
+ }));
8746
+ }
8747
+ function rest(value) {
8748
+ resting = value;
8749
+ unrest();
8750
+ if (dead || !value)
8751
+ return;
8752
+ const probe = ask.probes(value);
8753
+ if (!probe || probes[value] !== undefined)
8754
+ return;
8755
+ let fired = false;
8756
+ track(new Promise((resolve) => {
8757
+ wake = resolve;
8758
+ timer = setTimeout(() => {
8759
+ fired = true;
8760
+ timer = null;
8761
+ wake = null;
8762
+ resolve();
8763
+ }, restMs);
8764
+ }).then(() => {
8765
+ if (fired && !dead)
8766
+ scan(value, probe);
8767
+ }));
8768
+ }
8769
+ async function reread() {
8770
+ const [nextFacts, nextWhere] = await Promise.allSettled([
8771
+ from.reload(),
8772
+ from.resite()
8773
+ ]);
8774
+ if (dead)
8775
+ return;
8776
+ if (nextFacts.status === "fulfilled")
8777
+ local = nextFacts.value;
8778
+ if (nextWhere.status === "fulfilled")
8779
+ where = nextWhere.value;
8780
+ if (!local.signedIn)
8781
+ feed = NO_FEED;
8782
+ set();
8783
+ askFeed();
8784
+ rest(resting);
8785
+ }
8786
+ function forgetScans() {
8787
+ epoch += 1;
8788
+ probes = {};
8789
+ probing = null;
8790
+ set();
8791
+ }
8792
+ function back() {
8793
+ forgetScans();
8794
+ track(reread());
8795
+ }
8796
+ function loggedIn() {
8797
+ forgetScans();
8798
+ return track(ask.learnOrganization(server).catch(noop3).then(reread));
8799
+ }
8800
+ function stop() {
8801
+ dead = true;
8802
+ unrest();
8803
+ listeners.clear();
8804
+ }
8805
+ async function settled() {
8806
+ while (pending.size > 0)
8807
+ await Promise.all([...pending]);
8808
+ }
8809
+ askUpgrade();
8810
+ askFeed();
8811
+ return {
8812
+ read: () => state,
8813
+ subscribe: (listener) => {
8814
+ listeners.add(listener);
8815
+ return () => {
8816
+ listeners.delete(listener);
8817
+ };
8818
+ },
8819
+ back,
8820
+ loggedIn,
8821
+ rest,
8822
+ client,
8823
+ forget: () => {
8824
+ held = null;
8825
+ },
8826
+ resite: from.resite,
8827
+ stop,
8828
+ settled
8829
+ };
8830
+ }
8229
8831
  // src/shell/screens/home.tsx
8230
8832
  import { Box as Box14, Text as Text16, useApp as useApp7, useInput as useInput8 } from "ink";
8231
- import { useCallback as useCallback3, useEffect as useEffect9, useMemo as useMemo4, useRef, useState as useState11 } from "react";
8833
+ import {
8834
+ useCallback as useCallback3,
8835
+ useEffect as useEffect9,
8836
+ useMemo as useMemo4,
8837
+ useState as useState11,
8838
+ useSyncExternalStore
8839
+ } from "react";
8232
8840
 
8233
8841
  // src/draw.tsx
8234
8842
  import { jsx as jsx15 } from "react/jsx-runtime";
@@ -8289,50 +8897,6 @@ function drawInk(answer, {
8289
8897
  }
8290
8898
  }
8291
8899
 
8292
- // src/shell/feed.ts
8293
- async function feedFacts(client, where, server) {
8294
- const installed = Object.keys(where.manifest.skills).length > 0;
8295
- const [skills, bundles, passes, drift, orgs] = await Promise.allSettled([
8296
- client.skills.list.query({}),
8297
- client.bundles.list.query({}),
8298
- client.mirror.passes.query(),
8299
- installed ? driftNow(client, where) : Promise.resolve(null),
8300
- organizationChoices(server)
8301
- ]);
8302
- return {
8303
- ...skills.status === "fulfilled" ? { skills: skills.value.length } : {},
8304
- ...bundles.status === "fulfilled" ? { bundles: bundles.value.length } : {},
8305
- ...passes.status === "fulfilled" ? { pending: passes.value.length } : {},
8306
- ...drift.status === "fulfilled" && drift.value ? { outdated: drift.value.moved.length } : {},
8307
- ...orgs.status === "fulfilled" ? { organizations: orgs.value.length } : {}
8308
- };
8309
- }
8310
-
8311
- // src/shell/probe.ts
8312
- var importProbe = async (where) => {
8313
- const found = await listInstalled(where.scope);
8314
- const recorded = new Set(Object.keys(where.manifest.skills));
8315
- const news = found.filter((skill) => !recorded.has(skill.name)).length;
8316
- if (found.length === 0)
8317
- return "nothing on this machine to adopt";
8318
- return news === 0 ? `${count2(found.length, "skill")} on this machine, all recorded here already` : `${count2(found.length, "skill")} on this machine, ${news} not recorded here`;
8319
- };
8320
- var doctorProbe = async (where, server) => {
8321
- const checks = await diagnose(where, {
8322
- local: true,
8323
- ...server ? { server } : {}
8324
- });
8325
- const bad = checks.filter((check) => !check.ok);
8326
- return bad.length === 0 ? `${count2(checks.length, "local check")}, all passing` : `${bad.length} of ${checks.length} local checks failing: ${bad.map((check) => check.name).join(", ")}`;
8327
- };
8328
- var PROBES = {
8329
- "setup.import": importProbe,
8330
- "setup.doctor": doctorProbe
8331
- };
8332
- function probeFor(value) {
8333
- return value ? PROBES[value] : undefined;
8334
- }
8335
-
8336
8900
  // src/args.ts
8337
8901
  function configCommand2(rest, explain) {
8338
8902
  const [action, key, ...value] = rest;
@@ -8962,61 +9526,30 @@ function StaleLine({ stale, width }) {
8962
9526
  });
8963
9527
  }
8964
9528
  var RUN = "run";
8965
- var REST_MS = 250;
8966
- var ASK_NPM = () => upgradeCheck();
8967
- var ASK_REGISTRY = feedFacts;
8968
9529
  var NO_EXIT_CODE = () => {
8969
9530
  return;
8970
9531
  };
8971
9532
  function HomeScreen({
8972
- where: initialWhere,
8973
- resite,
8974
- facts: initial,
8975
- reload,
9533
+ known,
8976
9534
  server,
8977
- openClient,
8978
9535
  onError,
8979
9536
  onSummary,
8980
9537
  onWelcomed = async () => {
8981
9538
  return;
8982
- },
8983
- checkUpgrade = ASK_NPM,
8984
- loadFeed = ASK_REGISTRY,
8985
- probes: probeSet = probeFor
9539
+ }
8986
9540
  }) {
8987
9541
  const { exit } = useApp7();
8988
9542
  const width = useFrameWidth();
8989
- const [facts, setFacts] = useState11(initial);
8990
- const [where, setWhere] = useState11(initialWhere);
9543
+ useEffect9(() => () => known.stop(), [known]);
9544
+ const machine = useSyncExternalStore(known.subscribe, known.read, known.read);
9545
+ const { where, facts, upgrade: stale, probes, probing } = machine;
9546
+ const { resite } = known;
8991
9547
  const [query, setQuery] = useState11("");
8992
9548
  const [cursor, setCursor] = useState11(0);
8993
9549
  const [route, setRoute] = useState11(null);
8994
- const [welcome, setWelcome] = useState11(initial.firstRun);
9550
+ const [welcome, setWelcome] = useState11(() => known.read().facts.firstRun);
8995
9551
  const [wizard, setWizard] = useState11(false);
8996
- const [stale, setStale] = useState11(undefined);
8997
- useEffect9(() => {
8998
- let live = true;
8999
- checkUpgrade().then((found) => {
9000
- if (live)
9001
- setStale(found);
9002
- }).catch(() => {
9003
- return;
9004
- });
9005
- return () => {
9006
- live = false;
9007
- };
9008
- }, [checkUpgrade]);
9009
- const [feed, setFeed] = useState11(NO_FEED);
9010
- const [probes, setProbes] = useState11({});
9011
- const [probing, setProbing] = useState11(null);
9012
- const shown = useMemo4(() => ({
9013
- ...facts,
9014
- feed: {
9015
- ...feed,
9016
- ...stale === undefined ? {} : { upgrade: stale ? stale.latest : null }
9017
- }
9018
- }), [facts, feed, stale]);
9019
- const groups = useMemo4(() => paletteGroups(shown), [shown]);
9552
+ const groups = useMemo4(() => paletteGroups(facts), [facts]);
9020
9553
  const typed = useMemo4(() => typedCommand(query), [query]);
9021
9554
  const visible = useMemo4(() => {
9022
9555
  const filtered = filterCommands(groups, query);
@@ -9034,25 +9567,7 @@ function HomeScreen({
9034
9567
  useEffect9(() => {
9035
9568
  setCursor((c) => Math.max(0, Math.min(c, rows.length - 1)));
9036
9569
  }, [rows.length]);
9037
- const held = useRef(null);
9038
- const connect = useCallback3(async () => {
9039
- held.current ??= await openClient();
9040
- return held.current;
9041
- }, [openClient]);
9042
- useEffect9(() => {
9043
- if (!facts.signedIn)
9044
- return;
9045
- let live = true;
9046
- connect().then((client) => loadFeed(client, where, server)).then((found) => {
9047
- if (live)
9048
- setFeed(found);
9049
- }).catch(() => {
9050
- return;
9051
- });
9052
- return () => {
9053
- live = false;
9054
- };
9055
- }, [facts.signedIn, connect, loadFeed, where, server]);
9570
+ const connect = known.client;
9056
9571
  const checkSession = useCallback3(async () => facts.signedIn, [facts.signedIn]);
9057
9572
  const failing = useCallback3((title, error) => ({
9058
9573
  kind: "answer",
@@ -9105,91 +9620,21 @@ function HomeScreen({
9105
9620
  const asked = await row.confirm?.(command, ctx, facts);
9106
9621
  const answer = asked ?? await row.run(command, ctx);
9107
9622
  if (command.kind === "logout")
9108
- held.current = null;
9623
+ known.forget();
9109
9624
  return routeFrom(answer, say(command, facts));
9110
- }, [catalogDoor, ctx, facts, routeFrom]);
9111
- const ownRoute = useCallback3(async (value) => {
9112
- switch (value) {
9113
- case "catalog.search":
9114
- return { kind: "browse", tab: "skills" };
9115
- case "installed.remove":
9116
- return {
9117
- kind: "pick",
9118
- title: "remove",
9119
- rows: installedRows(where).map((row) => ({
9120
- name: row.name,
9121
- description: `${row.bundle ?? "loose"} ${shortRef(row.ref)}`
9122
- })),
9123
- empty: "Nothing installed here.",
9124
- verb: "remove",
9125
- command: (names) => ({ kind: "remove", names })
9126
- };
9127
- case "setup.scope":
9128
- return {
9129
- kind: "pick",
9130
- title: "scope",
9131
- rows: [
9132
- {
9133
- name: "global",
9134
- description: "installs land in your home directory, where every project sees them"
9135
- },
9136
- {
9137
- name: "project",
9138
- description: "installs land in this project's yourskills.json, and travel with the repo"
9139
- }
9140
- ],
9141
- empty: "",
9142
- verb: "set",
9143
- single: true,
9144
- marked: facts.scope,
9145
- command: (names) => ({
9146
- kind: "config",
9147
- action: "set",
9148
- key: "scope",
9149
- value: names[0] ?? ""
9150
- })
9151
- };
9152
- case "setup.agents": {
9153
- const valid = await knownAgents() ?? [];
9154
- const onDisk = new Set(facts.agentsOnDisk);
9155
- return {
9156
- kind: "pick",
9157
- title: "agents",
9158
- rows: valid.map((name) => ({
9159
- name,
9160
- description: onDisk.has(name) ? "on this machine" : ""
9161
- })),
9162
- empty: "Could not ask the `skills` CLI which agent names it accepts. Run `yourskills doctor`.",
9163
- verb: "set",
9164
- preset: facts.agents,
9165
- command: (names) => ({
9166
- kind: "config",
9167
- action: "set",
9168
- key: "agents",
9169
- value: names.join(",")
9170
- })
9171
- };
9172
- }
9173
- case "account.org": {
9174
- const orgs = await organizationChoices(server);
9175
- const active = orgs.find((org) => org.active)?.name;
9176
- return {
9177
- kind: "pick",
9178
- title: "org",
9179
- rows: orgs.map((org) => ({
9180
- name: org.name,
9181
- description: org.slug ?? ""
9182
- })),
9183
- empty: "You are not a member of any organization yet.",
9184
- verb: "switch",
9185
- single: true,
9186
- ...active ? { marked: active } : {},
9187
- command: (names) => ({ kind: "org", name: names[0] ?? "" })
9188
- };
9189
- }
9190
- }
9191
- }, [where, server, facts]);
9192
- const routeForDoor = useCallback3(async (door) => door.opens === undefined ? ownRoute(door.value) : routeForCommand(door.opens), [ownRoute, routeForCommand]);
9625
+ }, [catalogDoor, ctx, facts, known, routeFrom]);
9626
+ const routeForDoor = useCallback3(async (door) => {
9627
+ if (door.opens !== undefined)
9628
+ return routeForCommand(door.opens);
9629
+ const { asks } = door;
9630
+ const choices = await asks.choices(ctx, facts);
9631
+ return {
9632
+ kind: "pick",
9633
+ ask: asks,
9634
+ rows: [...choices.rows],
9635
+ chosen: [...choices.chosen]
9636
+ };
9637
+ }, [ctx, facts, routeForCommand]);
9193
9638
  const open = useCallback3((pending, title) => {
9194
9639
  pending.then((next) => {
9195
9640
  if (next)
@@ -9205,29 +9650,10 @@ function HomeScreen({
9205
9650
  open(routeForCommand(parsed.command), parsed.text);
9206
9651
  }, [open, routeForCommand]);
9207
9652
  const current = rows[cursor];
9653
+ const resting = current?.value;
9208
9654
  useEffect9(() => {
9209
- const value = current?.value;
9210
- const probe = probeSet(value);
9211
- if (!value || !probe || probes[value] !== undefined)
9212
- return;
9213
- let live = true;
9214
- const timer = setTimeout(() => {
9215
- setProbing(value);
9216
- probe(where, server).then((text) => {
9217
- if (live)
9218
- setProbes((was) => ({ ...was, [value]: text }));
9219
- }).catch(() => {
9220
- return;
9221
- }).finally(() => {
9222
- if (live)
9223
- setProbing(null);
9224
- });
9225
- }, REST_MS);
9226
- return () => {
9227
- live = false;
9228
- clearTimeout(timer);
9229
- };
9230
- }, [current?.value, probes, probeSet, where, server]);
9655
+ known.rest(resting);
9656
+ }, [known, resting]);
9231
9657
  const activate = useCallback3(() => {
9232
9658
  const item = rows[cursor];
9233
9659
  if (!item)
@@ -9250,32 +9676,24 @@ function HomeScreen({
9250
9676
  setQuery(item.hint.includes("<") ? `${bare} ` : bare);
9251
9677
  setCursor(0);
9252
9678
  }, [rows, cursor]);
9253
- const back = useCallback3(() => {
9679
+ const land = useCallback3(() => {
9254
9680
  setRoute(null);
9255
9681
  setQuery("");
9256
9682
  setCursor(0);
9257
- setProbes({});
9258
- reload().then(setFacts).catch(() => {
9259
- return;
9260
- });
9261
- resite().then(setWhere).catch(() => {
9262
- return;
9263
- });
9264
- }, [reload, resite]);
9683
+ }, []);
9684
+ const back = useCallback3(() => {
9685
+ land();
9686
+ known.back();
9687
+ }, [land, known]);
9265
9688
  const afterLogin = useCallback3(() => {
9266
- learnActiveOrganization(server).then(() => reload()).then(setFacts).catch(() => {
9267
- return;
9268
- });
9269
9689
  if (!wizard) {
9270
- back();
9690
+ land();
9691
+ known.loggedIn();
9271
9692
  return;
9272
9693
  }
9273
9694
  setWizard(false);
9274
- reload().then((next) => {
9275
- setFacts(next);
9276
- setRoute({ kind: "browse" });
9277
- }).catch(() => setRoute(null));
9278
- }, [wizard, back, reload, server]);
9695
+ known.loggedIn().then(() => setRoute({ kind: "browse" }));
9696
+ }, [wizard, land, known]);
9279
9697
  const leaveWelcome = useCallback3((start) => {
9280
9698
  setWelcome(false);
9281
9699
  onWelcomed().catch(() => {
@@ -9351,17 +9769,15 @@ function HomeScreen({
9351
9769
  if (drawn)
9352
9770
  return drawn;
9353
9771
  if (route?.kind === "pick") {
9354
- const picked = route;
9772
+ const { ask, rows: choices, chosen } = route;
9355
9773
  return /* @__PURE__ */ jsx19(PickScreen, {
9356
- title: picked.title,
9357
- rows: picked.rows,
9358
- empty: picked.empty,
9359
- verb: picked.verb,
9360
- ...picked.single ? { single: true } : {},
9361
- ...picked.marked ? { marked: picked.marked } : {},
9362
- ...picked.preset ? { preset: picked.preset } : {},
9774
+ title: ask.title,
9775
+ rows: choices,
9776
+ empty: ask.empty,
9777
+ verb: ask.verb,
9778
+ ...ask.pick === "one" ? { single: true, ...chosen[0] ? { marked: chosen[0] } : {} } : { preset: chosen },
9363
9779
  onPick: (names) => {
9364
- open(routeForCommand(picked.command(names)), picked.title);
9780
+ open(routeForCommand(ask.opens(names)), ask.title);
9365
9781
  },
9366
9782
  onBack: back
9367
9783
  });
@@ -9396,7 +9812,7 @@ function HomeScreen({
9396
9812
  });
9397
9813
  }
9398
9814
  const highlighted = current ? doorFor(current.value) : undefined;
9399
- const description = current?.value === RUN && runnable(typed) ? say(typed.command, shown) : highlighted ? doorSays(highlighted, shown) : typed ? say(typed.command, shown) : "";
9815
+ const description = current?.value === RUN && runnable(typed) ? say(typed.command, facts) : highlighted ? doorSays(highlighted, facts) : typed ? say(typed.command, facts) : "";
9400
9816
  const found = current ? probes[current.value] ?? (probing === current.value ? "looking..." : "") : "";
9401
9817
  const empty = typed?.command.kind === "error" && typed.command.next ? `${mark.fail} ${typed.command.message} try: ${typed.command.next.replace(/^yourskills /, "")}` : "No matching commands.";
9402
9818
  return /* @__PURE__ */ jsxs16(Frame, {
@@ -9404,7 +9820,7 @@ function HomeScreen({
9404
9820
  width,
9405
9821
  children: [
9406
9822
  /* @__PURE__ */ jsx19(AccountLine, {
9407
- facts: shown,
9823
+ facts,
9408
9824
  width
9409
9825
  }),
9410
9826
  stale ? /* @__PURE__ */ jsx19(StaleLine, {
@@ -9538,13 +9954,17 @@ async function screenFor(command, ctx, resite, fail, exit) {
9538
9954
  const { row, answer } = await answerFor(command, ctx);
9539
9955
  if (answer.kind === "home") {
9540
9956
  const reload = () => paletteFacts(ctx.where.scope, ctx.server);
9541
- return /* @__PURE__ */ jsx20(HomeScreen, {
9957
+ const known = knowMachine({
9542
9958
  where: ctx.where,
9543
- resite,
9544
9959
  facts: await reload(),
9960
+ ...ctx.server ? { server: ctx.server } : {},
9545
9961
  reload,
9962
+ resite,
9963
+ client: ctx.client
9964
+ });
9965
+ return /* @__PURE__ */ jsx20(HomeScreen, {
9966
+ known,
9546
9967
  ...ctx.server ? { server: ctx.server } : {},
9547
- openClient: ctx.client,
9548
9968
  onError: fail,
9549
9969
  onSummary: summarise,
9550
9970
  onWelcomed: async () => {