konstruct 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1197 @@
1
+ import {
2
+ AGENT_REGISTRY,
3
+ Banner,
4
+ KONSTRUCT_DIR,
5
+ MultiSelect,
6
+ StatusMessage,
7
+ addSkillToManifest,
8
+ diffHashes,
9
+ exists,
10
+ getAgentLabels,
11
+ getAgentSkillDirs,
12
+ hashDirectory,
13
+ initCommand,
14
+ parseSkillEntry,
15
+ parseSource,
16
+ readConfig,
17
+ readManifest,
18
+ removeSkillFromManifest,
19
+ writeConfig
20
+ } from "./chunk-MYTZHNE6.js";
21
+
22
+ // src/cli/index.ts
23
+ import { Command } from "commander";
24
+ import { fileURLToPath } from "url";
25
+ import { dirname as dirname2, resolve as resolve2 } from "path";
26
+ import { readFileSync } from "fs";
27
+ import pc from "picocolors";
28
+
29
+ // src/cli/commands/add.tsx
30
+ import { render, Box as Box2, useApp } from "ink";
31
+ import { useState as useState3, useCallback, useEffect as useEffect2 } from "react";
32
+
33
+ // src/core/installer.ts
34
+ import { mkdir, rm as rm2, cp } from "fs/promises";
35
+ import { resolve, join as join3, relative } from "path";
36
+
37
+ // src/core/git.ts
38
+ import simpleGit from "simple-git";
39
+ import { join } from "path";
40
+ import { mkdtemp, rm } from "fs/promises";
41
+ import { tmpdir } from "os";
42
+ var CLONE_TIMEOUT_MS = 6e4;
43
+ var GitCloneError = class extends Error {
44
+ constructor(message, url, isTimeout = false, isAuthError = false) {
45
+ super(message);
46
+ this.name = "GitCloneError";
47
+ this.url = url;
48
+ this.isTimeout = isTimeout;
49
+ this.isAuthError = isAuthError;
50
+ }
51
+ /** Infer auth transport from the URL. */
52
+ getAuthType() {
53
+ return this.url.startsWith("git@") || this.url.startsWith("ssh://") ? "ssh" : "https";
54
+ }
55
+ };
56
+ function httpsToSshUrl(url) {
57
+ const match = url.match(/^https:\/\/(github\.com|gitlab\.com)\/(.+)$/);
58
+ if (!match) return null;
59
+ return `git@${match[1]}:${match[2]}`;
60
+ }
61
+ function formatAuthTroubleshootingGuide(url, transport) {
62
+ const displayUrl = url.replace(/^(https?:\/\/|git@|ssh:\/\/)/, "").replace(/\.git$/, "").replace(":", "/");
63
+ let guide = `Authentication failed for ${displayUrl}.
64
+
65
+ Troubleshooting steps:
66
+
67
+ `;
68
+ guide += ` 1. Verify the repository exists and you have access.
69
+
70
+ `;
71
+ if (transport === "https" || transport === "both") {
72
+ guide += ` ${transport === "both" ? "2" : "2"}. HTTPS \u2014 check your credential state:
73
+ `;
74
+ guide += ` gh auth status
75
+ `;
76
+ guide += ` gh auth login
77
+
78
+ `;
79
+ }
80
+ if (transport === "ssh" || transport === "both") {
81
+ const num = transport === "both" ? "3" : "2";
82
+ guide += ` ${num}. SSH \u2014 check your key state:
83
+ `;
84
+ guide += ` ssh-add -l # list loaded keys
85
+ `;
86
+ guide += ` ssh -T git@github.com # test the connection
87
+
88
+ `;
89
+ }
90
+ if (transport === "both") {
91
+ guide += ` 4. If using corporate SSO, make sure your SSH key or
92
+ `;
93
+ guide += ` token has been authorized for your organization.
94
+ `;
95
+ }
96
+ return guide.trimEnd();
97
+ }
98
+ async function cloneRepo(url, ref, options) {
99
+ const useSsh = options?.ssh ?? false;
100
+ if (useSsh) {
101
+ const sshUrl = httpsToSshUrl(url) ?? url;
102
+ return attemptClone(sshUrl, ref, url, "ssh");
103
+ }
104
+ try {
105
+ return await attemptClone(url, ref, url, "https");
106
+ } catch (e) {
107
+ if (!(e instanceof GitCloneError) || !e.isAuthError) throw e;
108
+ const sshUrl = httpsToSshUrl(url);
109
+ if (!sshUrl) {
110
+ throw new GitCloneError(formatAuthTroubleshootingGuide(url, "https"), url, false, true);
111
+ }
112
+ console.error(" ! HTTPS auth failed, retrying with SSH\u2026");
113
+ return attemptClone(sshUrl, ref, url, "both");
114
+ }
115
+ }
116
+ async function attemptClone(url, ref, originalUrl, transportsTried) {
117
+ const tempDir = await mkdtemp(join(tmpdir(), "konstruct-"));
118
+ const git = simpleGit({ timeout: { block: CLONE_TIMEOUT_MS } });
119
+ const cloneOptions = ["--depth", "1"];
120
+ if (ref) cloneOptions.push("--branch", ref);
121
+ try {
122
+ await git.clone(url, tempDir, cloneOptions);
123
+ return tempDir;
124
+ } catch (error) {
125
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {
126
+ });
127
+ const msg = error instanceof Error ? error.message : String(error);
128
+ const isTimeout = msg.includes("block timeout") || msg.includes("timed out");
129
+ const isAuthError = msg.includes("Authentication failed") || msg.includes("could not read Username") || msg.includes("Permission denied") || msg.includes("Repository not found");
130
+ if (isTimeout) {
131
+ throw new GitCloneError(
132
+ `Clone timed out after 60 s \u2014 this often happens with private repos.
133
+ ` + formatAuthTroubleshootingGuide(originalUrl, transportsTried),
134
+ originalUrl,
135
+ true,
136
+ false
137
+ );
138
+ }
139
+ if (isAuthError) {
140
+ throw new GitCloneError(
141
+ formatAuthTroubleshootingGuide(originalUrl, transportsTried),
142
+ originalUrl,
143
+ false,
144
+ true
145
+ );
146
+ }
147
+ throw new GitCloneError(`Failed to clone ${url}: ${msg}`, originalUrl, false, false);
148
+ }
149
+ }
150
+ async function cleanupTempDir(dir) {
151
+ const { realpathSync } = await import("fs");
152
+ const { sep } = await import("path");
153
+ const normalizedDir = realpathSync(dir);
154
+ const normalizedTmpDir = realpathSync(tmpdir());
155
+ if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) {
156
+ throw new Error("Attempted to clean up a directory outside of the system temp directory");
157
+ }
158
+ await rm(dir, { recursive: true, force: true });
159
+ }
160
+
161
+ // src/core/discover.ts
162
+ import { readdir, readFile } from "fs/promises";
163
+ import { join as join2, dirname } from "path";
164
+ import matter from "gray-matter";
165
+ var SKILL_FILENAME = "SKILL.md";
166
+ var MAX_DEPTH = 3;
167
+ async function discoverSkills(rootPath, subpath) {
168
+ const searchPath = subpath ? join2(rootPath, subpath) : rootPath;
169
+ const skillFiles = await findSkillFiles(searchPath);
170
+ const skills = [];
171
+ for (const file of skillFiles) {
172
+ const skill = await parseSkillFile(file);
173
+ if (skill) skills.push(skill);
174
+ }
175
+ return skills;
176
+ }
177
+ async function findSkillFiles(dirPath, depth = 0) {
178
+ if (depth > MAX_DEPTH) return [];
179
+ let entries;
180
+ try {
181
+ entries = await readdir(dirPath, { withFileTypes: true });
182
+ } catch {
183
+ return [];
184
+ }
185
+ const results = [];
186
+ for (const entry of entries) {
187
+ if (entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
188
+ const fullPath = join2(dirPath, entry.name);
189
+ if (entry.isDirectory()) {
190
+ results.push(...await findSkillFiles(fullPath, depth + 1));
191
+ } else if (entry.name.toLowerCase() === SKILL_FILENAME.toLowerCase()) {
192
+ results.push(fullPath);
193
+ }
194
+ }
195
+ return results;
196
+ }
197
+ async function parseSkillFile(filePath) {
198
+ const content = await readFile(filePath, "utf-8");
199
+ let data;
200
+ try {
201
+ const result = matter(content);
202
+ data = result.data;
203
+ } catch {
204
+ data = extractFrontmatterFields(content);
205
+ }
206
+ if (!data.name || !data.description) {
207
+ const fallbackData = extractFrontmatterFields(content);
208
+ if (fallbackData.name && fallbackData.description) {
209
+ data = fallbackData;
210
+ }
211
+ }
212
+ if (!data.name || !data.description) return null;
213
+ return {
214
+ name: String(data.name),
215
+ description: String(data.description),
216
+ path: dirname(filePath),
217
+ // the whole directory is the skill
218
+ metadata: data
219
+ };
220
+ }
221
+ function extractFrontmatterFields(content) {
222
+ const block = content.match(/^---\n([\s\S]*?)\n---/);
223
+ if (!block) return {};
224
+ const result = {};
225
+ const nameMatch = block[1]?.match(/^name:\s*(.+)$/m);
226
+ const descMatch = block[1]?.match(/^description:\s*(.+)$/m);
227
+ if (nameMatch) result.name = nameMatch[1]?.trim();
228
+ if (descMatch) result.description = descMatch[1]?.trim();
229
+ return result;
230
+ }
231
+
232
+ // src/core/installer.ts
233
+ async function installGitSkill(source, skillName, options = {}) {
234
+ const installDirs = await resolveInstallDirs(options);
235
+ let tempDir;
236
+ try {
237
+ tempDir = await cloneRepo(source.url, source.ref, { ssh: options.ssh });
238
+ const skills = await discoverSkills(tempDir, source.subpath);
239
+ let skill = skills.find((s) => s.name === skillName);
240
+ if (!skill && skills.length === 1) {
241
+ skill = skills[0];
242
+ }
243
+ if (!skill) {
244
+ const found = skills.map((s) => `"${s.name}"`).join(", ");
245
+ throw new Error(
246
+ `Skill "${skillName}" not found in ${source.url}${source.subpath ? `/${source.subpath}` : ""}. ` + (found ? `Found: ${found}` : "No SKILL.md files discovered.")
247
+ );
248
+ }
249
+ const installedPaths = await copyToAll(skill.path, skillName, installDirs);
250
+ return { success: true, skill: skillName, paths: installedPaths };
251
+ } catch (error) {
252
+ return {
253
+ success: false,
254
+ skill: skillName,
255
+ paths: [],
256
+ error: error instanceof Error ? error.message : String(error)
257
+ };
258
+ } finally {
259
+ if (tempDir) await cleanupTempDir(tempDir).catch(() => {
260
+ });
261
+ }
262
+ }
263
+ async function discoverSkillsFromSource(source, options = {}) {
264
+ const tempDir = await cloneRepo(source.url, source.ref, { ssh: options.ssh });
265
+ try {
266
+ const skills = await discoverSkills(tempDir, source.subpath);
267
+ return skills.map((s) => ({
268
+ name: s.name,
269
+ description: s.description,
270
+ repoPath: relative(tempDir, s.path)
271
+ // e.g. "skills/canvas-design"
272
+ }));
273
+ } finally {
274
+ await cleanupTempDir(tempDir).catch(() => {
275
+ });
276
+ }
277
+ }
278
+ async function checkSkillForUpdates(source, skillName, options = {}) {
279
+ const installDirs = await resolveInstallDirs(options);
280
+ const localPath = join3(installDirs[0], skillName);
281
+ if (!await exists(localPath)) return null;
282
+ let tempDir;
283
+ try {
284
+ tempDir = await cloneRepo(source.url, source.ref, { ssh: options.ssh });
285
+ const skills = await discoverSkills(tempDir, source.subpath);
286
+ let skill = skills.find((s) => s.name === skillName);
287
+ if (!skill && skills.length === 1) skill = skills[0];
288
+ if (!skill) return null;
289
+ const [remoteHashes, localHashes] = await Promise.all([
290
+ hashDirectory(skill.path),
291
+ hashDirectory(localPath)
292
+ ]);
293
+ const diff = diffHashes(localHashes, remoteHashes);
294
+ return { ...diff, upToDate: diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0 };
295
+ } finally {
296
+ if (tempDir) await cleanupTempDir(tempDir).catch(() => {
297
+ });
298
+ }
299
+ }
300
+ async function installUserSkill(source, skillName, options = {}) {
301
+ const installDirs = await resolveInstallDirs(options);
302
+ const sourcePath = resolve(source.url);
303
+ if (!await exists(sourcePath)) {
304
+ return {
305
+ success: false,
306
+ skill: skillName,
307
+ paths: [],
308
+ error: `User skill path not found: ${sourcePath}`
309
+ };
310
+ }
311
+ try {
312
+ const installedPaths = await copyToAll(sourcePath, skillName, installDirs);
313
+ return { success: true, skill: skillName, paths: installedPaths };
314
+ } catch (error) {
315
+ return {
316
+ success: false,
317
+ skill: skillName,
318
+ paths: [],
319
+ error: error instanceof Error ? error.message : String(error)
320
+ };
321
+ }
322
+ }
323
+ async function resolveInstallDirs(options) {
324
+ if (options.customPath) return [options.customPath];
325
+ const config = await getEffectiveConfig(options.global ?? false);
326
+ const agents = options.agents ?? (config.agents.length > 0 ? config.agents : config.global?.defaultAgents ?? ["claude"]);
327
+ return getAgentSkillDirs(agents, options.global, process.cwd());
328
+ }
329
+ async function copyToAll(sourcePath, skillName, installDirs) {
330
+ const paths = [];
331
+ for (const dir of installDirs) {
332
+ const targetPath = join3(dir, skillName);
333
+ await mkdir(dir, { recursive: true });
334
+ await rm2(targetPath, { recursive: true, force: true });
335
+ await cp(sourcePath, targetPath, { recursive: true });
336
+ paths.push(targetPath);
337
+ }
338
+ return paths;
339
+ }
340
+ async function getEffectiveConfig(global) {
341
+ if (global) {
342
+ const g2 = await readConfig(process.cwd(), true);
343
+ return g2 ?? { version: 1, agents: ["claude"] };
344
+ }
345
+ const project = await readConfig();
346
+ if (project) return project;
347
+ const g = await readConfig(process.cwd(), true);
348
+ return g ?? { version: 1, agents: ["claude"] };
349
+ }
350
+
351
+ // src/cli/utils.ts
352
+ var globalDirToSlug = new Map(
353
+ AGENT_REGISTRY.filter((a) => a.globalSkillsDir).map((a) => [a.globalSkillsDir, a.slug])
354
+ );
355
+ function formatInstallTargets(paths, customPath) {
356
+ if (customPath) return customPath;
357
+ const agents = [];
358
+ for (const p of paths) {
359
+ const parent = p.replace(/\/[^/]+$/, "");
360
+ const slug = globalDirToSlug.get(parent);
361
+ if (slug) {
362
+ agents.push(slug);
363
+ continue;
364
+ }
365
+ const localMatch = p.match(/\.([^/.]+)\/(?:skills|rules)\/[^/]+$/);
366
+ if (localMatch) {
367
+ agents.push(localMatch[1]);
368
+ continue;
369
+ }
370
+ agents.push(p);
371
+ }
372
+ return agents.join(", ");
373
+ }
374
+
375
+ // src/cli/components/Spinner.tsx
376
+ import { Text } from "ink";
377
+ import { useState, useEffect } from "react";
378
+ import { jsx, jsxs } from "react/jsx-runtime";
379
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
380
+ function Spinner({ label }) {
381
+ const [frame, setFrame] = useState(0);
382
+ useEffect(() => {
383
+ const id = setInterval(() => {
384
+ setFrame((prev) => (prev + 1) % FRAMES.length);
385
+ }, 80);
386
+ return () => clearInterval(id);
387
+ }, []);
388
+ return /* @__PURE__ */ jsxs(Text, { children: [
389
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: FRAMES[frame] }),
390
+ " ",
391
+ label
392
+ ] });
393
+ }
394
+
395
+ // src/cli/components/Select.tsx
396
+ import { Text as Text2, Box, useInput } from "ink";
397
+ import { useState as useState2 } from "react";
398
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
399
+ function Select({ prompt, items, onSelect }) {
400
+ const [cursor, setCursor] = useState2(0);
401
+ useInput((input, key) => {
402
+ if (key.upArrow) {
403
+ setCursor((prev) => (prev - 1 + items.length) % items.length);
404
+ } else if (key.downArrow) {
405
+ setCursor((prev) => (prev + 1) % items.length);
406
+ } else if (key.return) {
407
+ onSelect(cursor);
408
+ }
409
+ });
410
+ if (!process.stdin.isTTY) {
411
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
412
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: prompt }),
413
+ items.map((item, i) => /* @__PURE__ */ jsxs2(Text2, { children: [
414
+ " ",
415
+ i + 1,
416
+ ". ",
417
+ item
418
+ ] }, i))
419
+ ] });
420
+ }
421
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
422
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: prompt }),
423
+ items.map((item, i) => /* @__PURE__ */ jsxs2(Text2, { children: [
424
+ " ",
425
+ i === cursor ? /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u203A" }) : " ",
426
+ " ",
427
+ i === cursor ? /* @__PURE__ */ jsx2(Text2, { bold: true, children: item }) : item
428
+ ] }, i)),
429
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " \u2191\u2193 move enter select" })
430
+ ] });
431
+ }
432
+
433
+ // src/cli/commands/add.tsx
434
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
435
+ function AddApp({ source, options: initialOptions }) {
436
+ const { exit } = useApp();
437
+ const [phase, setPhase] = useState3("parsing");
438
+ const [messages, setMessages] = useState3([]);
439
+ const [spinnerLabel, setSpinnerLabel] = useState3("");
440
+ const [skills, setSkills] = useState3([]);
441
+ const [parsed, setParsed] = useState3(null);
442
+ const [options, setOptions] = useState3(initialOptions);
443
+ useEffect2(() => {
444
+ if (phase === "done") exit();
445
+ }, [phase, exit]);
446
+ function finish() {
447
+ setSpinnerLabel("");
448
+ setPhase("done");
449
+ }
450
+ function addMsg(variant, text) {
451
+ setMessages((prev) => [...prev, { variant, text }]);
452
+ }
453
+ const [initialized, setInitialized] = useState3(false);
454
+ if (!initialized) {
455
+ setInitialized(true);
456
+ (async () => {
457
+ let parsedSource;
458
+ try {
459
+ parsedSource = parseSource(source);
460
+ } catch (e) {
461
+ addMsg("error", e instanceof Error ? e.message : String(e));
462
+ finish();
463
+ return;
464
+ }
465
+ setParsed(parsedSource);
466
+ if (!options.global) {
467
+ const localManifest = await readManifest(process.cwd());
468
+ if (!localManifest) {
469
+ addMsg("warn", "No skills.json found in the current directory.");
470
+ setPhase("no-manifest");
471
+ return;
472
+ }
473
+ }
474
+ await startInstall(parsedSource, options);
475
+ })();
476
+ }
477
+ const onNoManifestSelect = useCallback(async (index) => {
478
+ const opts = { ...options };
479
+ if (index === 0) {
480
+ opts.global = true;
481
+ setOptions(opts);
482
+ } else {
483
+ const { initCommand: initCommand2 } = await import("./init-CJ7EZ75L.js");
484
+ await initCommand2();
485
+ }
486
+ if (parsed) await startInstall(parsed, opts);
487
+ }, [options, parsed]);
488
+ async function startInstall(parsedSource, opts) {
489
+ if (parsedSource.type === "file" || opts.user) {
490
+ if (opts.user && parsedSource.type !== "file") {
491
+ addMsg("error", "--user flag requires a file: source (e.g. file:./my-skill)");
492
+ finish();
493
+ return;
494
+ }
495
+ const skillName = deriveSkillName(source, parsedSource);
496
+ setSpinnerLabel(`Adding "${skillName}"\u2026`);
497
+ setPhase("installing");
498
+ const result = await installUserSkill(parsedSource, skillName, {
499
+ global: opts.global,
500
+ customPath: opts.path
501
+ });
502
+ if (!result.success) {
503
+ addMsg("error", result.error ?? "Unknown error");
504
+ finish();
505
+ return;
506
+ }
507
+ addMsg("success", `Added "${skillName}"`);
508
+ await addSkillToManifest(skillName, source, {
509
+ isUserSkill: true,
510
+ cwd: opts.global ? KONSTRUCT_DIR : void 0,
511
+ customPath: opts.path
512
+ });
513
+ addMsg("info", `Installed to: ${formatInstallTargets(result.paths, opts.path)}`);
514
+ addMsg("info", "Added to skills.json");
515
+ finish();
516
+ return;
517
+ }
518
+ setSpinnerLabel("Cloning and discovering skills\u2026");
519
+ setPhase("discovering");
520
+ let discovered;
521
+ try {
522
+ discovered = await discoverSkillsFromSource(parsedSource, { ssh: opts.ssh });
523
+ } catch (e) {
524
+ addMsg("error", e instanceof Error ? e.message : String(e));
525
+ finish();
526
+ return;
527
+ }
528
+ addMsg("success", "Discovery complete");
529
+ if (discovered.length === 0) {
530
+ addMsg("error", "No SKILL.md files found in that repository.");
531
+ finish();
532
+ return;
533
+ }
534
+ if (discovered.length === 1) {
535
+ addMsg("info", `Found 1 skill: "${discovered[0].name}"`);
536
+ await installPicks(parsedSource, opts, [discovered[0]], discovered);
537
+ } else {
538
+ setSpinnerLabel("");
539
+ setSkills(discovered);
540
+ setPhase("selecting");
541
+ }
542
+ }
543
+ const onSkillsConfirm = useCallback(async (indices) => {
544
+ if (indices.length === 0) {
545
+ addMsg("info", "Nothing selected.");
546
+ finish();
547
+ return;
548
+ }
549
+ const picks = indices.map((i) => skills[i]);
550
+ if (parsed) await installPicks(parsed, options, picks, skills);
551
+ }, [skills, parsed, options]);
552
+ async function installPicks(parsedSource, opts, picks, allSkills) {
553
+ setPhase("installing");
554
+ let installed = 0;
555
+ for (const chosen of picks) {
556
+ const persistedSource = parsedSource.subpath || allSkills.length === 1 ? source : serializeSource(parsedSource, chosen.repoPath);
557
+ const installSource = {
558
+ ...parsedSource,
559
+ subpath: chosen.repoPath || void 0
560
+ };
561
+ setSpinnerLabel(`Installing "${chosen.name}"\u2026`);
562
+ const result = await installGitSkill(installSource, chosen.name, {
563
+ global: opts.global,
564
+ customPath: opts.path,
565
+ ssh: opts.ssh
566
+ });
567
+ if (!result.success) {
568
+ addMsg("error", result.error ?? "Unknown error");
569
+ continue;
570
+ }
571
+ addMsg("success", `Installed "${chosen.name}"`);
572
+ await addSkillToManifest(chosen.name, persistedSource, {
573
+ cwd: opts.global ? KONSTRUCT_DIR : void 0,
574
+ customPath: opts.path
575
+ });
576
+ addMsg("info", `Installed to: ${formatInstallTargets(result.paths, opts.path)}`);
577
+ installed++;
578
+ }
579
+ addMsg("info", `${installed}/${picks.length} skill(s) added to skills.json`);
580
+ finish();
581
+ }
582
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", children: [
583
+ /* @__PURE__ */ jsx3(Banner, {}),
584
+ messages.map((m, i) => /* @__PURE__ */ jsx3(StatusMessage, { variant: m.variant, children: m.text }, i)),
585
+ phase === "no-manifest" && /* @__PURE__ */ jsx3(
586
+ Select,
587
+ {
588
+ prompt: "How would you like to proceed?",
589
+ items: ["Install globally (default agents)", "Initialize this project and install here"],
590
+ onSelect: onNoManifestSelect
591
+ }
592
+ ),
593
+ phase === "selecting" && skills.length > 0 && /* @__PURE__ */ jsx3(
594
+ MultiSelect,
595
+ {
596
+ prompt: "Select skills to install:",
597
+ items: skills.map((s) => s.name),
598
+ onConfirm: onSkillsConfirm
599
+ }
600
+ ),
601
+ (phase === "discovering" || phase === "installing") && spinnerLabel && /* @__PURE__ */ jsx3(Spinner, { label: spinnerLabel })
602
+ ] });
603
+ }
604
+ function serializeSource(parsed, repoPath) {
605
+ const ref = parsed.ref ? `#${parsed.ref}` : "";
606
+ if (parsed.type === "github") {
607
+ const match = parsed.url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
608
+ if (match) return `github:${match[1]}/${match[2]}/${repoPath}${ref}`;
609
+ }
610
+ if (parsed.type === "gitlab") {
611
+ const match = parsed.url.match(/gitlab\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
612
+ if (match) return `gitlab:${match[1]}/${match[2]}/${repoPath}${ref}`;
613
+ }
614
+ return `git:${parsed.url}${ref}`;
615
+ }
616
+ function deriveSkillName(source, parsed) {
617
+ if (parsed.subpath) {
618
+ const segments = parsed.subpath.split("/").filter(Boolean);
619
+ if (segments.length > 0) return segments[segments.length - 1];
620
+ }
621
+ if (source.startsWith("file:")) {
622
+ const path = source.slice("file:".length).replace(/\/+$/, "");
623
+ const segments = path.split("/").filter(Boolean);
624
+ return segments[segments.length - 1] ?? "skill";
625
+ }
626
+ try {
627
+ const url = new URL(parsed.url);
628
+ const parts = url.pathname.split("/").filter(Boolean);
629
+ const repo = parts[parts.length - 1]?.replace(/\.git$/, "");
630
+ return repo ?? "skill";
631
+ } catch {
632
+ return "skill";
633
+ }
634
+ }
635
+ async function addCommand(source, options) {
636
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx3(AddApp, { source, options }));
637
+ await waitUntilExit();
638
+ }
639
+
640
+ // src/cli/commands/install.tsx
641
+ import { render as render2, Text as Text3, Box as Box3, Static, useApp as useApp2 } from "ink";
642
+ import { useEffect as useEffect3, useState as useState4 } from "react";
643
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
644
+ function InstallApp({ options }) {
645
+ const { exit } = useApp2();
646
+ const [completed, setCompleted] = useState4([]);
647
+ const [current, setCurrent] = useState4();
648
+ const [total, setTotal] = useState4(0);
649
+ const [fatalError, setFatalError] = useState4();
650
+ const [done, setDone] = useState4(false);
651
+ useEffect3(() => {
652
+ if (done) exit();
653
+ }, [done, exit]);
654
+ useEffect3(() => {
655
+ (async () => {
656
+ const manifest = await readManifest();
657
+ if (!manifest) {
658
+ setFatalError('No skills.json found. Run "konstruct init" first.');
659
+ setDone(true);
660
+ return;
661
+ }
662
+ const gitEntries = Object.entries(manifest.skills);
663
+ const userEntries = Object.entries(manifest.userSkills ?? {});
664
+ const count = gitEntries.length + userEntries.length;
665
+ setTotal(count);
666
+ if (count === 0) {
667
+ setDone(true);
668
+ return;
669
+ }
670
+ for (const [name, entry] of gitEntries) {
671
+ setCurrent(name);
672
+ const { source, customPath } = parseSkillEntry(entry);
673
+ const parsed = parseSource(source);
674
+ const result = await installGitSkill(parsed, name, {
675
+ global: options.global,
676
+ ssh: options.ssh,
677
+ customPath
678
+ });
679
+ setCompleted((prev) => [
680
+ ...prev,
681
+ {
682
+ name,
683
+ ok: result.success,
684
+ detail: result.success ? `\u2192 ${formatInstallTargets(result.paths, customPath)}` : result.error ?? "Unknown error"
685
+ }
686
+ ]);
687
+ }
688
+ for (const [name, entry] of userEntries) {
689
+ setCurrent(name);
690
+ const { source, customPath } = parseSkillEntry(entry);
691
+ const parsed = parseSource(source);
692
+ const result = await installUserSkill(parsed, name, {
693
+ global: options.global,
694
+ customPath
695
+ });
696
+ setCompleted((prev) => [
697
+ ...prev,
698
+ {
699
+ name,
700
+ ok: result.success,
701
+ detail: result.success ? `\u2192 ${formatInstallTargets(result.paths, customPath)}` : result.error ?? "Unknown error"
702
+ }
703
+ ]);
704
+ }
705
+ setCurrent(void 0);
706
+ setDone(true);
707
+ })();
708
+ }, []);
709
+ if (fatalError) {
710
+ return /* @__PURE__ */ jsx4(StatusMessage, { variant: "error", children: fatalError });
711
+ }
712
+ if (total === 0 && done) {
713
+ return /* @__PURE__ */ jsxs4(StatusMessage, { variant: "info", children: [
714
+ 'No skills in manifest. Use "konstruct add ',
715
+ "<source>",
716
+ '" to add some.'
717
+ ] });
718
+ }
719
+ const failures = completed.filter((c) => !c.ok).length;
720
+ return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", children: [
721
+ total > 0 && /* @__PURE__ */ jsxs4(StatusMessage, { variant: "info", children: [
722
+ "Installing ",
723
+ total,
724
+ " skill(s)\u2026"
725
+ ] }),
726
+ /* @__PURE__ */ jsx4(Static, { items: completed, children: (item) => /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", children: [
727
+ /* @__PURE__ */ jsx4(StatusMessage, { variant: item.ok ? "success" : "error", children: item.name }),
728
+ item.ok ? /* @__PURE__ */ jsxs4(Text3, { children: [
729
+ " ",
730
+ /* @__PURE__ */ jsx4(Text3, { color: "cyan", children: "\u2139" }),
731
+ " ",
732
+ item.detail
733
+ ] }) : /* @__PURE__ */ jsxs4(Text3, { children: [
734
+ " ",
735
+ /* @__PURE__ */ jsx4(Text3, { color: "red", children: "\u2717" }),
736
+ " ",
737
+ item.detail
738
+ ] })
739
+ ] }, item.name) }),
740
+ current && /* @__PURE__ */ jsx4(Spinner, { label: current }),
741
+ done && /* @__PURE__ */ jsx4(Box3, { marginTop: 1, children: failures === 0 ? /* @__PURE__ */ jsxs4(StatusMessage, { variant: "success", children: [
742
+ "All ",
743
+ total,
744
+ " skill(s) installed."
745
+ ] }) : /* @__PURE__ */ jsxs4(StatusMessage, { variant: "error", children: [
746
+ failures,
747
+ " of ",
748
+ total,
749
+ " skill(s) failed."
750
+ ] }) })
751
+ ] });
752
+ }
753
+ async function installCommand(options) {
754
+ const { waitUntilExit } = render2(/* @__PURE__ */ jsx4(InstallApp, { options }));
755
+ await waitUntilExit();
756
+ }
757
+
758
+ // src/cli/commands/remove.tsx
759
+ import { render as render3, Box as Box4, useApp as useApp3 } from "ink";
760
+ import { useEffect as useEffect4, useState as useState5 } from "react";
761
+ import { rm as rm3 } from "fs/promises";
762
+ import { join as join4 } from "path";
763
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
764
+ function RemoveApp({ names, options }) {
765
+ const { exit } = useApp3();
766
+ const [results, setResults] = useState5([]);
767
+ const [done, setDone] = useState5(false);
768
+ const [fatalError, setFatalError] = useState5();
769
+ useEffect4(() => {
770
+ if (done) exit();
771
+ }, [done, exit]);
772
+ useEffect4(() => {
773
+ (async () => {
774
+ const isGlobal = options.global ?? false;
775
+ const manifestCwd = isGlobal ? KONSTRUCT_DIR : void 0;
776
+ const manifest = await readManifest(manifestCwd);
777
+ if (!isGlobal && !manifest) {
778
+ setFatalError("No skills.json found.");
779
+ setDone(true);
780
+ return;
781
+ }
782
+ const config = await readConfig(process.cwd(), isGlobal);
783
+ const agents = config && config.agents.length > 0 ? config.agents : config?.global?.defaultAgents ?? ["claude"];
784
+ const dirs = getAgentSkillDirs(agents, isGlobal);
785
+ const statuses = [];
786
+ for (const name of names) {
787
+ const inManifest = manifest ? name in manifest.skills || (manifest.userSkills ? name in manifest.userSkills : false) : false;
788
+ let onDisk = false;
789
+ if (isGlobal) {
790
+ for (const dir of dirs) {
791
+ if (await exists(join4(dir, name))) {
792
+ onDisk = true;
793
+ break;
794
+ }
795
+ }
796
+ }
797
+ if (!inManifest && !onDisk) {
798
+ statuses.push({
799
+ name,
800
+ status: "error",
801
+ message: `Skill "${name}" not found${isGlobal ? " in global skill directories" : " in skills.json"}`
802
+ });
803
+ continue;
804
+ }
805
+ if (inManifest) await removeSkillFromManifest(name, manifestCwd);
806
+ for (const dir of dirs) {
807
+ await rm3(join4(dir, name), { recursive: true, force: true }).catch(() => {
808
+ });
809
+ }
810
+ statuses.push({ name, status: "success", message: `Removed "${name}"` });
811
+ }
812
+ setResults(statuses);
813
+ setDone(true);
814
+ })();
815
+ }, []);
816
+ return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
817
+ fatalError && /* @__PURE__ */ jsx5(StatusMessage, { variant: "error", children: fatalError }),
818
+ results.map((r) => /* @__PURE__ */ jsx5(StatusMessage, { variant: r.status === "success" ? "success" : "error", children: r.message }, r.name)),
819
+ done && !fatalError && results.length > 1 && /* @__PURE__ */ jsxs5(StatusMessage, { variant: "info", children: [
820
+ results.filter((r) => r.status === "success").length,
821
+ "/",
822
+ names.length,
823
+ " skill(s) removed"
824
+ ] })
825
+ ] });
826
+ }
827
+ async function removeCommand(names, options = {}) {
828
+ const { waitUntilExit } = render3(/* @__PURE__ */ jsx5(RemoveApp, { names, options }));
829
+ await waitUntilExit();
830
+ }
831
+
832
+ // src/cli/commands/list.tsx
833
+ import { render as render4, Text as Text5, Box as Box5 } from "ink";
834
+ import { useEffect as useEffect5, useState as useState6 } from "react";
835
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
836
+ function ListApp({ options }) {
837
+ const [state, setState] = useState6({
838
+ installed: [],
839
+ user: [],
840
+ untracked: [],
841
+ done: false
842
+ });
843
+ useEffect5(() => {
844
+ (async () => {
845
+ const isGlobal = options.global ?? false;
846
+ const manifest = await readManifest(isGlobal ? KONSTRUCT_DIR : void 0);
847
+ if (!manifest) {
848
+ setState((s) => ({ ...s, error: 'No skills.json found. Run "konstruct init" first.', done: true }));
849
+ return;
850
+ }
851
+ const config = await readConfig(process.cwd(), isGlobal);
852
+ const agents = config && config.agents.length > 0 ? config.agents : config?.global?.defaultAgents ?? ["claude"];
853
+ const dirs = getAgentSkillDirs(agents, isGlobal);
854
+ const installedEntries = Object.entries(manifest.skills);
855
+ const userEntries = Object.entries(manifest.userSkills ?? {});
856
+ const manifestNames = /* @__PURE__ */ new Set([
857
+ ...installedEntries.map(([name]) => name),
858
+ ...userEntries.map(([name]) => name)
859
+ ]);
860
+ const untracked = [];
861
+ for (const dir of dirs) {
862
+ const discovered = await discoverSkills(dir);
863
+ for (const skill of discovered) {
864
+ if (!manifestNames.has(skill.name) && !untracked.some((u) => u.name === skill.name)) {
865
+ untracked.push({ name: skill.name, path: skill.path });
866
+ }
867
+ }
868
+ }
869
+ setState({
870
+ installed: installedEntries,
871
+ user: userEntries,
872
+ untracked,
873
+ done: true
874
+ });
875
+ })();
876
+ }, []);
877
+ if (state.error) {
878
+ return /* @__PURE__ */ jsx6(StatusMessage, { variant: "error", children: state.error });
879
+ }
880
+ if (!state.done) return null;
881
+ const hasAnything = state.installed.length > 0 || state.user.length > 0 || state.untracked.length > 0;
882
+ if (!hasAnything) {
883
+ return /* @__PURE__ */ jsxs6(StatusMessage, { variant: "info", children: [
884
+ 'No skills found. Use "konstruct add ',
885
+ "<source>",
886
+ '" to add some.'
887
+ ] });
888
+ }
889
+ return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", children: [
890
+ state.installed.length > 0 && /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", marginTop: 1, children: [
891
+ /* @__PURE__ */ jsx6(Text5, { bold: true, children: "Installed skills:" }),
892
+ state.installed.map(([name]) => /* @__PURE__ */ jsxs6(Text5, { children: [
893
+ " ",
894
+ /* @__PURE__ */ jsx6(Text5, { color: "cyan", children: name })
895
+ ] }, name))
896
+ ] }),
897
+ state.user.length > 0 && /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", marginTop: 1, children: [
898
+ /* @__PURE__ */ jsx6(Text5, { bold: true, children: "User skills:" }),
899
+ state.user.map(([name]) => /* @__PURE__ */ jsxs6(Text5, { children: [
900
+ " ",
901
+ /* @__PURE__ */ jsx6(Text5, { color: "cyan", children: name })
902
+ ] }, name))
903
+ ] }),
904
+ state.untracked.length > 0 && /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", marginTop: 1, children: [
905
+ /* @__PURE__ */ jsx6(Text5, { bold: true, children: "Untracked skills:" }),
906
+ state.untracked.map(({ name }) => /* @__PURE__ */ jsxs6(Text5, { children: [
907
+ " ",
908
+ /* @__PURE__ */ jsx6(Text5, { color: "cyan", children: name })
909
+ ] }, name))
910
+ ] }),
911
+ /* @__PURE__ */ jsx6(Text5, { children: "" })
912
+ ] });
913
+ }
914
+ async function listCommand(options = {}) {
915
+ const { waitUntilExit } = render4(/* @__PURE__ */ jsx6(ListApp, { options }));
916
+ await waitUntilExit();
917
+ }
918
+
919
+ // src/cli/commands/update.tsx
920
+ import { render as render5, Text as Text7, Box as Box7, Static as Static2, useApp as useApp4 } from "ink";
921
+ import { useEffect as useEffect6, useState as useState7 } from "react";
922
+
923
+ // src/cli/components/DiffView.tsx
924
+ import { Text as Text6, Box as Box6 } from "ink";
925
+ import { jsxs as jsxs7 } from "react/jsx-runtime";
926
+ function DiffView({ diff }) {
927
+ return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", paddingLeft: 4, children: [
928
+ diff.added.map((f, i) => /* @__PURE__ */ jsxs7(Text6, { color: "green", children: [
929
+ "+ ",
930
+ f
931
+ ] }, `a-${i}`)),
932
+ diff.changed.map((f, i) => /* @__PURE__ */ jsxs7(Text6, { color: "yellow", children: [
933
+ "~ ",
934
+ f
935
+ ] }, `c-${i}`)),
936
+ diff.removed.map((f, i) => /* @__PURE__ */ jsxs7(Text6, { color: "red", children: [
937
+ "- ",
938
+ f
939
+ ] }, `r-${i}`))
940
+ ] });
941
+ }
942
+
943
+ // src/cli/commands/update.tsx
944
+ import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
945
+ function UpdateApp({ options }) {
946
+ const { exit } = useApp4();
947
+ const [completed, setCompleted] = useState7([]);
948
+ const [current, setCurrent] = useState7();
949
+ const [total, setTotal] = useState7(0);
950
+ const [fatalError, setFatalError] = useState7();
951
+ const [done, setDone] = useState7(false);
952
+ useEffect6(() => {
953
+ if (done) exit();
954
+ }, [done, exit]);
955
+ useEffect6(() => {
956
+ (async () => {
957
+ const manifest = await readManifest(options.global ? KONSTRUCT_DIR : void 0);
958
+ if (!manifest) {
959
+ setFatalError('No skills.json found. Run "konstruct init" first.');
960
+ setDone(true);
961
+ return;
962
+ }
963
+ const gitEntries = Object.entries(manifest.skills);
964
+ setTotal(gitEntries.length);
965
+ if (gitEntries.length === 0) {
966
+ setDone(true);
967
+ return;
968
+ }
969
+ for (const [name, entry] of gitEntries) {
970
+ setCurrent(`${name} \u2014 checking\u2026`);
971
+ const { source, customPath } = parseSkillEntry(entry);
972
+ const parsed = parseSource(source);
973
+ let diff;
974
+ try {
975
+ diff = await checkSkillForUpdates(parsed, name, {
976
+ global: options.global,
977
+ ssh: options.ssh,
978
+ customPath
979
+ });
980
+ } catch (e) {
981
+ setCompleted((prev) => [
982
+ ...prev,
983
+ { name, status: "failed", detail: e instanceof Error ? e.message : String(e) }
984
+ ]);
985
+ continue;
986
+ }
987
+ if (diff === null) {
988
+ setCurrent(`${name} \u2014 installing\u2026`);
989
+ const result2 = await installGitSkill(parsed, name, {
990
+ global: options.global,
991
+ ssh: options.ssh,
992
+ customPath
993
+ });
994
+ setCompleted((prev) => [
995
+ ...prev,
996
+ result2.success ? { name, status: "installed", detail: `installed \u2192 ${formatInstallTargets(result2.paths, customPath)}` } : { name, status: "failed", detail: result2.error }
997
+ ]);
998
+ continue;
999
+ }
1000
+ if (diff.upToDate) {
1001
+ setCompleted((prev) => [...prev, { name, status: "up-to-date" }]);
1002
+ continue;
1003
+ }
1004
+ setCurrent(`${name} \u2014 updating\u2026`);
1005
+ const result = await installGitSkill(parsed, name, {
1006
+ global: options.global,
1007
+ ssh: options.ssh,
1008
+ customPath
1009
+ });
1010
+ setCompleted((prev) => [
1011
+ ...prev,
1012
+ result.success ? { name, status: "updated", diff } : { name, status: "failed", detail: result.error }
1013
+ ]);
1014
+ }
1015
+ setCurrent(void 0);
1016
+ setDone(true);
1017
+ })();
1018
+ }, []);
1019
+ if (fatalError) {
1020
+ return /* @__PURE__ */ jsx7(StatusMessage, { variant: "error", children: fatalError });
1021
+ }
1022
+ if (total === 0 && done) {
1023
+ return /* @__PURE__ */ jsx7(StatusMessage, { variant: "info", children: "No git skills to update." });
1024
+ }
1025
+ const updated = completed.filter((c) => c.status === "updated" || c.status === "installed").length;
1026
+ const upToDate = completed.filter((c) => c.status === "up-to-date").length;
1027
+ const failures = completed.filter((c) => c.status === "failed").length;
1028
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
1029
+ total > 0 && /* @__PURE__ */ jsxs8(StatusMessage, { variant: "info", children: [
1030
+ "Checking ",
1031
+ total,
1032
+ " git skill(s)\u2026 (userSkills are skipped)"
1033
+ ] }),
1034
+ /* @__PURE__ */ jsx7(Static2, { items: completed, children: (item) => /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
1035
+ /* @__PURE__ */ jsxs8(StatusMessage, { variant: item.status === "failed" ? "error" : "success", children: [
1036
+ item.name,
1037
+ item.status === "up-to-date" && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " up to date" }),
1038
+ item.detail && ` ${item.detail}`
1039
+ ] }),
1040
+ item.diff && /* @__PURE__ */ jsx7(DiffView, { diff: item.diff })
1041
+ ] }, item.name) }),
1042
+ current && /* @__PURE__ */ jsx7(Spinner, { label: current }),
1043
+ done && /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", marginTop: 1, children: [
1044
+ failures > 0 && /* @__PURE__ */ jsxs8(StatusMessage, { variant: "error", children: [
1045
+ failures,
1046
+ " skill(s) failed."
1047
+ ] }),
1048
+ updated > 0 && /* @__PURE__ */ jsxs8(StatusMessage, { variant: "success", children: [
1049
+ updated,
1050
+ " skill(s) updated."
1051
+ ] }),
1052
+ upToDate > 0 && /* @__PURE__ */ jsxs8(StatusMessage, { variant: "info", children: [
1053
+ upToDate,
1054
+ " skill(s) already up to date."
1055
+ ] })
1056
+ ] })
1057
+ ] });
1058
+ }
1059
+ async function updateCommand(options = {}) {
1060
+ const { waitUntilExit } = render5(/* @__PURE__ */ jsx7(UpdateApp, { options }));
1061
+ await waitUntilExit();
1062
+ }
1063
+
1064
+ // src/cli/commands/defaults.tsx
1065
+ import { render as render6, Box as Box8, useApp as useApp5 } from "ink";
1066
+ import { useState as useState8, useCallback as useCallback2, useEffect as useEffect7 } from "react";
1067
+ import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
1068
+ function DefaultsApp() {
1069
+ const { exit } = useApp5();
1070
+ const [phase, setPhase] = useState8("scope");
1071
+ const [isGlobal, setIsGlobal] = useState8(false);
1072
+ const [hasLocalManifest, setHasLocalManifest] = useState8(false);
1073
+ const [currentAgents, setCurrentAgents] = useState8([]);
1074
+ const [config, setConfig] = useState8(null);
1075
+ const [orderedSlugs, setOrderedSlugs] = useState8([]);
1076
+ const [labels, setLabels] = useState8([]);
1077
+ const [result, setResult] = useState8();
1078
+ useEffect7(() => {
1079
+ if (phase === "done") exit();
1080
+ }, [phase, exit]);
1081
+ const [initialized, setInitialized] = useState8(false);
1082
+ if (!initialized) {
1083
+ setInitialized(true);
1084
+ (async () => {
1085
+ const localManifest = await readManifest(process.cwd());
1086
+ if (localManifest) {
1087
+ setHasLocalManifest(true);
1088
+ } else {
1089
+ setIsGlobal(true);
1090
+ await loadConfig(true);
1091
+ }
1092
+ })();
1093
+ }
1094
+ async function loadConfig(global) {
1095
+ const cwd = process.cwd();
1096
+ const cfg = await readConfig(cwd, global);
1097
+ setConfig(cfg);
1098
+ const agents = global ? cfg?.global?.defaultAgents ?? cfg?.agents ?? [] : cfg?.agents ?? [];
1099
+ setCurrentAgents(agents);
1100
+ const { ordered, labels: lbls } = getAgentLabels();
1101
+ setOrderedSlugs(ordered);
1102
+ setLabels(lbls);
1103
+ setPhase("display");
1104
+ }
1105
+ const onScopeSelect = useCallback2(async (index) => {
1106
+ const global = index === 1;
1107
+ setIsGlobal(global);
1108
+ await loadConfig(global);
1109
+ }, []);
1110
+ const onAgentsConfirm = useCallback2(async (indices) => {
1111
+ const agents = indices.length === 0 ? ["claude"] : indices.map((i) => orderedSlugs[i]);
1112
+ const scope = isGlobal ? "global" : "project";
1113
+ const cwd = process.cwd();
1114
+ if (config) {
1115
+ if (isGlobal) {
1116
+ if (!config.global) config.global = { defaultAgents: agents };
1117
+ else config.global.defaultAgents = agents;
1118
+ } else {
1119
+ config.agents = agents;
1120
+ }
1121
+ await writeConfig(config, cwd, isGlobal);
1122
+ } else {
1123
+ const newConfig = {
1124
+ version: 1,
1125
+ agents: isGlobal ? [] : agents,
1126
+ ...isGlobal && { global: { defaultAgents: agents } }
1127
+ };
1128
+ await writeConfig(newConfig, cwd, isGlobal);
1129
+ }
1130
+ setResult({ agents, scope });
1131
+ setPhase("done");
1132
+ }, [config, isGlobal, orderedSlugs]);
1133
+ return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", children: [
1134
+ phase === "scope" && hasLocalManifest && /* @__PURE__ */ jsx8(
1135
+ Select,
1136
+ {
1137
+ prompt: "Update project defaults or global defaults?",
1138
+ items: ["Project", "Global"],
1139
+ onSelect: onScopeSelect
1140
+ }
1141
+ ),
1142
+ phase === "scope" && !hasLocalManifest && /* @__PURE__ */ jsx8(StatusMessage, { variant: "info", children: "No skills.json in current directory \u2014 updating global defaults." }),
1143
+ phase === "display" && /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: currentAgents.length > 0 ? /* @__PURE__ */ jsxs9(StatusMessage, { variant: "info", children: [
1144
+ "Current default agents: ",
1145
+ currentAgents.join(", ")
1146
+ ] }) : /* @__PURE__ */ jsx8(StatusMessage, { variant: "warn", children: "No default agents configured." }) }),
1147
+ (phase === "display" || phase === "select-agents") && labels.length > 0 && /* @__PURE__ */ jsx8(
1148
+ MultiSelect,
1149
+ {
1150
+ prompt: "Select your default agent(s)",
1151
+ items: labels,
1152
+ onConfirm: onAgentsConfirm
1153
+ }
1154
+ ),
1155
+ phase === "done" && result && /* @__PURE__ */ jsxs9(StatusMessage, { variant: "success", children: [
1156
+ "Updated ",
1157
+ result.scope,
1158
+ " default agents: ",
1159
+ result.agents.join(", ")
1160
+ ] })
1161
+ ] });
1162
+ }
1163
+ async function defaultsCommand() {
1164
+ const { waitUntilExit } = render6(/* @__PURE__ */ jsx8(DefaultsApp, {}));
1165
+ await waitUntilExit();
1166
+ }
1167
+
1168
+ // src/cli/index.ts
1169
+ var __filename = fileURLToPath(import.meta.url);
1170
+ var __dirname = dirname2(__filename);
1171
+ function getVersion() {
1172
+ const candidates = [
1173
+ resolve2(__dirname, "..", "..", "package.json"),
1174
+ resolve2(__dirname, "..", "package.json")
1175
+ ];
1176
+ for (const pkgPath of candidates) {
1177
+ try {
1178
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1179
+ if (pkg.version) return pkg.version;
1180
+ } catch {
1181
+ }
1182
+ }
1183
+ return "0.0.0";
1184
+ }
1185
+ var program = new Command();
1186
+ program.name("konstruct").description("Package manager for AI agent skills").version(getVersion());
1187
+ program.command("init").description("Initialize skills.json and konstruct.config.json").option("-g, --global", "Initialize global configuration (~/.konstruct/) instead of project-local").action(initCommand);
1188
+ program.command("install").description("Install all skills from skills.json").option("-g, --global", "Install globally (~/) instead of project-local").option("-s, --ssh", "Use SSH for cloning (default: HTTPS with auto-retry on auth failure)").action(installCommand);
1189
+ program.command("add <source>").description("Add a skill from a git or local source").option("-g, --global", "Install globally").option("--user", "Add as a userSkill (local, never auto-updated)").option("--path <path>", "Custom installation path").option("-s, --ssh", "Use SSH for cloning (default: HTTPS with auto-retry on auth failure)").action(addCommand);
1190
+ program.command("remove <names...>").description("Remove one or more skills by name").option("-g, --global", "Remove from global (~/) directories instead of project-local").action(removeCommand);
1191
+ program.command("list").description("List all skills in the current manifest").option("-g, --global", "List skills from the global manifest instead of project-local").action(listCommand);
1192
+ program.command("update").description("Re-install git skills at their manifest refs (skips userSkills)").option("-g, --global", "Update in global (~/) directories instead of project-local").option("-s, --ssh", "Use SSH for cloning (default: HTTPS with auto-retry on auth failure)").action(updateCommand);
1193
+ program.command("defaults").description("View and update default agent preferences").action(defaultsCommand);
1194
+ program.parseAsync().catch((err) => {
1195
+ console.error(pc.red("\u2717"), err instanceof Error ? err.message : String(err));
1196
+ process.exit(1);
1197
+ });