yourskills 0.4.3 → 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 +2164 -1718
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -61,136 +61,647 @@ 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",
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
+ });
286
+ }
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))
305
+ });
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
+ ]
330
+ });
331
+ }
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
+ });
357
+ }
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 };
372
+ }
373
+ function position(cursor, total) {
374
+ return `${total > 0 ? cursor + 1 : 0}/${total}`;
375
+ }
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);
387
+ }
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 });
402
+ }
403
+ return out;
404
+ }
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
+ ]
444
+ });
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
+ ]
506
+ });
507
+ }
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
+ });
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
+ });
615
+ }
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
+ });
632
+ }
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`";
655
+ }
656
+ }
657
+ function trackingDisabledByEnv() {
658
+ const value = process.env.YOURSKILLS_NO_TRACK;
659
+ return value !== undefined && value !== "" && value !== "0";
660
+ }
661
+ function trackingOn(config) {
662
+ return !trackingDisabledByEnv() && config.track !== false;
663
+ }
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
+ };
676
+ }
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) {
688
+ return {
689
+ state: "off-for-organization",
690
+ sending: true,
691
+ note: "off; your organization turned usage tracking off for everyone",
184
692
  value: "off",
185
- from: "usage tracking in your organization's settings"
693
+ layer: "organization",
694
+ from: trackingFrom("organization", configPath)
186
695
  };
187
696
  }
697
+ const layer = track === true ? "config" : "default";
188
698
  return {
189
699
  state: "on",
190
700
  sending: true,
191
701
  note: ON_NOTE,
192
702
  value: "on",
193
- from: track === true ? `track in ${configPath}` : "built-in default: turn off with `config set track false`"
703
+ layer,
704
+ from: trackingFrom(layer, configPath)
194
705
  };
195
706
  }
196
707
  async function trackingNow(question) {
@@ -249,20 +760,6 @@ function near(a, b) {
249
760
  return true;
250
761
  }
251
762
  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
- }
265
- }
266
763
  function setConfigValue(config, key, value) {
267
764
  if (!isConfigKey(key)) {
268
765
  return {
@@ -314,875 +811,686 @@ function setConfigValue(config, key, value) {
314
811
  const serverUrl = url.toString().replace(/\/$/, "");
315
812
  return { ok: true, config: { ...config, serverUrl }, shown: serverUrl };
316
813
  }
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
- }
335
- }
336
- }
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}`
346
- });
347
- lines.push({
348
- mark: "none",
349
- text: `${"token".padEnd(width)} ${config.token ? REDACTED : UNSET}`
350
- });
351
- return lines;
352
- }
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 }
385
- };
386
- }
387
-
388
- // src/settings/command.ts
389
- async function configGet(key) {
390
- return configGetReport(await readConfig(), key, configPath());
391
- }
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;
409
- }
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
- }
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
- }
459
- }
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
- }
467
- }
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 };
476
- }
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 {
498
- return {
499
- error: "invalid_response",
500
- error_description: text.slice(0, 200) || res.statusText
501
- };
502
- }
503
- if (res.ok)
504
- return { ok: true, data: parsed };
505
- const err = parsed;
506
- return {
507
- error: err.error ?? `http_${res.status}`,
508
- error_description: err.error_description
509
- };
510
- }
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
516
- });
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;
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) };
547
820
  }
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}`);
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
+ };
562
831
  }
563
832
  }
564
- throw new Error("The device code expired. Run `yourskills login` again.");
565
833
  }
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();
834
+
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");
577
843
  }
578
- async function fetchSession(base, token) {
579
- const res = await fetch(`${authBase(base)}/get-session`, {
580
- headers: { authorization: `Bearer ${token}` }
581
- });
582
- if (!res.ok)
583
- return null;
584
- return await res.json();
844
+ function configPath() {
845
+ return join(configDir(), "config.json");
585
846
  }
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
- };
847
+ async function readConfig() {
848
+ try {
849
+ return JSON.parse(await readFile(configPath(), "utf8"));
850
+ } catch {
851
+ return {};
852
+ }
599
853
  }
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) };
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;
861
+ }
862
+ const next = change(current);
863
+ await writeConfig(next);
864
+ return next;
605
865
  }
606
- async function hasSession() {
607
- return Boolean((await readConfig()).token);
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);
608
871
  }
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
- };
872
+ function clearConfig() {
873
+ rmSync(configPath(), { force: true });
617
874
  }
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
875
 
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 "";
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
+ };
643
888
  }
644
- const unit = seq.length > 0 ? seq : " ";
645
- return unit.repeat(Math.ceil(count / unit.length)).slice(0, count);
889
+ return {
890
+ value: last.value,
891
+ layer: last.layer,
892
+ from: last.from,
893
+ layers,
894
+ decided: false
895
+ };
646
896
  }
647
- function junctionGlyph(chars) {
648
- return chars.junction.length > 0 ? chars.junction[0] : DEFAULT_ASCII_CHARS.junction;
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" });
649
920
  }
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}`;
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"
943
+ });
659
944
  }
660
-
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));
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" });
693
964
  }
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
- });
965
+ function serverUrl(config = {}, override) {
966
+ return serverAnswer({
967
+ config,
968
+ configPath: configPath(),
969
+ flags: { server: override }
970
+ }).value;
730
971
  }
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
- })
972
+ function trackingOf(held) {
973
+ return trackingAnswer({
974
+ track: held.config.track,
975
+ configPath: held.configPath,
976
+ organization: held.organization
739
977
  });
740
978
  }
741
- function Blank() {
742
- return /* @__PURE__ */ jsx(Text, {
743
- children: " "
744
- });
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
+ };
1011
+ }
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" });
1020
+ }
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
+ };
745
1035
  }
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)}~`;
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
+ }
1049
+ }
1050
+ function settingsShown(held) {
1051
+ return CONFIG_KEYS.map((key) => shown(key, held));
752
1052
  }
753
1053
 
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.`);
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 [];
776
1069
  }
777
- return lifecycle;
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") : [];
778
1078
  }
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}]`;
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;
786
1087
  }
787
- function lifecycleColor(status) {
788
- return stateTone[lifecycleOf(status ?? "active").tone];
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
+ }
789
1104
  }
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)));
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 {
1110
+ return null;
1111
+ }
794
1112
  }
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
- });
1113
+ async function nearestProject(cwd) {
1114
+ const path = findProjectManifest(cwd);
1115
+ return path ? { path, scope: await scopeInManifest(path) } : null;
839
1116
  }
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))
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
858
1127
  });
1128
+ return {
1129
+ scope: answer.value,
1130
+ source: answer.layer,
1131
+ manifestPath: project?.path ?? null
1132
+ };
859
1133
  }
860
1134
 
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)
873
- 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
- ]
883
- });
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
+ };
884
1150
  }
885
- // src/ui/command.tsx
886
- import { Box as Box3, Text as Text4 } from "ink";
887
1151
 
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
- ]
1152
+ // src/settings/command.ts
1153
+ async function configGet(key) {
1154
+ return configGetReport(await holdSettings(), key);
1155
+ }
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}`
909
1169
  });
1170
+ return lines;
1171
+ }
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
+ };
910
1206
  }
911
-
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 };
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
+ };
925
1231
  }
926
- function position(cursor, total) {
927
- return `${total > 0 ? cursor + 1 : 0}/${total}`;
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
+ };
928
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
+ };
929
1273
 
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);
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;
1283
+ }
940
1284
  }
941
- function flattenCommands(groups) {
942
- return groups.flatMap((group) => group.items);
1285
+ function isSessionRefusal(error) {
1286
+ return error instanceof Error && error.name === "SessionRefusal";
943
1287
  }
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 });
955
- }
956
- return out;
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) };
957
1293
  }
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);
1294
+ async function hasSession() {
1295
+ return Boolean((await readConfig()).token);
963
1296
  }
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
- });
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
+ };
998
1305
  }
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)
1017
- });
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
- ]
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)
1059
1311
  });
1060
- }
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();
1088
- }
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)
1089
1333
  });
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
- ]
1334
+ if (!res.ok)
1335
+ return null;
1336
+ return await res.json();
1337
+ }
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)
1111
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
1112
1378
  };
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
- });
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;
1398
+ }
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);
1168
1416
  }
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]
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;
1422
+ });
1423
+ }
1424
+ clearConfig();
1425
+ }
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":
@@ -2371,64 +2679,13 @@ function Installer({
2371
2679
  ]
2372
2680
  }) : null
2373
2681
  ]
2374
- }),
2375
- done || error !== null ? /* @__PURE__ */ jsx10(Back, {
2376
- width,
2377
- onBack
2378
- }) : null
2379
- ]
2380
- });
2381
- }
2382
- function plainLine(line) {
2383
- switch (line.state) {
2384
- case "installing":
2385
- return `installing ${line.name} ${line.where}`;
2386
- case "installed":
2387
- return `${line.name} ${line.where}${line.deprecated ? ` ${lifecycleTag("deprecated")}` : ""}`;
2388
- case "pending":
2389
- return pendingMessage(line.vendor);
2390
- case "question":
2391
- return licenceQuestionMessage(line.name, line.licence);
2392
- case "failed":
2393
- return `error: ${line.name}: ${line.message}`;
2394
- default:
2395
- return null;
2396
- }
2397
- }
2398
- async function installPlain(stream) {
2399
- let lines = [];
2400
- const printed = new Set;
2401
- const started = new Set;
2402
- for await (const event of stream) {
2403
- lines = reduce(lines, event);
2404
- for (const line of lines) {
2405
- if (line.state === "installing" && !started.has(line.name)) {
2406
- started.add(line.name);
2407
- const text = plainLine(line);
2408
- if (text !== null)
2409
- console.log(text);
2410
- continue;
2411
- }
2412
- if (printed.has(line.name) || !settled(line))
2413
- continue;
2414
- const text = plainLine(line);
2415
- if (text === null)
2416
- continue;
2417
- printed.add(line.name);
2418
- if (line.state === "failed") {
2419
- console.error(text);
2420
- if (line.detail)
2421
- console.error(line.detail);
2422
- } else {
2423
- console.log(text);
2424
- }
2425
- }
2426
- }
2427
- const counts = receipt(lines);
2428
- if (counts) {
2429
- console.log(`${counts.installed} installed, ${counts.pending} pending, ${counts.failed} failed` + (counts.questions > 0 ? `, ${counts.questions} unanswered` : ""));
2430
- }
2431
- return summarise(lines);
2682
+ }),
2683
+ done || error !== null ? /* @__PURE__ */ jsx10(Back, {
2684
+ width,
2685
+ onBack
2686
+ }) : null
2687
+ ]
2688
+ });
2432
2689
  }
2433
2690
  // src/screens.tsx
2434
2691
  import { Box as Box8, Text as Text10, useApp as useApp4, useInput as useInput4 } from "ink";
@@ -2437,29 +2694,41 @@ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
2437
2694
  var HELP_COLUMN = 26;
2438
2695
  function NoticeScreen({
2439
2696
  title,
2440
- message
2697
+ message,
2698
+ exitWhenDone = true,
2699
+ onBack
2441
2700
  }) {
2442
2701
  const { exit } = useApp4();
2443
2702
  const width = useFrameWidth();
2444
- useEffect5(exit, [exit]);
2445
- return /* @__PURE__ */ jsx11(Frame, {
2703
+ useEffect5(() => {
2704
+ if (exitWhenDone)
2705
+ exit();
2706
+ }, [exit, exitWhenDone]);
2707
+ return /* @__PURE__ */ jsxs10(Frame, {
2446
2708
  title,
2447
2709
  width,
2448
- children: /* @__PURE__ */ jsxs10(Text10, {
2449
- children: [
2450
- /* @__PURE__ */ jsx11(Text10, {
2451
- color: "green",
2452
- children: mark.ok
2453
- }),
2454
- /* @__PURE__ */ jsx11(Text10, {
2455
- children: ` ${message}`
2456
- })
2457
- ]
2458
- })
2710
+ children: [
2711
+ /* @__PURE__ */ jsxs10(Text10, {
2712
+ children: [
2713
+ /* @__PURE__ */ jsx11(Text10, {
2714
+ color: "green",
2715
+ children: mark.ok
2716
+ }),
2717
+ /* @__PURE__ */ jsx11(Text10, {
2718
+ children: ` ${message}`
2719
+ })
2720
+ ]
2721
+ }),
2722
+ /* @__PURE__ */ jsx11(Back, {
2723
+ width,
2724
+ onBack
2725
+ })
2726
+ ]
2459
2727
  });
2460
2728
  }
2461
2729
  var COLOURS = {
2462
2730
  ok: "green",
2731
+ warn: tone.notice,
2463
2732
  fail: tone.danger,
2464
2733
  pending: tone.notice,
2465
2734
  none: undefined
@@ -2571,39 +2840,105 @@ function numberedLines(text) {
2571
2840
  return text.split(`
2572
2841
  `).map((line, i) => ({ id: `${i}:${line}`, line }));
2573
2842
  }
2574
- function HelpScreen({ text }) {
2843
+ function HelpScreen({
2844
+ text,
2845
+ exitWhenDone = true,
2846
+ onBack
2847
+ }) {
2575
2848
  const { exit } = useApp4();
2576
2849
  const width = useFrameWidth();
2577
- useEffect5(exit, [exit]);
2578
- return /* @__PURE__ */ jsx11(Frame, {
2850
+ useEffect5(() => {
2851
+ if (exitWhenDone)
2852
+ exit();
2853
+ }, [exit, exitWhenDone]);
2854
+ return /* @__PURE__ */ jsxs10(Frame, {
2579
2855
  title: "yourskills",
2580
2856
  width,
2581
- children: /* @__PURE__ */ jsx11(Box8, {
2582
- flexDirection: "column",
2583
- children: numberedLines(text).map(({ id, line }) => {
2584
- if (line.length === 0)
2585
- return /* @__PURE__ */ jsx11(Blank, {}, id);
2586
- if (!line.startsWith(" "))
2587
- return /* @__PURE__ */ jsx11(Text10, {
2588
- children: clip(line, width - FRAME_CHROME)
2589
- }, id);
2590
- return /* @__PURE__ */ jsxs10(Text10, {
2591
- children: [
2592
- /* @__PURE__ */ jsx11(Text10, {
2593
- color: tone.primary,
2594
- children: line.slice(0, HELP_COLUMN)
2595
- }),
2596
- /* @__PURE__ */ jsx11(Text10, {
2597
- dimColor: true,
2598
- children: clip(line.slice(HELP_COLUMN), width - FRAME_CHROME - HELP_COLUMN)
2599
- })
2600
- ]
2601
- }, line);
2857
+ children: [
2858
+ /* @__PURE__ */ jsx11(Box8, {
2859
+ flexDirection: "column",
2860
+ children: numberedLines(text).map(({ id, line }) => {
2861
+ if (line.length === 0)
2862
+ return /* @__PURE__ */ jsx11(Blank, {}, id);
2863
+ if (!line.startsWith(" "))
2864
+ return /* @__PURE__ */ jsx11(Text10, {
2865
+ children: clip(line, width - FRAME_CHROME)
2866
+ }, id);
2867
+ return /* @__PURE__ */ jsxs10(Text10, {
2868
+ children: [
2869
+ /* @__PURE__ */ jsx11(Text10, {
2870
+ color: tone.primary,
2871
+ children: line.slice(0, HELP_COLUMN)
2872
+ }),
2873
+ /* @__PURE__ */ jsx11(Text10, {
2874
+ dimColor: true,
2875
+ children: clip(line.slice(HELP_COLUMN), width - FRAME_CHROME - HELP_COLUMN)
2876
+ })
2877
+ ]
2878
+ }, line);
2879
+ })
2880
+ }),
2881
+ /* @__PURE__ */ jsx11(Back, {
2882
+ width,
2883
+ onBack
2602
2884
  })
2603
- })
2885
+ ]
2604
2886
  });
2605
2887
  }
2606
2888
 
2889
+ // src/install/plain.ts
2890
+ function plainLine(line) {
2891
+ switch (line.state) {
2892
+ case "installing":
2893
+ return `installing ${line.name} ${line.where}`;
2894
+ case "installed":
2895
+ return `${line.name} ${line.where}${line.deprecated ? ` ${lifecycleTag("deprecated")}` : ""}`;
2896
+ case "pending":
2897
+ return pendingMessage(line.vendor);
2898
+ case "question":
2899
+ return licenceQuestionMessage(line.name, line.licence);
2900
+ case "failed":
2901
+ return `error: ${line.name}: ${line.message}`;
2902
+ default:
2903
+ return null;
2904
+ }
2905
+ }
2906
+ async function installPlain(stream, io) {
2907
+ let lines = [];
2908
+ const printed = new Set;
2909
+ const started = new Set;
2910
+ for await (const event of stream) {
2911
+ lines = reduce(lines, event);
2912
+ for (const line of lines) {
2913
+ if (line.state === "installing" && !started.has(line.name)) {
2914
+ started.add(line.name);
2915
+ const text = plainLine(line);
2916
+ if (text !== null)
2917
+ io.out(text);
2918
+ continue;
2919
+ }
2920
+ if (printed.has(line.name) || !settled(line))
2921
+ continue;
2922
+ const text = plainLine(line);
2923
+ if (text === null)
2924
+ continue;
2925
+ printed.add(line.name);
2926
+ if (line.state === "failed") {
2927
+ io.err(text);
2928
+ if (line.detail)
2929
+ io.err(line.detail);
2930
+ } else {
2931
+ io.out(text);
2932
+ }
2933
+ }
2934
+ }
2935
+ const counts = receipt(lines);
2936
+ if (counts) {
2937
+ io.out(`${counts.installed} installed, ${counts.pending} pending, ${counts.failed} failed` + (counts.questions > 0 ? `, ${counts.questions} unanswered` : ""));
2938
+ }
2939
+ return summarise(lines);
2940
+ }
2941
+
2607
2942
  // src/install/screens/run.tsx
2608
2943
  import { jsx as jsx12 } from "react/jsx-runtime";
2609
2944
  function RunScreen({
@@ -2636,13 +2971,13 @@ function RunScreen({
2636
2971
  ...onNext ? { onNext } : {}
2637
2972
  });
2638
2973
  }
2639
- async function plainRun(run, emit) {
2974
+ async function plainRun(run, emit, io) {
2640
2975
  if (run.kind === "report") {
2641
2976
  const done = await run.report();
2642
2977
  emit(done);
2643
2978
  return done;
2644
2979
  }
2645
- return installPlain(run.stream());
2980
+ return installPlain(run.stream(), io);
2646
2981
  }
2647
2982
  // src/install/empty.ts
2648
2983
  function nothingTracked(nearest, kind, all = false) {
@@ -3272,7 +3607,7 @@ function shellFromEnv(shell) {
3272
3607
  // src/doctor/doctor.ts
3273
3608
  import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
3274
3609
  import { homedir as homedir4 } from "node:os";
3275
- import { dirname as dirname5, join as join8 } from "node:path";
3610
+ import { dirname as dirname6, join as join8 } from "node:path";
3276
3611
 
3277
3612
  // ../../node_modules/.bun/@trpc+client@11.18.0+acc6907a906fa511/node_modules/@trpc/client/dist/objectSpread2-BvkFp-_Y.mjs
3278
3613
  var __create = Object.create;
@@ -4702,7 +5037,7 @@ var EXIT_CODES = [
4702
5037
  [EXIT.usage, "the command line was wrong; the last line says the spelling"],
4703
5038
  [EXIT.session, "needs a session: run `yourskills login`"]
4704
5039
  ];
4705
- var LOGIN = "yourskills login";
5040
+ var LOGIN2 = "yourskills login";
4706
5041
  function needsTerminal(kind) {
4707
5042
  if (kind === "login")
4708
5043
  return new CliError("login needs a terminal", {
@@ -4726,7 +5061,7 @@ function describeError(error) {
4726
5061
  const message = error instanceof Error ? error.message : String(error);
4727
5062
  if (isUnauthorized(error)) {
4728
5063
  return {
4729
- 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),
4730
5065
  code: EXIT.session
4731
5066
  };
4732
5067
  }
@@ -4845,7 +5180,7 @@ import { existsSync as existsSync2 } from "node:fs";
4845
5180
  // src/hooks/settings.ts
4846
5181
  import { mkdirSync as mkdirSync3 } from "node:fs";
4847
5182
  import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
4848
- import { dirname as dirname4 } from "node:path";
5183
+ import { dirname as dirname5 } from "node:path";
4849
5184
  function binaryName(command) {
4850
5185
  const parts = command.split(/[\\/]/);
4851
5186
  return (parts[parts.length - 1] ?? "").replace(/\.(js|mjs|cjs|exe)$/i, "");
@@ -4967,7 +5302,7 @@ function serialiseSettings(settings, indent) {
4967
5302
  `;
4968
5303
  }
4969
5304
  async function writeSettingsFile(path, settings, indent) {
4970
- mkdirSync3(dirname4(path), { recursive: true });
5305
+ mkdirSync3(dirname5(path), { recursive: true });
4971
5306
  await writeFile2(path, serialiseSettings(settings, indent));
4972
5307
  }
4973
5308
  async function applyTo(path, change) {
@@ -5752,7 +6087,7 @@ async function hooksStatus() {
5752
6087
  { mark: "none", text: "tracking", note: tracking.note }
5753
6088
  ];
5754
6089
  const agents = [];
5755
- for (const agent of agentsToReport(config.agents ?? [])) {
6090
+ for (const agent of agentsToReport(await resolveAgents())) {
5756
6091
  const adapter = adapterFor(agent);
5757
6092
  if (!adapter) {
5758
6093
  lines.push({
@@ -5817,7 +6152,7 @@ import { realpathSync } from "node:fs";
5817
6152
  // package.json
5818
6153
  var package_default = {
5819
6154
  name: "yourskills",
5820
- version: "0.4.3",
6155
+ version: "0.5.0",
5821
6156
  description: "Browse, install and keep up to date the agent skills and bundles your team publishes on yourskills.",
5822
6157
  license: "UNLICENSED",
5823
6158
  homepage: "https://yourskills.store",
@@ -6262,40 +6597,6 @@ async function runSessionStart() {
6262
6597
  const outcome = await sessionStart();
6263
6598
  emit(outcome, await upgradeCheck());
6264
6599
  }
6265
- // src/org/wire.ts
6266
- var authBase2 = (base) => `${base.replace(/\/$/, "")}/api/auth`;
6267
- async function activeOrganizationId(base, token) {
6268
- const res = await fetch(`${authBase2(base)}/get-session`, {
6269
- headers: { authorization: `Bearer ${token}` }
6270
- });
6271
- if (!res.ok)
6272
- return null;
6273
- const body = await res.json();
6274
- return body?.session?.activeOrganizationId ?? null;
6275
- }
6276
- async function listOrganizations(base, token) {
6277
- let res;
6278
- try {
6279
- res = await fetch(`${authBase2(base)}/organization/list`, {
6280
- headers: { authorization: `Bearer ${token}` }
6281
- });
6282
- } catch (error) {
6283
- throw new Error(`Could not reach ${base}: ${error instanceof Error ? error.message : String(error)}`);
6284
- }
6285
- if (!res.ok) {
6286
- throw new Error(`Could not list organizations: ${res.status} ${res.statusText}`);
6287
- }
6288
- return await res.json();
6289
- }
6290
- async function rememberOrganization(name) {
6291
- try {
6292
- const config = await readConfig();
6293
- if ((config.orgName ?? null) === name)
6294
- return;
6295
- await writeConfig(name === null ? { ...config, orgName: undefined } : { ...config, orgName: name });
6296
- } catch {}
6297
- }
6298
-
6299
6600
  // src/org/org.ts
6300
6601
  var NO_ACTIVE_ORGANIZATION = "none: choose one with `yourskills org <name>`, or in the web app";
6301
6602
  async function namedOrganization(server) {
@@ -6304,8 +6605,8 @@ async function namedOrganization(server) {
6304
6605
  return view.organization;
6305
6606
  }
6306
6607
  async function whoamiReport(server) {
6307
- const who = await whoami(server);
6308
- const organization = who.org ? await namedOrganization(server) : null;
6608
+ const who = await readSession({ server });
6609
+ const organization = who.organizationId ? await namedOrganization(server) : null;
6309
6610
  await rememberOrganization(organization?.name ?? null);
6310
6611
  return drawWhoami({ user: who.user, server: who.server, organization });
6311
6612
  }
@@ -6343,152 +6644,167 @@ function rows(orgs, active) {
6343
6644
  }
6344
6645
  async function learnActiveOrganization(server) {
6345
6646
  try {
6346
- const { token, base } = await requireToken(server);
6347
- const [orgs, active] = await Promise.all([
6348
- listOrganizations(base, token),
6349
- activeOrganizationId(base, token)
6350
- ]);
6351
- const name = orgs.find((o) => o.id === active)?.name ?? null;
6352
- await rememberOrganization(name);
6353
- return name;
6647
+ return (await organizations({ server })).active?.name ?? null;
6354
6648
  } catch {
6355
6649
  return null;
6356
6650
  }
6357
6651
  }
6358
6652
  async function organizationChoices(server) {
6359
- const { token, base } = await requireToken(server);
6360
- const [orgs, active] = await Promise.all([
6361
- listOrganizations(base, token),
6362
- activeOrganizationId(base, token)
6363
- ]);
6364
- await rememberOrganization(orgs.find((o) => o.id === active)?.name ?? null);
6365
- return orgs.map((org) => ({
6653
+ const { all, active } = await organizations({ server });
6654
+ return all.map((org) => ({
6366
6655
  name: org.name,
6367
6656
  slug: org.slug ?? null,
6368
- active: org.id === active
6657
+ active: org.id === active?.id
6369
6658
  }));
6370
6659
  }
6371
6660
  async function orgListReport(server) {
6372
- const { token, base } = await requireToken(server);
6373
- const [orgs, active] = await Promise.all([
6374
- listOrganizations(base, token),
6375
- activeOrganizationId(base, token)
6376
- ]);
6377
- await rememberOrganization(orgs.find((o) => o.id === active)?.name ?? null);
6661
+ const { all, active } = await organizations({ server });
6378
6662
  return {
6379
6663
  title: "org",
6380
- lines: rows(orgs, active),
6664
+ lines: rows(all, active?.id ?? null),
6381
6665
  json: {
6382
- active,
6383
- organizations: orgs.map((o) => ({
6666
+ active: active?.id ?? null,
6667
+ organizations: all.map((o) => ({
6384
6668
  id: o.id,
6385
6669
  name: o.name,
6386
6670
  slug: o.slug ?? null,
6387
- active: o.id === active
6671
+ active: o.id === active?.id
6388
6672
  }))
6389
6673
  }
6390
6674
  };
6391
6675
  }
6392
- async function orgSwitchReport(name, server) {
6393
- const { token, base } = await requireToken(server);
6394
- const orgs = await listOrganizations(base, token);
6395
- const wanted = name.toLowerCase();
6396
- const match = orgs.find((o) => o.name.toLowerCase() === wanted || o.slug?.toLowerCase() === wanted);
6397
- if (!match) {
6398
- return {
6399
- title: "org",
6400
- lines: [
6401
- {
6402
- mark: "fail",
6403
- text: `No organization called ${name}.`,
6404
- 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
6405
6715
  }
6406
- ],
6407
- json: { error: `unknown organization: ${name}` },
6408
- failed: true
6409
- };
6716
+ };
6717
+ }
6410
6718
  }
6411
- const res = await fetch(`${authBase2(base)}/organization/set-active`, {
6412
- method: "POST",
6413
- headers: {
6414
- "content-type": "application/json",
6415
- authorization: `Bearer ${token}`
6416
- },
6417
- body: JSON.stringify({ organizationId: match.id })
6418
- });
6419
- if (!res.ok) {
6420
- 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)
6421
6741
  return {
6422
- title: "org",
6423
- lines: [
6424
- {
6425
- mark: "fail",
6426
- text: `Could not switch to ${match.name}.`,
6427
- note: detail || `${res.status} ${res.statusText}`
6428
- }
6429
- ],
6430
- json: { error: detail || res.statusText },
6431
- failed: true
6742
+ command: "yourskills doctor --fix",
6743
+ why: `repair ${fixes.length === 1 ? "it" : "them"}`
6432
6744
  };
6433
- }
6434
- await rememberOrganization(match.name);
6435
- return {
6436
- title: "org",
6437
- lines: [{ mark: "ok", text: `Active organization: ${match.name}` }],
6438
- json: { active: match.id, name: match.name, slug: match.slug ?? null }
6439
- };
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;
6440
6751
  }
6752
+
6441
6753
  // src/doctor/doctor.ts
6442
- var REACH_TIMEOUT_MS = 5000;
6443
6754
  var liveDiagnosis = async (server) => (await createClient(server)).downstream.diagnosis.query();
6444
- function withLogin(message) {
6445
- return message.includes("yourskills login") ? message : `${message} — run \`yourskills login\``;
6446
- }
6447
6755
  function home() {
6448
6756
  const env = process.env.HOME;
6449
6757
  return env && env.length > 0 ? env : homedir4();
6450
6758
  }
6451
- async function reachable(base) {
6452
- const url = `${base.replace(/\/$/, "")}/api/auth/ok`;
6453
- try {
6454
- const res = await fetch(url, {
6455
- signal: AbortSignal.timeout(REACH_TIMEOUT_MS)
6456
- });
6457
- return {
6458
- name: "server reachable",
6459
- ok: true,
6460
- detail: `${base} (${res.status})`
6461
- };
6462
- } catch (error) {
6463
- return {
6464
- name: "server reachable",
6465
- ok: false,
6466
- detail: `${base}: ${error instanceof Error ? error.message : String(error)}`
6467
- };
6468
- }
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
+ };
6469
6775
  }
6470
6776
  async function session(ask, server) {
6471
6777
  try {
6472
- const who = await whoami(server);
6778
+ const who = await readSession({ server });
6779
+ const id = who.organizationId;
6473
6780
  return [
6474
- { name: "session valid", ok: true, detail: who.user },
6781
+ { name: "session valid", state: "pass", summary: who.user, fix: null },
6475
6782
  {
6476
6783
  name: "active organization",
6477
- ok: who.org !== null,
6478
- 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
6479
6787
  }
6480
6788
  ];
6481
6789
  } catch (error) {
6790
+ const refused = isSessionRefusal(error);
6482
6791
  return [
6483
- {
6792
+ refused ? {
6793
+ name: "session valid",
6794
+ state: "fail",
6795
+ summary: error.fact,
6796
+ fix: { name: "login", sentence: `run \`${error.next}\`` }
6797
+ } : {
6484
6798
  name: "session valid",
6485
- ok: false,
6486
- detail: withLogin(error instanceof Error ? error.message : String(error))
6799
+ state: "fail",
6800
+ summary: said(error),
6801
+ fix: null
6487
6802
  },
6488
6803
  {
6489
6804
  name: "active organization",
6490
- ok: false,
6491
- 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
6492
6808
  }
6493
6809
  ];
6494
6810
  }
@@ -6496,27 +6812,42 @@ async function session(ask, server) {
6496
6812
  function configParses() {
6497
6813
  const path = configPath();
6498
6814
  if (!existsSync5(path))
6499
- 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
+ };
6500
6821
  try {
6501
6822
  JSON.parse(readFileSync3(path, "utf8"));
6502
- return { name: "config", ok: true, detail: path };
6823
+ return { name: "config", state: "pass", summary: path, fix: null };
6503
6824
  } catch (error) {
6504
6825
  return {
6505
6826
  name: "config",
6506
- ok: false,
6507
- 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
+ }
6508
6833
  };
6509
6834
  }
6510
6835
  }
6511
6836
  function skillsResolvable() {
6512
6837
  try {
6513
6838
  const bin = skillsBin();
6514
- 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
+ };
6515
6845
  } catch (error) {
6516
6846
  return {
6517
6847
  name: "skills dependency",
6518
- ok: false,
6519
- detail: error instanceof Error ? error.message : String(error)
6848
+ state: "fail",
6849
+ summary: said(error),
6850
+ fix: null
6520
6851
  };
6521
6852
  }
6522
6853
  }
@@ -6525,7 +6856,7 @@ async function agents(nearest, local) {
6525
6856
  const found = local ? [] : [
6526
6857
  ...new Set((await listInstalled(nearest.scope)).flatMap((row) => row.agents))
6527
6858
  ];
6528
- const said = [
6859
+ const sentences = [
6529
6860
  pinned.length > 0 ? `pinned: ${pinned.join(", ")}` : "",
6530
6861
  found.length > 0 ? `found: ${found.join(", ")} (the stock CLI's labels)` : ""
6531
6862
  ].filter(Boolean);
@@ -6536,15 +6867,25 @@ async function agents(nearest, local) {
6536
6867
  const hint = suggest.length > 0 ? `did you mean ${suggest.join(", ")}?` : "run `yourskills config set agents <name>` with a name the stock CLI knows";
6537
6868
  return {
6538
6869
  name: "agents",
6539
- ok: false,
6540
- 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 }
6541
6873
  };
6542
6874
  }
6543
6875
  }
6544
- return {
6876
+ return sentences.length > 0 ? {
6877
+ name: "agents",
6878
+ state: "pass",
6879
+ summary: sentences.join(" "),
6880
+ fix: null
6881
+ } : {
6545
6882
  name: "agents",
6546
- ok: said.length > 0,
6547
- 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
+ }
6548
6889
  };
6549
6890
  }
6550
6891
  function hookCheck(gap) {
@@ -6552,22 +6893,28 @@ function hookCheck(gap) {
6552
6893
  if (gap.error)
6553
6894
  return {
6554
6895
  name,
6555
- ok: false,
6556
- 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
+ }
6557
6902
  };
6558
6903
  if (gap.missing.length === 0)
6559
6904
  return {
6560
6905
  name,
6561
- ok: true,
6562
- detail: `${gap.registered.join(" + ")} ${gap.path}`
6906
+ state: "pass",
6907
+ summary: `${gap.registered.join(" + ")} ${gap.path}`,
6908
+ fix: null
6563
6909
  };
6564
6910
  const kinds = gap.missing.join(" + ");
6565
6911
  return {
6566
6912
  name,
6567
- ok: false,
6568
- detail: `${kinds} not registered ${gap.path}`,
6913
+ state: "fail",
6914
+ summary: `${kinds} not registered`,
6569
6915
  fix: {
6570
- what: `register ${kinds} in ${gap.path}`,
6916
+ name: "register-hooks",
6917
+ sentence: `register ${kinds} in ${gap.path}`,
6571
6918
  apply: () => registerMissing(gap)
6572
6919
  }
6573
6920
  };
@@ -6576,26 +6923,30 @@ function manifestParses(nearest) {
6576
6923
  if (!existsSync5(nearest.path)) {
6577
6924
  return {
6578
6925
  name: "manifest",
6579
- ok: true,
6580
- detail: `${nearest.path} (not created yet)`
6926
+ state: "pass",
6927
+ summary: `${nearest.path} (not created yet)`,
6928
+ fix: null
6581
6929
  };
6582
6930
  }
6583
6931
  try {
6584
6932
  const raw = JSON.parse(readFileSync3(nearest.path, "utf8"));
6585
6933
  return raw && typeof raw === "object" ? {
6586
6934
  name: "manifest",
6587
- ok: true,
6588
- detail: `${nearest.path} (${nearest.scope})`
6935
+ state: "pass",
6936
+ summary: `${nearest.path} (${nearest.scope})`,
6937
+ fix: null
6589
6938
  } : {
6590
6939
  name: "manifest",
6591
- ok: false,
6592
- detail: `${nearest.path}: not an object`
6940
+ state: "fail",
6941
+ summary: `${nearest.path}: not an object`,
6942
+ fix: null
6593
6943
  };
6594
6944
  } catch (error) {
6595
6945
  return {
6596
6946
  name: "manifest",
6597
- ok: false,
6598
- detail: `${nearest.path}: ${error instanceof Error ? error.message : String(error)}`
6947
+ state: "fail",
6948
+ summary: `${nearest.path}: ${said(error)}`,
6949
+ fix: null
6599
6950
  };
6600
6951
  }
6601
6952
  }
@@ -6603,7 +6954,7 @@ function onDisk(nearest, name) {
6603
6954
  const entry = nearest.manifest.skills[name];
6604
6955
  if (entry?.unpinned)
6605
6956
  return existsSync5(entry.path);
6606
- const root = nearest.scope === "project" ? dirname5(nearest.path) : home();
6957
+ const root = nearest.scope === "project" ? dirname6(nearest.path) : home();
6607
6958
  return existsSync5(join8(root, ".agents", "skills", name));
6608
6959
  }
6609
6960
  function manifestMatchesDisk(nearest) {
@@ -6612,16 +6963,18 @@ function manifestMatchesDisk(nearest) {
6612
6963
  if (missing.length === 0)
6613
6964
  return {
6614
6965
  name: "manifest matches disk",
6615
- ok: true,
6616
- detail: `${names.length} skill(s) recorded`
6966
+ state: "pass",
6967
+ summary: `${names.length} skill(s) recorded`,
6968
+ fix: null
6617
6969
  };
6618
6970
  const list = missing.join(", ");
6619
6971
  return {
6620
6972
  name: "manifest matches disk",
6621
- ok: false,
6622
- detail: `missing on disk: ${list} — run \`yourskills install\` to restore, or \`doctor --fix\` to forget`,
6973
+ state: "fail",
6974
+ summary: `missing on disk: ${list}`,
6623
6975
  fix: {
6624
- 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"}`,
6625
6978
  apply: async () => {
6626
6979
  openManifest({ path: nearest.path, scope: nearest.scope }).forget(missing);
6627
6980
  return `forgot ${list}; \`yourskills add <name>\` brings one back`;
@@ -6635,8 +6988,9 @@ function unpinned(nearest) {
6635
6988
  return null;
6636
6989
  return {
6637
6990
  name: "unpinned",
6638
- ok: true,
6639
- 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
6640
6994
  };
6641
6995
  }
6642
6996
  async function downstreamChecks(ask, server) {
@@ -6647,8 +7001,9 @@ async function downstreamChecks(ask, server) {
6647
7001
  return [
6648
7002
  {
6649
7003
  name: "downstream",
6650
- ok: false,
6651
- 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
6652
7007
  }
6653
7008
  ];
6654
7009
  }
@@ -6656,21 +7011,24 @@ async function downstreamChecks(ask, server) {
6656
7011
  return [
6657
7012
  {
6658
7013
  name: "downstream",
6659
- ok: false,
6660
- 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
6661
7017
  }
6662
7018
  ];
6663
7019
  }
6664
7020
  return [
6665
7021
  {
6666
7022
  name: "downstream",
6667
- ok: answer.tone !== "fail",
6668
- detail: `${answer.repo.repo} — ${answer.headline}`
7023
+ state: answer.tone,
7024
+ summary: `${answer.repo.repo}: ${answer.headline}`,
7025
+ fix: null
6669
7026
  },
6670
7027
  ...answer.checks.map((check) => ({
6671
7028
  name: `downstream: ${check.key}`,
6672
- ok: check.state !== "fail",
6673
- detail: check.fix ? `${check.summary} — ${check.fix.sentence}` : check.summary
7029
+ state: check.state,
7030
+ summary: check.summary,
7031
+ fix: check.fix
6674
7032
  }))
6675
7033
  ];
6676
7034
  }
@@ -6683,16 +7041,18 @@ async function unwatchedCheck(nearest, ask, server) {
6683
7041
  } catch (error) {
6684
7042
  return {
6685
7043
  name,
6686
- ok: false,
6687
- 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
6688
7047
  };
6689
7048
  }
6690
7049
  const orphans = drift.unknown.filter((row) => row.reason === "orphaned");
6691
7050
  if (orphans.length === 0)
6692
7051
  return {
6693
7052
  name,
6694
- ok: true,
6695
- 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
6696
7056
  };
6697
7057
  const names = orphans.map((row) => row.name);
6698
7058
  const repos = [
@@ -6701,8 +7061,12 @@ async function unwatchedCheck(nearest, ask, server) {
6701
7061
  const one = names.length === 1;
6702
7062
  return {
6703
7063
  name,
6704
- ok: true,
6705
- 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
+ }
6706
7070
  };
6707
7071
  }
6708
7072
  function version(config) {
@@ -6710,20 +7074,23 @@ function version(config) {
6710
7074
  if (latest && isNewer(latest, VERSION))
6711
7075
  return {
6712
7076
  name: "version",
6713
- ok: true,
6714
- 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
+ }
6715
7083
  };
6716
- return { name: "version", ok: true, detail: VERSION };
7084
+ return { name: "version", state: "pass", summary: VERSION, fix: null };
6717
7085
  }
6718
7086
  async function diagnose(nearest, options = {}) {
6719
7087
  const config = await readConfig();
6720
- const base = serverUrl(config, options.server);
6721
7088
  const local = options.local ?? false;
6722
7089
  const checks = [configParses()];
6723
7090
  if (!local) {
6724
7091
  const account = await session(options.askOrganization ?? namedOrganization, options.server);
6725
- checks.push(await reachable(base), ...account);
6726
- if (account[0].ok) {
7092
+ checks.push(await reached(options.server), ...account);
7093
+ if (account[0].state !== "fail") {
6727
7094
  checks.push(...await downstreamChecks(options.ask ?? liveDiagnosis, options.server), await unwatchedCheck(nearest, options.askDrift ?? liveDrift, options.server));
6728
7095
  }
6729
7096
  }
@@ -6736,53 +7103,41 @@ async function diagnose(nearest, options = {}) {
6736
7103
  checks.push(version(config));
6737
7104
  return checks;
6738
7105
  }
6739
- function fixable(checks) {
6740
- return checks.filter((check) => !check.ok && check.fix !== undefined);
6741
- }
6742
7106
  async function applyFixes(checks) {
6743
7107
  const out = [];
6744
7108
  for (const check of checks) {
6745
- if (check.ok || !check.fix) {
7109
+ const [own] = fixable([check]);
7110
+ if (!own) {
6746
7111
  out.push(check);
6747
7112
  continue;
6748
7113
  }
6749
- try {
6750
- const detail = await check.fix.apply();
6751
- out.push({ ...check, ok: true, fixed: { ok: true, detail } });
6752
- } catch (error) {
6753
- out.push({
6754
- ...check,
6755
- fixed: {
6756
- ok: false,
6757
- detail: error instanceof Error ? error.message : String(error)
6758
- }
6759
- });
6760
- }
7114
+ out.push(await applied(own));
6761
7115
  }
6762
7116
  return out;
6763
7117
  }
6764
- function nextFor2(checks, fixing) {
6765
- if (!fixing && fixable(checks).length > 0)
6766
- return {
6767
- command: "yourskills doctor --fix",
6768
- why: `repair ${fixable(checks).length === 1 ? "it" : "them"}`
6769
- };
6770
- if (checks.some((check) => check.name === "session valid" && !check.ok))
6771
- return { command: "yourskills login", why: "start a session" };
6772
- if (checks.some((check) => check.name === "version" && check.detail.includes("is out")))
6773
- return { command: "yourskills upgrade", why: "get the newer yourskills" };
6774
- 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}`;
6775
7133
  }
6776
7134
  function doctorLines(checks, fixing = false) {
6777
- const width = Math.max(...checks.map((check) => check.name.length));
6778
- const lines = checks.map((check) => {
6779
- 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;
6780
- return {
6781
- mark: check.ok ? "ok" : "fail",
6782
- text: check.name.padEnd(width),
6783
- note
6784
- };
6785
- });
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
+ }));
6786
7141
  const next = nextFor2(checks, fixing);
6787
7142
  return {
6788
7143
  title: fixing ? "doctor --fix" : "doctor",
@@ -6790,11 +7145,12 @@ function doctorLines(checks, fixing = false) {
6790
7145
  json: {
6791
7146
  checks: checks.map(({ fix, ...rest }) => ({
6792
7147
  ...rest,
6793
- fixable: fix !== undefined
7148
+ fix: fix ? { name: fix.name, sentence: fix.sentence } : null,
7149
+ fixable: fix?.apply !== undefined
6794
7150
  })),
6795
7151
  fixed: checks.filter((check) => check.fixed?.ok).map((check) => check.fixed?.detail)
6796
7152
  },
6797
- failed: checks.some((check) => !check.ok),
7153
+ failed: blocks(checks),
6798
7154
  ...next ? { next } : {}
6799
7155
  };
6800
7156
  }
@@ -6806,38 +7162,6 @@ async function doctorReport(nearest, options = {}) {
6806
7162
  return { ...report, json: { ...report.json, manifest: nearest.path } };
6807
7163
  }
6808
7164
  // src/doctor/explain.ts
6809
- import { existsSync as existsSync6 } from "node:fs";
6810
- import { dirname as dirname6 } from "node:path";
6811
- function scopeLayers(resolved, manifestScope, config) {
6812
- const project = resolved.manifestPath ? {
6813
- layer: "project",
6814
- value: manifestScope,
6815
- from: `yourskills.json at ${dirname6(resolved.manifestPath)}${manifestScope ? "" : " (declares no scope)"}`
6816
- } : { layer: "project", value: null, from: "no yourskills.json above here" };
6817
- return [
6818
- {
6819
- layer: "flag",
6820
- value: resolved.source === "flag" ? "given" : null,
6821
- from: "-g or -p on the command line"
6822
- },
6823
- project,
6824
- {
6825
- layer: "config",
6826
- value: config.defaultScope ?? null,
6827
- from: `defaultScope in ${configPath()}`
6828
- },
6829
- { layer: "default", value: "global", from: "built-in default" }
6830
- ];
6831
- }
6832
- function serverFrom(config, override) {
6833
- if (override)
6834
- return "--server on the command line";
6835
- if (process.env.YOURSKILLS_URL)
6836
- return "YOURSKILLS_URL in the environment";
6837
- if (config.serverUrl)
6838
- return `server in ${configPath()}`;
6839
- return "built-in default";
6840
- }
6841
7165
  var LABEL = 12;
6842
7166
  function row(label, value, from) {
6843
7167
  return {
@@ -6846,36 +7170,32 @@ function row(label, value, from) {
6846
7170
  note: `<- ${from}`
6847
7171
  };
6848
7172
  }
6849
- function losers(layers, winner) {
6850
- 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) => ({
6851
7176
  mark: "none",
6852
- text: `${"".padEnd(LABEL)}(${layer.layer}: ${layer.value ?? "unset"}`.padEnd(LABEL + 18),
7177
+ text: `${"".padEnd(LABEL)}(${layer.layer}: ${layer.value ?? "unset"}`.padEnd(LOSER),
6853
7178
  note: `${layer.from})`
6854
7179
  }));
6855
7180
  }
7181
+ function settingRows(settings) {
7182
+ return settings.flatMap((setting) => [
7183
+ row(setting.key, setting.value, setting.from),
7184
+ ...losers(setting)
7185
+ ]);
7186
+ }
6856
7187
  async function explainReport(server) {
6857
- const [config, resolved, nearest, queue] = await Promise.all([
6858
- readConfig(),
6859
- resolveScope({}),
7188
+ const [held, nearest, queue] = await Promise.all([
7189
+ holdSettings({
7190
+ flags: { server },
7191
+ askOrganization: () => organizationTracking(server)
7192
+ }),
6860
7193
  nearestManifest(),
6861
7194
  readQueue()
6862
7195
  ]);
6863
- const track = await trackingNow({
6864
- track: config.track,
6865
- configPath: configPath(),
6866
- askOrganization: () => organizationTracking(server)
6867
- });
6868
- const manifestScope = resolved.manifestPath && existsSync6(resolved.manifestPath) ? resolved.source === "project" ? resolved.scope : null : null;
6869
- const layers = scopeLayers(resolved, manifestScope, config);
6870
- const winner = layers.find((layer) => layer.layer === resolved.source);
6871
- const lines = [
6872
- row("scope", resolved.scope, winner?.from ?? resolved.source),
6873
- ...losers(layers, resolved.source),
6874
- 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"),
6875
- row("server", serverUrl(config, server), serverFrom(config, server)),
6876
- row("track", track.value, track.from),
6877
- row("autoUpdate", config.autoUpdate === false ? "off" : "on", config.autoUpdate === undefined ? "built-in default" : `autoUpdate in ${configPath()}`)
6878
- ];
7196
+ const settings = settingsShown(held);
7197
+ const tracking = trackingOf(held);
7198
+ const lines = settingRows(settings);
6879
7199
  const skills = Object.keys(nearest.manifest.skills).length;
6880
7200
  const bundles = Object.keys(nearest.manifest.bundles).length;
6881
7201
  const loose = looseSkills(nearest.manifest).length;
@@ -6912,16 +7232,19 @@ async function explainReport(server) {
6912
7232
  title: "config --explain",
6913
7233
  lines,
6914
7234
  json: {
6915
- path: configPath(),
6916
- scope: { value: resolved.scope, source: resolved.source, layers },
6917
- agents: config.agents ?? [],
6918
- server: {
6919
- value: serverUrl(config, server),
6920
- source: serverFrom(config, server)
6921
- },
6922
- track: track.value === "on",
6923
- trackingState: track.state,
6924
- 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,
6925
7248
  manifest: { path: nearest.path, skills, bundles, loose },
6926
7249
  queue: { path: queuePath(), events: queue.length },
6927
7250
  hooks: hooksJson
@@ -7199,7 +7522,25 @@ var TABLE = {
7199
7522
  hint: "org <name>",
7200
7523
  shows: (facts) => facts.feed.organizations === undefined ? "" : facts.feed.organizations <= 1 ? "only this one" : count2(facts.feed.organizations - 1, "other"),
7201
7524
  when: (facts) => facts.signedIn,
7202
- 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
+ }
7203
7544
  }
7204
7545
  ],
7205
7546
  invitesHooks: false,
@@ -7272,6 +7613,7 @@ var TABLE = {
7272
7613
  label: "search",
7273
7614
  hint: "search <query>",
7274
7615
  when: (facts) => facts.signedIn,
7616
+ opens: { kind: "search", query: "" },
7275
7617
  says: () => "filter the same catalog by name and description"
7276
7618
  }
7277
7619
  ],
@@ -7341,7 +7683,21 @@ var TABLE = {
7341
7683
  label: "remove",
7342
7684
  hint: "remove <name...>",
7343
7685
  when: (facts) => facts.skills > 0,
7344
- 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
+ }
7345
7701
  }
7346
7702
  ],
7347
7703
  invitesHooks: true,
@@ -7617,7 +7973,7 @@ var TABLE = {
7617
7973
  lines: fixes.map((check) => ({
7618
7974
  mark: "fail",
7619
7975
  text: check.name,
7620
- note: check.fix?.what ?? ""
7976
+ note: check.fix.sentence
7621
7977
  })),
7622
7978
  notes: [
7623
7979
  "Every other check stays as it is; a session or an upgrade is yours to run."
@@ -7663,14 +8019,62 @@ var TABLE = {
7663
8019
  label: "scope",
7664
8020
  hint: "config set scope project",
7665
8021
  shows: (facts) => facts.scope,
7666
- 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
+ }
7667
8048
  },
7668
8049
  {
7669
8050
  value: "setup.agents",
7670
8051
  label: "agents",
7671
8052
  hint: "config set agents <name>",
7672
8053
  shows: (facts) => facts.agents.length > 0 ? count2(facts.agents.length, "agent") : "unset",
7673
- 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
+ }
7674
8078
  },
7675
8079
  {
7676
8080
  value: "setup.explain",
@@ -8173,14 +8577,13 @@ function paletteGroups(facts) {
8173
8577
  }
8174
8578
  return groups;
8175
8579
  }
8176
- // src/shell/facts.ts
8177
- import { existsSync as existsSync7 } from "node:fs";
8580
+ // src/shell/machine.ts
8581
+ import { existsSync as existsSync6 } from "node:fs";
8178
8582
  async function paletteFacts(scope, server) {
8179
- const [account, nearest, config, agents] = await Promise.all([
8583
+ const [account, nearest, held] = await Promise.all([
8180
8584
  localAccount(server),
8181
8585
  nearestManifest(scope),
8182
- readConfig(),
8183
- resolveAgents()
8586
+ holdSettings({ flags: { server } })
8184
8587
  ]);
8185
8588
  const skills = Object.keys(nearest.manifest.skills).length;
8186
8589
  return {
@@ -8188,31 +8591,26 @@ async function paletteFacts(scope, server) {
8188
8591
  email: account.email,
8189
8592
  organization: account.organization,
8190
8593
  server: account.server,
8191
- firstRun: !account.signedIn && skills === 0 && !config.welcomed,
8192
- agentsOnDisk: HOOK_ADAPTERS.filter((adapter) => existsSync7(adapter.home())).map((adapter) => adapter.agent),
8193
- 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,
8194
8597
  scope: nearest.scope,
8195
8598
  manifest: nearest.path,
8196
8599
  skills,
8197
8600
  bundles: Object.keys(nearest.manifest.bundles).length,
8198
8601
  loose: looseSkills(nearest.manifest).length,
8199
- settings: CONFIG_KEYS.filter((key) => configValue(config, key) !== UNSET).length,
8602
+ settings: settingsShown(held).filter((setting) => setting.decided).length,
8200
8603
  feed: NO_FEED
8201
8604
  };
8202
8605
  }
8203
- // src/shell/screens/home.tsx
8204
- import { Box as Box14, Text as Text16, useApp as useApp7, useInput as useInput8 } from "ink";
8205
- import { useCallback as useCallback3, useEffect as useEffect9, useMemo as useMemo4, useRef, useState as useState11 } from "react";
8206
-
8207
- // src/shell/feed.ts
8208
- async function feedFacts(client, where, server) {
8606
+ async function feedFacts(client, where, server, choices = organizationChoices) {
8209
8607
  const installed = Object.keys(where.manifest.skills).length > 0;
8210
8608
  const [skills, bundles, passes, drift, orgs] = await Promise.allSettled([
8211
8609
  client.skills.list.query({}),
8212
8610
  client.bundles.list.query({}),
8213
8611
  client.mirror.passes.query(),
8214
8612
  installed ? driftNow(client, where) : Promise.resolve(null),
8215
- organizationChoices(server)
8613
+ choices(server)
8216
8614
  ]);
8217
8615
  return {
8218
8616
  ...skills.status === "fulfilled" ? { skills: skills.value.length } : {},
@@ -8222,8 +8620,6 @@ async function feedFacts(client, where, server) {
8222
8620
  ...orgs.status === "fulfilled" ? { organizations: orgs.value.length } : {}
8223
8621
  };
8224
8622
  }
8225
-
8226
- // src/shell/probe.ts
8227
8623
  var importProbe = async (where) => {
8228
8624
  const found = await listInstalled(where.scope);
8229
8625
  const recorded = new Set(Object.keys(where.manifest.skills));
@@ -8237,7 +8633,7 @@ var doctorProbe = async (where, server) => {
8237
8633
  local: true,
8238
8634
  ...server ? { server } : {}
8239
8635
  });
8240
- const bad = checks.filter((check) => !check.ok);
8636
+ const bad = checks.filter((check) => check.state === "fail");
8241
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(", ")}`;
8242
8638
  };
8243
8639
  var PROBES = {
@@ -8247,6 +8643,259 @@ var PROBES = {
8247
8643
  function probeFor(value) {
8248
8644
  return value ? PROBES[value] : undefined;
8249
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
+ }
8831
+ // src/shell/screens/home.tsx
8832
+ import { Box as Box14, Text as Text16, useApp as useApp7, useInput as useInput8 } from "ink";
8833
+ import {
8834
+ useCallback as useCallback3,
8835
+ useEffect as useEffect9,
8836
+ useMemo as useMemo4,
8837
+ useState as useState11,
8838
+ useSyncExternalStore
8839
+ } from "react";
8840
+
8841
+ // src/draw.tsx
8842
+ import { jsx as jsx15 } from "react/jsx-runtime";
8843
+ function drawInk(answer, {
8844
+ onError,
8845
+ onSummary,
8846
+ onReport,
8847
+ exitWhenDone,
8848
+ onBack,
8849
+ onNext,
8850
+ working
8851
+ }) {
8852
+ const leaves = exitWhenDone === undefined ? {} : { exitWhenDone };
8853
+ const back = onBack ? { onBack } : {};
8854
+ const next = onNext ? { onNext } : {};
8855
+ const reported = onReport ? { onReport } : {};
8856
+ switch (answer.kind) {
8857
+ case "text":
8858
+ return answer.style === "document" ? /* @__PURE__ */ jsx15(HelpScreen, {
8859
+ text: answer.body,
8860
+ ...leaves,
8861
+ ...back
8862
+ }) : /* @__PURE__ */ jsx15(NoticeScreen, {
8863
+ title: answer.title,
8864
+ message: answer.body,
8865
+ ...leaves,
8866
+ ...back
8867
+ });
8868
+ case "report": {
8869
+ const says = working ?? answer.working;
8870
+ return /* @__PURE__ */ jsx15(ReportScreen, {
8871
+ title: answer.title,
8872
+ load: answer.load,
8873
+ onError,
8874
+ ...says ? { working: says } : {},
8875
+ ...reported,
8876
+ ...leaves,
8877
+ ...back,
8878
+ ...next
8879
+ });
8880
+ }
8881
+ case "rows":
8882
+ return /* @__PURE__ */ jsx15(CatalogScreen, {
8883
+ title: answer.title,
8884
+ load: async () => toRows(await answer.load()),
8885
+ onError
8886
+ });
8887
+ case "stream":
8888
+ return /* @__PURE__ */ jsx15(RunScreen, {
8889
+ run: answer.run,
8890
+ onError,
8891
+ ...onSummary ? { onDone: onSummary } : {},
8892
+ ...reported,
8893
+ ...leaves,
8894
+ ...back,
8895
+ ...next
8896
+ });
8897
+ }
8898
+ }
8250
8899
 
8251
8900
  // src/args.ts
8252
8901
  function configCommand2(rest, explain) {
@@ -8502,7 +9151,7 @@ function runnable(typed) {
8502
9151
 
8503
9152
  // src/shell/screens/keys.tsx
8504
9153
  import { Box as Box11, Text as Text13 } from "ink";
8505
- import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
9154
+ import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
8506
9155
  var KEYS = [
8507
9156
  ["^ v", "move the cursor", "move, or scroll"],
8508
9157
  ["enter", "run the highlighted row", "confirm the layer's action"],
@@ -8528,15 +9177,15 @@ function KeysScreen({ onBack }) {
8528
9177
  children: [
8529
9178
  /* @__PURE__ */ jsxs13(Text13, {
8530
9179
  children: [
8531
- /* @__PURE__ */ jsx15(Text13, {
9180
+ /* @__PURE__ */ jsx16(Text13, {
8532
9181
  dimColor: true,
8533
9182
  children: "".padEnd(key)
8534
9183
  }),
8535
- /* @__PURE__ */ jsx15(Text13, {
9184
+ /* @__PURE__ */ jsx16(Text13, {
8536
9185
  dimColor: true,
8537
9186
  children: "at the prompt".padEnd(half)
8538
9187
  }),
8539
- /* @__PURE__ */ jsx15(Text13, {
9188
+ /* @__PURE__ */ jsx16(Text13, {
8540
9189
  dimColor: true,
8541
9190
  children: "in a layer"
8542
9191
  })
@@ -8544,21 +9193,21 @@ function KeysScreen({ onBack }) {
8544
9193
  }),
8545
9194
  KEYS.map(([k, root, layer]) => /* @__PURE__ */ jsxs13(Text13, {
8546
9195
  children: [
8547
- /* @__PURE__ */ jsx15(Text13, {
9196
+ /* @__PURE__ */ jsx16(Text13, {
8548
9197
  color: tone.primary,
8549
9198
  children: k.padEnd(key)
8550
9199
  }),
8551
- /* @__PURE__ */ jsx15(Text13, {
9200
+ /* @__PURE__ */ jsx16(Text13, {
8552
9201
  children: clip(root, half - 2).padEnd(half)
8553
9202
  }),
8554
- /* @__PURE__ */ jsx15(Text13, {
9203
+ /* @__PURE__ */ jsx16(Text13, {
8555
9204
  children: clip(layer, interior - key - half)
8556
9205
  })
8557
9206
  ]
8558
9207
  }, k))
8559
9208
  ]
8560
9209
  }),
8561
- /* @__PURE__ */ jsx15(Back, {
9210
+ /* @__PURE__ */ jsx16(Back, {
8562
9211
  width,
8563
9212
  onBack
8564
9213
  })
@@ -8569,7 +9218,7 @@ function KeysScreen({ onBack }) {
8569
9218
  // src/shell/screens/picker.tsx
8570
9219
  import { Box as Box12, Text as Text14, useInput as useInput6 } from "ink";
8571
9220
  import { useEffect as useEffect8, useMemo as useMemo3, useState as useState9 } from "react";
8572
- import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
9221
+ import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
8573
9222
  function PickScreen({
8574
9223
  title,
8575
9224
  rows,
@@ -8639,7 +9288,7 @@ function PickScreen({
8639
9288
  });
8640
9289
  const names = nameWidth(rows);
8641
9290
  const page = visible.slice(first, first + size);
8642
- return /* @__PURE__ */ jsx16(Frame, {
9291
+ return /* @__PURE__ */ jsx17(Frame, {
8643
9292
  title,
8644
9293
  width,
8645
9294
  children: /* @__PURE__ */ jsxs14(Box12, {
@@ -8647,26 +9296,26 @@ function PickScreen({
8647
9296
  children: [
8648
9297
  /* @__PURE__ */ jsxs14(Box12, {
8649
9298
  children: [
8650
- /* @__PURE__ */ jsx16(Text14, {
9299
+ /* @__PURE__ */ jsx17(Text14, {
8651
9300
  color: tone.primary,
8652
9301
  children: "> "
8653
9302
  }),
8654
- /* @__PURE__ */ jsx16(SearchInput, {
9303
+ /* @__PURE__ */ jsx17(SearchInput, {
8655
9304
  value: query,
8656
9305
  placeholder: "type to filter"
8657
9306
  })
8658
9307
  ]
8659
9308
  }),
8660
- /* @__PURE__ */ jsx16(Divider, {
9309
+ /* @__PURE__ */ jsx17(Divider, {
8661
9310
  width
8662
9311
  }),
8663
- rows.length === 0 ? /* @__PURE__ */ jsx16(Text14, {
9312
+ rows.length === 0 ? /* @__PURE__ */ jsx17(Text14, {
8664
9313
  dimColor: true,
8665
9314
  children: empty
8666
- }) : visible.length === 0 ? /* @__PURE__ */ jsx16(Text14, {
9315
+ }) : visible.length === 0 ? /* @__PURE__ */ jsx17(Text14, {
8667
9316
  dimColor: true,
8668
9317
  children: `Nothing matches "${query}".`
8669
- }) : page.map((row, i) => /* @__PURE__ */ jsx16(SkillRow, {
9318
+ }) : page.map((row, i) => /* @__PURE__ */ jsx17(SkillRow, {
8670
9319
  row,
8671
9320
  width: width - 2,
8672
9321
  names,
@@ -8674,20 +9323,20 @@ function PickScreen({
8674
9323
  cursor: first + i === cursor,
8675
9324
  selected: single ? row.name === marked : picked.has(row.name)
8676
9325
  }, row.name)),
8677
- /* @__PURE__ */ jsx16(Divider, {
9326
+ /* @__PURE__ */ jsx17(Divider, {
8678
9327
  width
8679
9328
  }),
8680
9329
  /* @__PURE__ */ jsxs14(Box12, {
8681
9330
  children: [
8682
- /* @__PURE__ */ jsx16(Text14, {
9331
+ /* @__PURE__ */ jsx17(Text14, {
8683
9332
  dimColor: true,
8684
9333
  children: `${position(cursor, visible.length)} `
8685
9334
  }),
8686
- picked.size > 0 && !single ? /* @__PURE__ */ jsx16(Text14, {
9335
+ picked.size > 0 && !single ? /* @__PURE__ */ jsx17(Text14, {
8687
9336
  color: tone.notice,
8688
9337
  children: `${mark.selected} ${picked.size} marked `
8689
9338
  }) : null,
8690
- /* @__PURE__ */ jsx16(Hints, {
9339
+ /* @__PURE__ */ jsx17(Hints, {
8691
9340
  hints: [
8692
9341
  ["^v", "move"],
8693
9342
  ...single ? [] : [["space", "mark"]],
@@ -8705,7 +9354,7 @@ function PickScreen({
8705
9354
  // src/shell/screens/welcome.tsx
8706
9355
  import { Box as Box13, Text as Text15, useInput as useInput7 } from "ink";
8707
9356
  import { useState as useState10 } from "react";
8708
- import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
9357
+ import { jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
8709
9358
  function WelcomeScreen({
8710
9359
  agents,
8711
9360
  onStart,
@@ -8736,16 +9385,16 @@ function WelcomeScreen({
8736
9385
  const gap = Math.max(interior - 2 - label.length - hint.length, 1);
8737
9386
  return /* @__PURE__ */ jsxs15(Text15, {
8738
9387
  children: [
8739
- /* @__PURE__ */ jsx17(Text15, {
9388
+ /* @__PURE__ */ jsx18(Text15, {
8740
9389
  color: tone.primary,
8741
9390
  children: `${active ? mark.cursor : " "} `
8742
9391
  }),
8743
- /* @__PURE__ */ jsx17(Text15, {
9392
+ /* @__PURE__ */ jsx18(Text15, {
8744
9393
  bold: active,
8745
9394
  color: active ? tone.primary : undefined,
8746
9395
  children: label
8747
9396
  }),
8748
- /* @__PURE__ */ jsx17(Text15, {
9397
+ /* @__PURE__ */ jsx18(Text15, {
8749
9398
  dimColor: true,
8750
9399
  children: `${" ".repeat(gap)}${hint}`
8751
9400
  })
@@ -8759,34 +9408,34 @@ function WelcomeScreen({
8759
9408
  /* @__PURE__ */ jsxs15(Box13, {
8760
9409
  flexDirection: "column",
8761
9410
  children: [
8762
- /* @__PURE__ */ jsx17(Text15, {
9411
+ /* @__PURE__ */ jsx18(Text15, {
8763
9412
  children: "Welcome. Three steps, about a minute."
8764
9413
  }),
8765
- /* @__PURE__ */ jsx17(Blank, {}),
9414
+ /* @__PURE__ */ jsx18(Blank, {}),
8766
9415
  steps.map(([n, what, how]) => /* @__PURE__ */ jsxs15(Text15, {
8767
9416
  children: [
8768
- /* @__PURE__ */ jsx17(Text15, {
9417
+ /* @__PURE__ */ jsx18(Text15, {
8769
9418
  color: tone.primary,
8770
9419
  children: `${n} `
8771
9420
  }),
8772
- /* @__PURE__ */ jsx17(Text15, {
9421
+ /* @__PURE__ */ jsx18(Text15, {
8773
9422
  children: what.padEnd(16)
8774
9423
  }),
8775
- /* @__PURE__ */ jsx17(Text15, {
9424
+ /* @__PURE__ */ jsx18(Text15, {
8776
9425
  dimColor: true,
8777
9426
  children: clip(how, interior - 19)
8778
9427
  })
8779
9428
  ]
8780
9429
  }, n)),
8781
- /* @__PURE__ */ jsx17(Blank, {}),
9430
+ /* @__PURE__ */ jsx18(Blank, {}),
8782
9431
  row("start", "", choice === "start"),
8783
9432
  row("skip, I know what I am doing", "yourskills login", choice === "skip")
8784
9433
  ]
8785
9434
  }),
8786
- /* @__PURE__ */ jsx17(Divider, {
9435
+ /* @__PURE__ */ jsx18(Divider, {
8787
9436
  width
8788
9437
  }),
8789
- /* @__PURE__ */ jsx17(Hints, {
9438
+ /* @__PURE__ */ jsx18(Hints, {
8790
9439
  hints: [
8791
9440
  ["enter", choice === "start" ? "start" : "skip"],
8792
9441
  ["esc", "skip"]
@@ -8797,7 +9446,7 @@ function WelcomeScreen({
8797
9446
  }
8798
9447
 
8799
9448
  // src/shell/screens/home.tsx
8800
- import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
9449
+ import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
8801
9450
  function failure(title, error) {
8802
9451
  const message = error instanceof Error ? error.message : String(error);
8803
9452
  return {
@@ -8841,25 +9490,25 @@ function AccountLine({ facts, width }) {
8841
9490
  children: [
8842
9491
  /* @__PURE__ */ jsxs16(Text16, {
8843
9492
  children: [
8844
- /* @__PURE__ */ jsx18(Text16, {
9493
+ /* @__PURE__ */ jsx19(Text16, {
8845
9494
  color: facts.signedIn ? "green" : tone.notice,
8846
9495
  children: facts.signedIn ? mark.ok : mark.fail
8847
9496
  }),
8848
- /* @__PURE__ */ jsx18(Text16, {
9497
+ /* @__PURE__ */ jsx19(Text16, {
8849
9498
  color: facts.signedIn ? undefined : tone.notice,
8850
9499
  children: ` ${clip(head, room - 2)}`
8851
9500
  }),
8852
- showOrg ? /* @__PURE__ */ jsx18(Text16, {
9501
+ showOrg ? /* @__PURE__ */ jsx19(Text16, {
8853
9502
  color: tone.primary,
8854
9503
  children: org
8855
9504
  }) : null,
8856
- showTail ? /* @__PURE__ */ jsx18(Text16, {
9505
+ showTail ? /* @__PURE__ */ jsx19(Text16, {
8857
9506
  dimColor: true,
8858
9507
  children: tail
8859
9508
  }) : null
8860
9509
  ]
8861
9510
  }),
8862
- /* @__PURE__ */ jsx18(Text16, {
9511
+ /* @__PURE__ */ jsx19(Text16, {
8863
9512
  dimColor: true,
8864
9513
  children: clip(right, inner - room)
8865
9514
  })
@@ -8868,70 +9517,39 @@ function AccountLine({ facts, width }) {
8868
9517
  }
8869
9518
  function StaleLine({ stale, width }) {
8870
9519
  const text = `${mark.ok} ${stale.latest} is out; you have ${stale.current}. type upgrade`;
8871
- return /* @__PURE__ */ jsx18(Box14, {
9520
+ return /* @__PURE__ */ jsx19(Box14, {
8872
9521
  width: width - FRAME_CHROME,
8873
- children: /* @__PURE__ */ jsx18(Text16, {
9522
+ children: /* @__PURE__ */ jsx19(Text16, {
8874
9523
  color: tone.notice,
8875
9524
  children: clip(text, width - FRAME_CHROME)
8876
9525
  })
8877
9526
  });
8878
9527
  }
8879
9528
  var RUN = "run";
8880
- var REST_MS = 250;
8881
- var ASK_NPM = () => upgradeCheck();
8882
- var ASK_REGISTRY = feedFacts;
8883
9529
  var NO_EXIT_CODE = () => {
8884
9530
  return;
8885
9531
  };
8886
9532
  function HomeScreen({
8887
- where: initialWhere,
8888
- resite,
8889
- facts: initial,
8890
- reload,
9533
+ known,
8891
9534
  server,
8892
- openClient,
8893
9535
  onError,
8894
9536
  onSummary,
8895
9537
  onWelcomed = async () => {
8896
9538
  return;
8897
- },
8898
- checkUpgrade = ASK_NPM,
8899
- loadFeed = ASK_REGISTRY,
8900
- probes: probeSet = probeFor
9539
+ }
8901
9540
  }) {
8902
9541
  const { exit } = useApp7();
8903
9542
  const width = useFrameWidth();
8904
- const [facts, setFacts] = useState11(initial);
8905
- 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;
8906
9547
  const [query, setQuery] = useState11("");
8907
9548
  const [cursor, setCursor] = useState11(0);
8908
9549
  const [route, setRoute] = useState11(null);
8909
- const [welcome, setWelcome] = useState11(initial.firstRun);
9550
+ const [welcome, setWelcome] = useState11(() => known.read().facts.firstRun);
8910
9551
  const [wizard, setWizard] = useState11(false);
8911
- const [stale, setStale] = useState11(undefined);
8912
- useEffect9(() => {
8913
- let live = true;
8914
- checkUpgrade().then((found) => {
8915
- if (live)
8916
- setStale(found);
8917
- }).catch(() => {
8918
- return;
8919
- });
8920
- return () => {
8921
- live = false;
8922
- };
8923
- }, [checkUpgrade]);
8924
- const [feed, setFeed] = useState11(NO_FEED);
8925
- const [probes, setProbes] = useState11({});
8926
- const [probing, setProbing] = useState11(null);
8927
- const shown = useMemo4(() => ({
8928
- ...facts,
8929
- feed: {
8930
- ...feed,
8931
- ...stale === undefined ? {} : { upgrade: stale ? stale.latest : null }
8932
- }
8933
- }), [facts, feed, stale]);
8934
- const groups = useMemo4(() => paletteGroups(shown), [shown]);
9552
+ const groups = useMemo4(() => paletteGroups(facts), [facts]);
8935
9553
  const typed = useMemo4(() => typedCommand(query), [query]);
8936
9554
  const visible = useMemo4(() => {
8937
9555
  const filtered = filterCommands(groups, query);
@@ -8949,48 +9567,17 @@ function HomeScreen({
8949
9567
  useEffect9(() => {
8950
9568
  setCursor((c) => Math.max(0, Math.min(c, rows.length - 1)));
8951
9569
  }, [rows.length]);
8952
- const held = useRef(null);
8953
- const connect = useCallback3(async () => {
8954
- held.current ??= await openClient();
8955
- return held.current;
8956
- }, [openClient]);
8957
- useEffect9(() => {
8958
- if (!facts.signedIn)
8959
- return;
8960
- let live = true;
8961
- connect().then((client) => loadFeed(client, where, server)).then((found) => {
8962
- if (live)
8963
- setFeed(found);
8964
- }).catch(() => {
8965
- return;
8966
- });
8967
- return () => {
8968
- live = false;
8969
- };
8970
- }, [facts.signedIn, connect, loadFeed, where, server]);
9570
+ const connect = known.client;
8971
9571
  const checkSession = useCallback3(async () => facts.signedIn, [facts.signedIn]);
8972
- const answering = useCallback3((title, working, load) => ({
8973
- kind: "report",
8974
- title,
8975
- working,
8976
- load: async () => {
8977
- try {
8978
- return await load();
8979
- } catch (error) {
8980
- return failure(title, error);
8981
- }
9572
+ const failing = useCallback3((title, error) => ({
9573
+ kind: "answer",
9574
+ working: "",
9575
+ answer: {
9576
+ kind: "report",
9577
+ title,
9578
+ load: async () => failure(title, error)
8982
9579
  }
8983
9580
  }), []);
8984
- const answeringRun = useCallback3((run) => run.kind === "stream" ? run : {
8985
- ...run,
8986
- report: async () => {
8987
- try {
8988
- return await run.report();
8989
- } catch (error) {
8990
- return failure(run.title, error);
8991
- }
8992
- }
8993
- }, []);
8994
9581
  const ctx = useMemo4(() => ({
8995
9582
  where,
8996
9583
  resite,
@@ -9020,20 +9607,9 @@ function HomeScreen({
9020
9607
  ...answer.tab ? { tab: answer.tab } : {},
9021
9608
  ...answer.query ? { query: answer.query } : {}
9022
9609
  };
9023
- case "text":
9024
- return answering(answer.title, plan, async () => ({
9025
- title: answer.title,
9026
- lines: [{ mark: "ok", text: answer.body }],
9027
- json: { ok: true }
9028
- }));
9029
- case "report":
9030
- return answering(answer.title, plan, answer.load);
9031
- case "rows":
9032
- return answering(answer.title, plan, async () => answer.report(await answer.load()));
9033
- case "stream":
9034
- return { kind: "run", run: answeringRun(answer.run) };
9035
9610
  }
9036
- }, [answering, answeringRun]);
9611
+ return { kind: "answer", answer, working: plan };
9612
+ }, []);
9037
9613
  const routeForCommand = useCallback3(async (command) => {
9038
9614
  if (command.kind === "error")
9039
9615
  return null;
@@ -9044,99 +9620,29 @@ function HomeScreen({
9044
9620
  const asked = await row.confirm?.(command, ctx, facts);
9045
9621
  const answer = asked ?? await row.run(command, ctx);
9046
9622
  if (command.kind === "logout")
9047
- held.current = null;
9623
+ known.forget();
9048
9624
  return routeFrom(answer, say(command, facts));
9049
- }, [catalogDoor, ctx, facts, routeFrom]);
9050
- const ownRoute = useCallback3(async (value) => {
9051
- switch (value) {
9052
- case "catalog.search":
9053
- return { kind: "browse", tab: "skills" };
9054
- case "installed.remove":
9055
- return {
9056
- kind: "pick",
9057
- title: "remove",
9058
- rows: installedRows(where).map((row) => ({
9059
- name: row.name,
9060
- description: `${row.bundle ?? "loose"} ${shortRef(row.ref)}`
9061
- })),
9062
- empty: "Nothing installed here.",
9063
- verb: "remove",
9064
- command: (names) => ({ kind: "remove", names })
9065
- };
9066
- case "setup.scope":
9067
- return {
9068
- kind: "pick",
9069
- title: "scope",
9070
- rows: [
9071
- {
9072
- name: "global",
9073
- description: "installs land in your home directory, where every project sees them"
9074
- },
9075
- {
9076
- name: "project",
9077
- description: "installs land in this project's yourskills.json, and travel with the repo"
9078
- }
9079
- ],
9080
- empty: "",
9081
- verb: "set",
9082
- single: true,
9083
- marked: facts.scope,
9084
- command: (names) => ({
9085
- kind: "config",
9086
- action: "set",
9087
- key: "scope",
9088
- value: names[0] ?? ""
9089
- })
9090
- };
9091
- case "setup.agents": {
9092
- const valid = await knownAgents() ?? [];
9093
- const onDisk = new Set(facts.agentsOnDisk);
9094
- return {
9095
- kind: "pick",
9096
- title: "agents",
9097
- rows: valid.map((name) => ({
9098
- name,
9099
- description: onDisk.has(name) ? "on this machine" : ""
9100
- })),
9101
- empty: "Could not ask the `skills` CLI which agent names it accepts. Run `yourskills doctor`.",
9102
- verb: "set",
9103
- preset: facts.agents,
9104
- command: (names) => ({
9105
- kind: "config",
9106
- action: "set",
9107
- key: "agents",
9108
- value: names.join(",")
9109
- })
9110
- };
9111
- }
9112
- case "account.org": {
9113
- const orgs = await organizationChoices(server);
9114
- const active = orgs.find((org) => org.active)?.name;
9115
- return {
9116
- kind: "pick",
9117
- title: "org",
9118
- rows: orgs.map((org) => ({
9119
- name: org.name,
9120
- description: org.slug ?? ""
9121
- })),
9122
- empty: "You are not a member of any organization yet.",
9123
- verb: "switch",
9124
- single: true,
9125
- ...active ? { marked: active } : {},
9126
- command: (names) => ({ kind: "org", name: names[0] ?? "" })
9127
- };
9128
- }
9129
- }
9130
- }, [where, server, facts]);
9131
- 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]);
9132
9638
  const open = useCallback3((pending, title) => {
9133
9639
  pending.then((next) => {
9134
9640
  if (next)
9135
9641
  setRoute(next);
9136
9642
  }).catch((error) => {
9137
- setRoute(answering(title, "", async () => failure(title, error)));
9643
+ setRoute(failing(title, error));
9138
9644
  });
9139
- }, [answering]);
9645
+ }, [failing]);
9140
9646
  const runText = useCallback3((text) => {
9141
9647
  const parsed = typedCommand(text);
9142
9648
  if (!runnable(parsed))
@@ -9144,29 +9650,10 @@ function HomeScreen({
9144
9650
  open(routeForCommand(parsed.command), parsed.text);
9145
9651
  }, [open, routeForCommand]);
9146
9652
  const current = rows[cursor];
9653
+ const resting = current?.value;
9147
9654
  useEffect9(() => {
9148
- const value = current?.value;
9149
- const probe = probeSet(value);
9150
- if (!value || !probe || probes[value] !== undefined)
9151
- return;
9152
- let live = true;
9153
- const timer = setTimeout(() => {
9154
- setProbing(value);
9155
- probe(where, server).then((text) => {
9156
- if (live)
9157
- setProbes((was) => ({ ...was, [value]: text }));
9158
- }).catch(() => {
9159
- return;
9160
- }).finally(() => {
9161
- if (live)
9162
- setProbing(null);
9163
- });
9164
- }, REST_MS);
9165
- return () => {
9166
- live = false;
9167
- clearTimeout(timer);
9168
- };
9169
- }, [current?.value, probes, probeSet, where, server]);
9655
+ known.rest(resting);
9656
+ }, [known, resting]);
9170
9657
  const activate = useCallback3(() => {
9171
9658
  const item = rows[cursor];
9172
9659
  if (!item)
@@ -9189,32 +9676,24 @@ function HomeScreen({
9189
9676
  setQuery(item.hint.includes("<") ? `${bare} ` : bare);
9190
9677
  setCursor(0);
9191
9678
  }, [rows, cursor]);
9192
- const back = useCallback3(() => {
9679
+ const land = useCallback3(() => {
9193
9680
  setRoute(null);
9194
9681
  setQuery("");
9195
9682
  setCursor(0);
9196
- setProbes({});
9197
- reload().then(setFacts).catch(() => {
9198
- return;
9199
- });
9200
- resite().then(setWhere).catch(() => {
9201
- return;
9202
- });
9203
- }, [reload, resite]);
9683
+ }, []);
9684
+ const back = useCallback3(() => {
9685
+ land();
9686
+ known.back();
9687
+ }, [land, known]);
9204
9688
  const afterLogin = useCallback3(() => {
9205
- learnActiveOrganization(server).then(() => reload()).then(setFacts).catch(() => {
9206
- return;
9207
- });
9208
9689
  if (!wizard) {
9209
- back();
9690
+ land();
9691
+ known.loggedIn();
9210
9692
  return;
9211
9693
  }
9212
9694
  setWizard(false);
9213
- reload().then((next) => {
9214
- setFacts(next);
9215
- setRoute({ kind: "browse" });
9216
- }).catch(() => setRoute(null));
9217
- }, [wizard, back, reload, server]);
9695
+ known.loggedIn().then(() => setRoute({ kind: "browse" }));
9696
+ }, [wizard, land, known]);
9218
9697
  const leaveWelcome = useCallback3((start) => {
9219
9698
  setWelcome(false);
9220
9699
  onWelcomed().catch(() => {
@@ -9227,7 +9706,7 @@ function HomeScreen({
9227
9706
  }, [onWelcomed]);
9228
9707
  const browseTab = route?.kind === "browse" ? route.tab : undefined;
9229
9708
  const browseQuery = route?.kind === "browse" ? route.query : undefined;
9230
- const browser = useCallback3((live) => /* @__PURE__ */ jsx18(BrowserScreen, {
9709
+ const browser = useCallback3((live) => /* @__PURE__ */ jsx19(BrowserScreen, {
9231
9710
  client: live,
9232
9711
  where,
9233
9712
  onError,
@@ -9236,6 +9715,14 @@ function HomeScreen({
9236
9715
  ...browseTab ? { initialTab: browseTab } : {},
9237
9716
  ...browseQuery ? { initialQuery: browseQuery } : {}
9238
9717
  }), [where, onError, onSummary, back, browseTab, browseQuery]);
9718
+ const drawn = useMemo4(() => route?.kind === "answer" ? drawInk(route.answer, {
9719
+ onError: (error) => setRoute(failing(route.answer.title, error)),
9720
+ onSummary,
9721
+ exitWhenDone: false,
9722
+ onBack: back,
9723
+ onNext: runText,
9724
+ working: route.working
9725
+ }) : null, [route, onSummary, back, runText, failing]);
9239
9726
  useInput8((input, key) => {
9240
9727
  if (key.upArrow) {
9241
9728
  setCursor((c) => Math.max(0, c - 1));
@@ -9261,64 +9748,43 @@ function HomeScreen({
9261
9748
  }
9262
9749
  }, { isActive: route === null && !welcome });
9263
9750
  if (welcome) {
9264
- return /* @__PURE__ */ jsx18(WelcomeScreen, {
9751
+ return /* @__PURE__ */ jsx19(WelcomeScreen, {
9265
9752
  agents: facts.agentsOnDisk,
9266
9753
  onStart: () => leaveWelcome(true),
9267
9754
  onSkip: () => leaveWelcome(false)
9268
9755
  });
9269
9756
  }
9270
9757
  if (route?.kind === "login") {
9271
- return /* @__PURE__ */ jsx18(LoginScreen, {
9758
+ return /* @__PURE__ */ jsx19(LoginScreen, {
9272
9759
  onError,
9273
9760
  onDone: afterLogin,
9274
9761
  onBack: back
9275
9762
  });
9276
9763
  }
9277
9764
  if (route?.kind === "keys") {
9278
- return /* @__PURE__ */ jsx18(KeysScreen, {
9765
+ return /* @__PURE__ */ jsx19(KeysScreen, {
9279
9766
  onBack: back
9280
9767
  });
9281
9768
  }
9282
- if (route?.kind === "report") {
9283
- return /* @__PURE__ */ jsx18(ReportScreen, {
9284
- title: route.title,
9285
- load: route.load,
9286
- working: route.working,
9287
- onError,
9288
- exitWhenDone: false,
9289
- onBack: back,
9290
- onNext: runText
9291
- });
9292
- }
9293
- if (route?.kind === "run") {
9294
- return /* @__PURE__ */ jsx18(RunScreen, {
9295
- run: route.run,
9296
- exitWhenDone: false,
9297
- onError,
9298
- onDone: onSummary,
9299
- onBack: back,
9300
- onNext: runText
9301
- });
9302
- }
9769
+ if (drawn)
9770
+ return drawn;
9303
9771
  if (route?.kind === "pick") {
9304
- const picked = route;
9305
- return /* @__PURE__ */ jsx18(PickScreen, {
9306
- title: picked.title,
9307
- rows: picked.rows,
9308
- empty: picked.empty,
9309
- verb: picked.verb,
9310
- ...picked.single ? { single: true } : {},
9311
- ...picked.marked ? { marked: picked.marked } : {},
9312
- ...picked.preset ? { preset: picked.preset } : {},
9772
+ const { ask, rows: choices, chosen } = route;
9773
+ return /* @__PURE__ */ jsx19(PickScreen, {
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 },
9313
9779
  onPick: (names) => {
9314
- open(routeForCommand(picked.command(names)), picked.title);
9780
+ open(routeForCommand(ask.opens(names)), ask.title);
9315
9781
  },
9316
9782
  onBack: back
9317
9783
  });
9318
9784
  }
9319
9785
  if (route?.kind === "ask") {
9320
9786
  const { spec } = route;
9321
- return /* @__PURE__ */ jsx18(ConfirmScreen, {
9787
+ return /* @__PURE__ */ jsx19(ConfirmScreen, {
9322
9788
  title: spec.title,
9323
9789
  headline: spec.headline,
9324
9790
  lines: spec.lines,
@@ -9326,12 +9792,16 @@ function HomeScreen({
9326
9792
  yes: spec.yes,
9327
9793
  no: spec.no,
9328
9794
  argv: spec.argv,
9329
- onYes: () => setRoute(answering(spec.title, spec.working, spec.run)),
9795
+ onYes: () => setRoute({
9796
+ kind: "answer",
9797
+ working: spec.working,
9798
+ answer: { kind: "report", title: spec.title, load: spec.run }
9799
+ }),
9330
9800
  onNo: back
9331
9801
  });
9332
9802
  }
9333
9803
  if (route?.kind === "browse") {
9334
- return /* @__PURE__ */ jsx18(AuthGate, {
9804
+ return /* @__PURE__ */ jsx19(AuthGate, {
9335
9805
  title: "browse",
9336
9806
  check: checkSession,
9337
9807
  connect,
@@ -9342,43 +9812,43 @@ function HomeScreen({
9342
9812
  });
9343
9813
  }
9344
9814
  const highlighted = current ? doorFor(current.value) : undefined;
9345
- 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) : "";
9346
9816
  const found = current ? probes[current.value] ?? (probing === current.value ? "looking..." : "") : "";
9347
9817
  const empty = typed?.command.kind === "error" && typed.command.next ? `${mark.fail} ${typed.command.message} try: ${typed.command.next.replace(/^yourskills /, "")}` : "No matching commands.";
9348
9818
  return /* @__PURE__ */ jsxs16(Frame, {
9349
9819
  title: "yourskills",
9350
9820
  width,
9351
9821
  children: [
9352
- /* @__PURE__ */ jsx18(AccountLine, {
9353
- facts: shown,
9822
+ /* @__PURE__ */ jsx19(AccountLine, {
9823
+ facts,
9354
9824
  width
9355
9825
  }),
9356
- stale ? /* @__PURE__ */ jsx18(StaleLine, {
9826
+ stale ? /* @__PURE__ */ jsx19(StaleLine, {
9357
9827
  stale,
9358
9828
  width
9359
9829
  }) : null,
9360
- /* @__PURE__ */ jsx18(Divider, {
9830
+ /* @__PURE__ */ jsx19(Divider, {
9361
9831
  width
9362
9832
  }),
9363
- /* @__PURE__ */ jsx18(CommandPalette, {
9833
+ /* @__PURE__ */ jsx19(CommandPalette, {
9364
9834
  groups: visible,
9365
9835
  query,
9366
9836
  cursor,
9367
9837
  width,
9368
9838
  emptyMessage: empty
9369
9839
  }),
9370
- /* @__PURE__ */ jsx18(Divider, {
9840
+ /* @__PURE__ */ jsx19(Divider, {
9371
9841
  width
9372
9842
  }),
9373
- /* @__PURE__ */ jsx18(Text16, {
9843
+ /* @__PURE__ */ jsx19(Text16, {
9374
9844
  dimColor: true,
9375
9845
  children: clip(description, width - FRAME_CHROME)
9376
9846
  }),
9377
- /* @__PURE__ */ jsx18(Text16, {
9847
+ /* @__PURE__ */ jsx19(Text16, {
9378
9848
  dimColor: true,
9379
9849
  children: found ? clip(` ${found}`, width - FRAME_CHROME) : " "
9380
9850
  }),
9381
- /* @__PURE__ */ jsx18(Hints, {
9851
+ /* @__PURE__ */ jsx19(Hints, {
9382
9852
  hints: [
9383
9853
  ["^v", "move"],
9384
9854
  ["tab", "complete"],
@@ -9391,7 +9861,7 @@ function HomeScreen({
9391
9861
  });
9392
9862
  }
9393
9863
  // src/drive.tsx
9394
- import { jsx as jsx19 } from "react/jsx-runtime";
9864
+ import { jsx as jsx20 } from "react/jsx-runtime";
9395
9865
  var liveChores = {
9396
9866
  sendUsage: flushQuietly,
9397
9867
  offerHooks: ensureHooksRegistered,
@@ -9466,7 +9936,7 @@ async function drawPlain(answer, output, io) {
9466
9936
  let code = EXIT.ok;
9467
9937
  const done = await plainRun(answer.run, (report) => {
9468
9938
  code = emit2(report, output, io);
9469
- });
9939
+ }, io);
9470
9940
  return "pending" in done ? landed(done) : code;
9471
9941
  }
9472
9942
  }
@@ -9474,7 +9944,7 @@ async function drawPlain(answer, output, io) {
9474
9944
  async function screenFor(command, ctx, resite, fail, exit) {
9475
9945
  const summarise = (summary) => exit.raise(landed(summary));
9476
9946
  const report = (result) => exit.raise(result.failed ? EXIT.failed : EXIT.ok);
9477
- const gate = (title, build) => /* @__PURE__ */ jsx19(AuthGate, {
9947
+ const gate = (title, build) => /* @__PURE__ */ jsx20(AuthGate, {
9478
9948
  title,
9479
9949
  build,
9480
9950
  connect: ctx.client,
@@ -9484,13 +9954,17 @@ async function screenFor(command, ctx, resite, fail, exit) {
9484
9954
  const { row, answer } = await answerFor(command, ctx);
9485
9955
  if (answer.kind === "home") {
9486
9956
  const reload = () => paletteFacts(ctx.where.scope, ctx.server);
9487
- return /* @__PURE__ */ jsx19(HomeScreen, {
9957
+ const known = knowMachine({
9488
9958
  where: ctx.where,
9489
- resite,
9490
9959
  facts: await reload(),
9960
+ ...ctx.server ? { server: ctx.server } : {},
9491
9961
  reload,
9962
+ resite,
9963
+ client: ctx.client
9964
+ });
9965
+ return /* @__PURE__ */ jsx20(HomeScreen, {
9966
+ known,
9492
9967
  ...ctx.server ? { server: ctx.server } : {},
9493
- openClient: ctx.client,
9494
9968
  onError: fail,
9495
9969
  onSummary: summarise,
9496
9970
  onWelcomed: async () => {
@@ -9503,54 +9977,26 @@ async function screenFor(command, ctx, resite, fail, exit) {
9503
9977
  });
9504
9978
  }
9505
9979
  if (answer.kind === "login")
9506
- return /* @__PURE__ */ jsx19(LoginScreen, {
9980
+ return /* @__PURE__ */ jsx20(LoginScreen, {
9507
9981
  onError: fail
9508
9982
  });
9509
9983
  if (answer.kind === "browse")
9510
- return gate("browse", (client) => /* @__PURE__ */ jsx19(BrowserScreen, {
9984
+ return gate("browse", (client) => /* @__PURE__ */ jsx20(BrowserScreen, {
9511
9985
  client,
9512
9986
  where: ctx.where,
9513
9987
  onError: fail,
9514
9988
  onSummary: summarise
9515
9989
  }));
9516
- const drawn = drawInk(answer, fail, summarise, report);
9990
+ const drawn = drawInk(answer, {
9991
+ onError: fail,
9992
+ onSummary: summarise,
9993
+ onReport: report
9994
+ });
9517
9995
  return sessionFirst(row, answer) ? gate(answer.title, () => drawn) : drawn;
9518
9996
  }
9519
9997
  function sessionFirst(row, answer) {
9520
9998
  return answer.kind === "stream" ? answer.run.needsSession : row.needs === "session";
9521
9999
  }
9522
- function drawInk(answer, fail, summarise, report) {
9523
- switch (answer.kind) {
9524
- case "text":
9525
- return answer.style === "document" ? /* @__PURE__ */ jsx19(HelpScreen, {
9526
- text: answer.body
9527
- }) : /* @__PURE__ */ jsx19(NoticeScreen, {
9528
- title: answer.title,
9529
- message: answer.body
9530
- });
9531
- case "report":
9532
- return /* @__PURE__ */ jsx19(ReportScreen, {
9533
- title: answer.title,
9534
- load: answer.load,
9535
- onError: fail,
9536
- onReport: report,
9537
- ...answer.working ? { working: answer.working } : {}
9538
- });
9539
- case "rows":
9540
- return /* @__PURE__ */ jsx19(CatalogScreen, {
9541
- title: answer.title,
9542
- load: async () => toRows(await answer.load()),
9543
- onError: fail
9544
- });
9545
- case "stream":
9546
- return /* @__PURE__ */ jsx19(RunScreen, {
9547
- run: answer.run,
9548
- onError: fail,
9549
- onDone: summarise,
9550
- onReport: report
9551
- });
9552
- }
9553
- }
9554
10000
  async function runInk(command, ctx, resite, io, exit) {
9555
10001
  let failure = null;
9556
10002
  let frame = null;