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.
@@ -0,0 +1,696 @@
1
+ // src/cli/commands/init.tsx
2
+ import { render, Box as Box3, useApp } from "ink";
3
+ import { useState as useState2, useCallback, useEffect } from "react";
4
+
5
+ // src/core/manifest.ts
6
+ import { readFile as readFile2, writeFile } from "fs/promises";
7
+ import { join as join2, basename } from "path";
8
+
9
+ // src/utils/fs.ts
10
+ import { access, constants, readdir, readFile } from "fs/promises";
11
+ import { join, relative } from "path";
12
+ import { createHash } from "crypto";
13
+ async function exists(filePath) {
14
+ try {
15
+ await access(filePath, constants.F_OK);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ async function hashDirectory(dirPath) {
22
+ const result = /* @__PURE__ */ new Map();
23
+ await walkAndHash(dirPath, dirPath, result);
24
+ return result;
25
+ }
26
+ async function walkAndHash(root, current, out) {
27
+ let entries;
28
+ try {
29
+ entries = await readdir(current, { withFileTypes: true });
30
+ } catch {
31
+ return;
32
+ }
33
+ for (const entry of entries) {
34
+ const full = join(current, entry.name);
35
+ if (entry.isDirectory()) {
36
+ await walkAndHash(root, full, out);
37
+ } else {
38
+ const content = await readFile(full);
39
+ const hash = createHash("sha256").update(content).digest("hex");
40
+ out.set(relative(root, full), hash);
41
+ }
42
+ }
43
+ }
44
+ function diffHashes(local, remote) {
45
+ const added = [];
46
+ const removed = [];
47
+ const changed = [];
48
+ for (const [path, hash] of remote) {
49
+ if (!local.has(path)) {
50
+ added.push(path);
51
+ } else if (local.get(path) !== hash) {
52
+ changed.push(path);
53
+ }
54
+ }
55
+ for (const path of local.keys()) {
56
+ if (!remote.has(path)) {
57
+ removed.push(path);
58
+ }
59
+ }
60
+ return { added, removed, changed };
61
+ }
62
+
63
+ // src/core/source-parser.ts
64
+ function parseSource(source) {
65
+ if (source.startsWith("github:")) {
66
+ return parseGitHub(source.slice("github:".length));
67
+ }
68
+ if (source.startsWith("gitlab:")) {
69
+ return parseGenericGit("gitlab", source.slice("gitlab:".length));
70
+ }
71
+ if (source.startsWith("git:")) {
72
+ return parseGenericGit("git", source.slice("git:".length));
73
+ }
74
+ if (source.startsWith("file:")) {
75
+ return { type: "file", url: source.slice("file:".length) };
76
+ }
77
+ if (!source.includes("://") && source.includes("/")) {
78
+ return parseGitHub(source);
79
+ }
80
+ throw new Error(
81
+ `Unknown source format: "${source}".
82
+
83
+ Supported formats:
84
+ github:owner/repo#ref GitHub repo (or owner/repo shorthand)
85
+ gitlab:owner/repo#ref GitLab repo
86
+ git:https://host/repo.git Arbitrary git URL
87
+ file:./path/to/skill Local directory
88
+ `
89
+ );
90
+ }
91
+ function parseGitHub(input) {
92
+ const { base, ref } = splitRef(input);
93
+ const segments = base.split("/");
94
+ if (segments.length < 2) {
95
+ throw new Error(`Invalid github source: "github:${input}" \u2014 expected at least owner/repo`);
96
+ }
97
+ const owner = segments[0];
98
+ const repo = segments[1].replace(/\.git$/, "");
99
+ const subpath = segments.length > 2 ? segments.slice(2).join("/") : void 0;
100
+ return {
101
+ type: "github",
102
+ url: `https://github.com/${owner}/${repo}.git`,
103
+ ref,
104
+ subpath
105
+ };
106
+ }
107
+ function parseGenericGit(type, input) {
108
+ const { base, ref } = splitRef(input);
109
+ if (type === "git") {
110
+ return { type: "git", url: base.endsWith(".git") ? base : base + ".git", ref };
111
+ }
112
+ const segments = base.split("/");
113
+ if (segments.length < 2) {
114
+ throw new Error(`Invalid gitlab source: "gitlab:${input}" \u2014 expected at least owner/repo`);
115
+ }
116
+ const owner = segments[0];
117
+ const repo = segments[1].replace(/\.git$/, "");
118
+ const subpath = segments.length > 2 ? segments.slice(2).join("/") : void 0;
119
+ return {
120
+ type: "gitlab",
121
+ url: `https://gitlab.com/${owner}/${repo}.git`,
122
+ ref,
123
+ subpath
124
+ };
125
+ }
126
+ function splitRef(input) {
127
+ const hashIndex = input.lastIndexOf("#");
128
+ if (hashIndex === -1) return { base: input, ref: void 0 };
129
+ return {
130
+ base: input.slice(0, hashIndex),
131
+ ref: input.slice(hashIndex + 1) || void 0
132
+ };
133
+ }
134
+
135
+ // src/core/manifest.ts
136
+ var MANIFEST_FILENAME = "skills.json";
137
+ function parseSkillEntry(entry) {
138
+ return { source: entry.source, customPath: entry.path };
139
+ }
140
+ async function readManifest(cwd = process.cwd()) {
141
+ const manifestPath = join2(cwd, MANIFEST_FILENAME);
142
+ if (!await exists(manifestPath)) return null;
143
+ const raw = await readFile2(manifestPath, "utf-8");
144
+ let manifest;
145
+ try {
146
+ manifest = JSON.parse(raw);
147
+ } catch (e) {
148
+ throw new Error(`Invalid JSON in ${manifestPath}: ${e instanceof Error ? e.message : e}`);
149
+ }
150
+ validateManifest(manifest);
151
+ return manifest;
152
+ }
153
+ async function writeManifest(manifest, cwd = process.cwd()) {
154
+ const manifestPath = join2(cwd, MANIFEST_FILENAME);
155
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
156
+ }
157
+ async function addSkillToManifest(skillName, source, options = {}) {
158
+ const cwd = options.cwd ?? process.cwd();
159
+ let manifest = await readManifest(cwd);
160
+ if (!manifest) {
161
+ manifest = {
162
+ name: basename(cwd),
163
+ version: "1.0.0",
164
+ skills: {}
165
+ };
166
+ }
167
+ const entry = {
168
+ source,
169
+ ...options.customPath && { path: options.customPath }
170
+ };
171
+ if (options.isUserSkill) {
172
+ if (!manifest.userSkills) manifest.userSkills = {};
173
+ manifest.userSkills[skillName] = entry;
174
+ } else {
175
+ manifest.skills[skillName] = entry;
176
+ }
177
+ await writeManifest(manifest, cwd);
178
+ }
179
+ async function removeSkillFromManifest(skillName, cwd = process.cwd()) {
180
+ const manifest = await readManifest(cwd);
181
+ if (!manifest) return false;
182
+ let removed = false;
183
+ if (manifest.skills[skillName]) {
184
+ delete manifest.skills[skillName];
185
+ removed = true;
186
+ }
187
+ if (manifest.userSkills?.[skillName]) {
188
+ delete manifest.userSkills[skillName];
189
+ removed = true;
190
+ }
191
+ if (removed) await writeManifest(manifest, cwd);
192
+ return removed;
193
+ }
194
+ function validateManifest(manifest) {
195
+ if (!manifest || typeof manifest !== "object") {
196
+ throw new Error("Invalid skills.json: must be a JSON object");
197
+ }
198
+ const m = manifest;
199
+ if (typeof m.name !== "string") {
200
+ throw new Error('Invalid skills.json: "name" must be a string');
201
+ }
202
+ if (typeof m.version !== "string") {
203
+ throw new Error('Invalid skills.json: "version" must be a string');
204
+ }
205
+ if (!m.skills || typeof m.skills !== "object" || Array.isArray(m.skills)) {
206
+ throw new Error('Invalid skills.json: "skills" must be an object');
207
+ }
208
+ for (const [name, entry] of Object.entries(m.skills)) {
209
+ validateSkillEntry(entry, name, false);
210
+ }
211
+ if (m.userSkills !== void 0) {
212
+ if (typeof m.userSkills !== "object" || Array.isArray(m.userSkills)) {
213
+ throw new Error('Invalid skills.json: "userSkills" must be an object');
214
+ }
215
+ for (const [name, entry] of Object.entries(m.userSkills)) {
216
+ validateSkillEntry(entry, name, true);
217
+ }
218
+ }
219
+ }
220
+ function validateSkillEntry(entry, name, isUserSkill) {
221
+ if (!entry || typeof entry !== "object") {
222
+ throw new Error(`Invalid skills.json: ${isUserSkill ? "userSkill" : "skill"} "${name}" must be an object`);
223
+ }
224
+ const e = entry;
225
+ if (typeof e.source !== "string") {
226
+ throw new Error(`Invalid skills.json: ${isUserSkill ? "userSkill" : "skill"} "${name}" must have a "source" string`);
227
+ }
228
+ if (e.path !== void 0 && typeof e.path !== "string") {
229
+ throw new Error(`Invalid skills.json: ${isUserSkill ? "userSkill" : "skill"} "${name}" path must be a string if provided`);
230
+ }
231
+ const parsed = parseSource(e.source);
232
+ if (isUserSkill && parsed.type !== "file") {
233
+ throw new Error(`Invalid skills.json: userSkill "${name}" must use the file: prefix`);
234
+ }
235
+ }
236
+
237
+ // src/core/config.ts
238
+ import { readFile as readFile3, writeFile as writeFile2, mkdir } from "fs/promises";
239
+ import { join as join3 } from "path";
240
+ import { homedir } from "os";
241
+ import { existsSync } from "fs";
242
+ var CONFIG_FILENAME = "konstruct.config.json";
243
+ var KONSTRUCT_DIR = join3(homedir(), ".konstruct");
244
+ var home = homedir();
245
+ var configHome = process.env.XDG_CONFIG_HOME?.trim() || join3(home, ".config");
246
+ var claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join3(home, ".claude");
247
+ var codexHome = process.env.CODEX_HOME?.trim() || join3(home, ".codex");
248
+ var AGENT_REGISTRY = [
249
+ {
250
+ slug: "claude",
251
+ skillsDir: ".claude/skills",
252
+ globalSkillsDir: join3(claudeHome, "skills"),
253
+ detectInstalled: () => existsSync(claudeHome)
254
+ },
255
+ {
256
+ slug: "cursor",
257
+ skillsDir: ".cursor/skills",
258
+ globalSkillsDir: join3(home, ".cursor", "skills"),
259
+ detectInstalled: () => existsSync(join3(home, ".cursor"))
260
+ },
261
+ {
262
+ slug: "windsurf",
263
+ skillsDir: ".windsurf/skills",
264
+ globalSkillsDir: join3(home, ".codeium", "windsurf", "skills"),
265
+ detectInstalled: () => existsSync(join3(home, ".codeium", "windsurf"))
266
+ },
267
+ {
268
+ slug: "continue",
269
+ skillsDir: ".continue/skills",
270
+ globalSkillsDir: join3(home, ".continue", "skills"),
271
+ detectInstalled: () => existsSync(join3(home, ".continue"))
272
+ },
273
+ {
274
+ slug: "copilot",
275
+ skillsDir: ".copilot/skills",
276
+ globalSkillsDir: join3(home, ".copilot", "skills"),
277
+ detectInstalled: () => existsSync(join3(home, ".copilot"))
278
+ },
279
+ {
280
+ slug: "gemini",
281
+ skillsDir: ".gemini/skills",
282
+ globalSkillsDir: join3(home, ".gemini", "skills"),
283
+ detectInstalled: () => existsSync(join3(home, ".gemini"))
284
+ },
285
+ {
286
+ slug: "augment",
287
+ skillsDir: ".augment/rules",
288
+ globalSkillsDir: join3(home, ".augment", "rules"),
289
+ detectInstalled: () => existsSync(join3(home, ".augment"))
290
+ },
291
+ {
292
+ slug: "cline",
293
+ skillsDir: ".cline/skills",
294
+ globalSkillsDir: join3(home, ".cline", "skills"),
295
+ detectInstalled: () => existsSync(join3(home, ".cline"))
296
+ },
297
+ {
298
+ slug: "goose",
299
+ skillsDir: ".goose/skills",
300
+ globalSkillsDir: join3(configHome, "goose", "skills"),
301
+ detectInstalled: () => existsSync(join3(configHome, "goose"))
302
+ },
303
+ {
304
+ slug: "junie",
305
+ skillsDir: ".junie/skills",
306
+ globalSkillsDir: join3(home, ".junie", "skills"),
307
+ detectInstalled: () => existsSync(join3(home, ".junie"))
308
+ },
309
+ {
310
+ slug: "kiro",
311
+ skillsDir: ".kiro/skills",
312
+ globalSkillsDir: join3(home, ".kiro", "skills"),
313
+ detectInstalled: () => existsSync(join3(home, ".kiro"))
314
+ },
315
+ {
316
+ slug: "opencode",
317
+ skillsDir: ".opencode/skills",
318
+ globalSkillsDir: join3(configHome, "opencode", "skills"),
319
+ detectInstalled: () => existsSync(join3(configHome, "opencode"))
320
+ },
321
+ {
322
+ slug: "openhands",
323
+ skillsDir: ".openhands/skills",
324
+ globalSkillsDir: join3(home, ".openhands", "skills"),
325
+ detectInstalled: () => existsSync(join3(home, ".openhands"))
326
+ },
327
+ {
328
+ slug: "roo",
329
+ skillsDir: ".roo/skills",
330
+ globalSkillsDir: join3(home, ".roo", "skills"),
331
+ detectInstalled: () => existsSync(join3(home, ".roo"))
332
+ },
333
+ {
334
+ slug: "trae",
335
+ skillsDir: ".trae/skills",
336
+ globalSkillsDir: join3(home, ".trae", "skills"),
337
+ detectInstalled: () => existsSync(join3(home, ".trae"))
338
+ },
339
+ {
340
+ slug: "kode",
341
+ skillsDir: ".kode/skills",
342
+ globalSkillsDir: join3(home, ".kode", "skills"),
343
+ detectInstalled: () => existsSync(join3(home, ".kode"))
344
+ },
345
+ {
346
+ slug: "qwen-code",
347
+ skillsDir: ".qwen/skills",
348
+ globalSkillsDir: join3(home, ".qwen", "skills"),
349
+ detectInstalled: () => existsSync(join3(home, ".qwen"))
350
+ },
351
+ {
352
+ slug: "codex",
353
+ skillsDir: ".codex/skills",
354
+ globalSkillsDir: join3(codexHome, "skills"),
355
+ detectInstalled: () => existsSync(codexHome) || existsSync("/etc/codex")
356
+ },
357
+ {
358
+ slug: "amp",
359
+ skillsDir: ".agents/skills",
360
+ globalSkillsDir: join3(configHome, "agents", "skills"),
361
+ detectInstalled: () => existsSync(join3(configHome, "agents"))
362
+ },
363
+ {
364
+ slug: "kilo",
365
+ skillsDir: ".kilocode/skills",
366
+ globalSkillsDir: join3(home, ".kilocode", "skills"),
367
+ detectInstalled: () => existsSync(join3(home, ".kilocode"))
368
+ },
369
+ {
370
+ slug: "pochi",
371
+ skillsDir: ".pochi/skills",
372
+ globalSkillsDir: join3(home, ".pochi", "skills"),
373
+ detectInstalled: () => existsSync(join3(home, ".pochi"))
374
+ },
375
+ {
376
+ slug: "neovate",
377
+ skillsDir: ".neovate/skills",
378
+ globalSkillsDir: join3(home, ".neovate", "skills"),
379
+ detectInstalled: () => existsSync(join3(home, ".neovate"))
380
+ },
381
+ {
382
+ slug: "mux",
383
+ skillsDir: ".mux/skills",
384
+ globalSkillsDir: join3(home, ".mux", "skills"),
385
+ detectInstalled: () => existsSync(join3(home, ".mux"))
386
+ },
387
+ {
388
+ slug: "zencoder",
389
+ skillsDir: ".zencoder/skills",
390
+ globalSkillsDir: join3(home, ".zencoder", "skills"),
391
+ detectInstalled: () => existsSync(join3(home, ".zencoder"))
392
+ },
393
+ {
394
+ slug: "adal",
395
+ skillsDir: ".adal/skills",
396
+ globalSkillsDir: join3(home, ".adal", "skills"),
397
+ detectInstalled: () => existsSync(join3(home, ".adal"))
398
+ }
399
+ ];
400
+ var AGENT_MAP = new Map(AGENT_REGISTRY.map((e) => [e.slug, e]));
401
+ var KNOWN_AGENTS = AGENT_REGISTRY.map((e) => e.slug);
402
+ async function readConfig(cwd = process.cwd(), global = false) {
403
+ const configPath = global ? join3(KONSTRUCT_DIR, CONFIG_FILENAME) : join3(cwd, CONFIG_FILENAME);
404
+ if (!await exists(configPath)) return null;
405
+ const raw = await readFile3(configPath, "utf-8");
406
+ let config;
407
+ try {
408
+ config = JSON.parse(raw);
409
+ } catch (e) {
410
+ throw new Error(`Invalid JSON in ${configPath}: ${e instanceof Error ? e.message : e}`);
411
+ }
412
+ validateConfig(config);
413
+ return config;
414
+ }
415
+ async function writeConfig(config, cwd = process.cwd(), global = false) {
416
+ const dir = global ? KONSTRUCT_DIR : cwd;
417
+ if (global) await mkdir(dir, { recursive: true });
418
+ await writeFile2(join3(dir, CONFIG_FILENAME), JSON.stringify(config, null, 2) + "\n", "utf-8");
419
+ }
420
+ function getAgentSkillDirs(agents, global = false, cwd = process.cwd(), customPath) {
421
+ if (customPath) return [customPath];
422
+ const dirs = [];
423
+ for (const slug of agents) {
424
+ const entry = AGENT_MAP.get(slug);
425
+ if (global) {
426
+ if (entry?.globalSkillsDir) dirs.push(entry.globalSkillsDir);
427
+ } else {
428
+ dirs.push(join3(cwd, entry?.skillsDir ?? `.${slug}/skills`));
429
+ }
430
+ }
431
+ return dirs;
432
+ }
433
+ function validateConfig(config) {
434
+ if (!config || typeof config !== "object") {
435
+ throw new Error("Invalid konstruct.config.json: must be a JSON object");
436
+ }
437
+ const c = config;
438
+ if (typeof c.version !== "number") {
439
+ throw new Error('Invalid konstruct.config.json: "version" must be a number');
440
+ }
441
+ if (!Array.isArray(c.agents)) {
442
+ throw new Error('Invalid konstruct.config.json: "agents" must be an array');
443
+ }
444
+ }
445
+
446
+ // src/cli/commands/init.tsx
447
+ import { basename as basename2 } from "path";
448
+
449
+ // src/cli/components/Banner.tsx
450
+ import { Text, Box } from "ink";
451
+ import { jsx, jsxs } from "react/jsx-runtime";
452
+ function Banner() {
453
+ const art = `
454
+ _ __ _ _
455
+ | |/ /___ _ __ ___ | |_ _ __ _ _ ___| |_
456
+ | ' // _ \\| '_ \\/ __| | __|| '__|| | | |/ __| __|
457
+ | . \\ (_) | | | \\__ \\ | |_ | | | |_| | (__| |_
458
+ |_|\\_\\___/|_| |_|___/ \\__||_| \\__,_|\\___|\\__|`.trimStart();
459
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
460
+ /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: art }),
461
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " Package manager for AI agent skills" })
462
+ ] });
463
+ }
464
+
465
+ // src/cli/components/StatusMessage.tsx
466
+ import { Text as Text2 } from "ink";
467
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
468
+ var VARIANTS = {
469
+ success: { icon: "\u2713", color: "green" },
470
+ error: { icon: "\u2717", color: "red" },
471
+ info: { icon: "\u2139", color: "cyan" },
472
+ warn: { icon: "!", color: "yellow" }
473
+ };
474
+ function StatusMessage({ variant, children }) {
475
+ const { icon, color } = VARIANTS[variant];
476
+ return /* @__PURE__ */ jsxs2(Text2, { children: [
477
+ /* @__PURE__ */ jsx2(Text2, { color, children: icon }),
478
+ " ",
479
+ children
480
+ ] });
481
+ }
482
+
483
+ // src/cli/components/MultiSelect.tsx
484
+ import { Text as Text3, Box as Box2, useInput } from "ink";
485
+ import { useState } from "react";
486
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
487
+ function MultiSelect({ prompt, items, onConfirm }) {
488
+ const [cursor, setCursor] = useState(0);
489
+ const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
490
+ useInput((input, key) => {
491
+ if (key.upArrow) {
492
+ setCursor((prev) => (prev - 1 + items.length) % items.length);
493
+ } else if (key.downArrow) {
494
+ setCursor((prev) => (prev + 1) % items.length);
495
+ } else if (input === " ") {
496
+ setSelected((prev) => {
497
+ const next = new Set(prev);
498
+ if (next.has(cursor)) next.delete(cursor);
499
+ else next.add(cursor);
500
+ return next;
501
+ });
502
+ } else if (key.return) {
503
+ onConfirm([...selected].sort((a, b) => a - b));
504
+ }
505
+ });
506
+ if (!process.stdin.isTTY) {
507
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", children: [
508
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: prompt }),
509
+ items.map((item, i) => /* @__PURE__ */ jsxs3(Text3, { children: [
510
+ " ",
511
+ i + 1,
512
+ ". ",
513
+ item
514
+ ] }, i))
515
+ ] });
516
+ }
517
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", children: [
518
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: prompt }),
519
+ items.map((item, i) => {
520
+ const arrow = i === cursor ? /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u203A" }) : /* @__PURE__ */ jsx3(Text3, { children: " " });
521
+ const box = selected.has(i) ? /* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u2611" }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2610" });
522
+ const label = i === cursor ? /* @__PURE__ */ jsx3(Text3, { bold: true, children: item }) : /* @__PURE__ */ jsx3(Text3, { children: item });
523
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
524
+ " ",
525
+ arrow,
526
+ " ",
527
+ box,
528
+ " ",
529
+ label
530
+ ] }, i);
531
+ }),
532
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " \u2191\u2193 move space select enter confirm" })
533
+ ] });
534
+ }
535
+
536
+ // src/cli/prompts.ts
537
+ import { existsSync as existsSync2 } from "fs";
538
+ import { homedir as homedir2 } from "os";
539
+ import { join as join4 } from "path";
540
+ var home2 = homedir2();
541
+ var configHome2 = process.env.XDG_CONFIG_HOME?.trim() || join4(home2, ".config");
542
+ var claudeHome2 = process.env.CLAUDE_CONFIG_DIR?.trim() || join4(home2, ".claude");
543
+ var codexHome2 = process.env.CODEX_HOME?.trim() || join4(home2, ".codex");
544
+ var AGENT_DETECTORS = {
545
+ claude: () => existsSync2(claudeHome2),
546
+ cursor: () => existsSync2(join4(home2, ".cursor")),
547
+ windsurf: () => existsSync2(join4(home2, ".codeium", "windsurf")),
548
+ continue: () => existsSync2(join4(home2, ".continue")),
549
+ copilot: () => existsSync2(join4(home2, ".copilot")),
550
+ gemini: () => existsSync2(join4(home2, ".gemini")),
551
+ augment: () => existsSync2(join4(home2, ".augment")),
552
+ cline: () => existsSync2(join4(home2, ".cline")),
553
+ goose: () => existsSync2(join4(configHome2, "goose")),
554
+ junie: () => existsSync2(join4(home2, ".junie")),
555
+ kiro: () => existsSync2(join4(home2, ".kiro")),
556
+ opencode: () => existsSync2(join4(configHome2, "opencode")),
557
+ openhands: () => existsSync2(join4(home2, ".openhands")),
558
+ roo: () => existsSync2(join4(home2, ".roo")),
559
+ trae: () => existsSync2(join4(home2, ".trae")),
560
+ kode: () => existsSync2(join4(home2, ".kode")),
561
+ "qwen-code": () => existsSync2(join4(home2, ".qwen")),
562
+ codex: () => existsSync2(codexHome2) || existsSync2("/etc/codex"),
563
+ amp: () => existsSync2(join4(configHome2, "agents")),
564
+ kilo: () => existsSync2(join4(home2, ".kilocode")),
565
+ pochi: () => existsSync2(join4(home2, ".pochi")),
566
+ neovate: () => existsSync2(join4(home2, ".neovate")),
567
+ mux: () => existsSync2(join4(home2, ".mux")),
568
+ zencoder: () => existsSync2(join4(home2, ".zencoder")),
569
+ adal: () => existsSync2(join4(home2, ".adal"))
570
+ };
571
+ function detectInstalledAgents() {
572
+ const installed = /* @__PURE__ */ new Set();
573
+ for (const slug of KNOWN_AGENTS) {
574
+ const detect = AGENT_DETECTORS[slug];
575
+ if (detect?.()) installed.add(slug);
576
+ }
577
+ return installed;
578
+ }
579
+ function getAgentLabels() {
580
+ const installed = detectInstalledAgents();
581
+ const detected = KNOWN_AGENTS.filter((a) => installed.has(a));
582
+ const rest = KNOWN_AGENTS.filter((a) => !installed.has(a));
583
+ const ordered = [...detected, ...rest];
584
+ const labels = ordered.map((a) => installed.has(a) ? `${a} (detected)` : a);
585
+ return { ordered, labels };
586
+ }
587
+
588
+ // src/cli/commands/init.tsx
589
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
590
+ function InitApp({ options }) {
591
+ const { exit } = useApp();
592
+ const [phase, setPhase] = useState2("init");
593
+ const [messages, setMessages] = useState2([]);
594
+ const [showAgentPrompt, setShowAgentPrompt] = useState2(false);
595
+ const [agentLabels, setAgentLabels] = useState2([]);
596
+ const [agentSlugs, setAgentSlugs] = useState2([]);
597
+ const [cwd] = useState2(() => options.global ? KONSTRUCT_DIR : process.cwd());
598
+ const [scope] = useState2(() => options.global ? "global" : "project");
599
+ useEffect(() => {
600
+ if (phase === "done") exit();
601
+ }, [phase, exit]);
602
+ const [initialized, setInitialized] = useState2(false);
603
+ if (!initialized) {
604
+ setInitialized(true);
605
+ (async () => {
606
+ const msgs = [];
607
+ const existingManifest = await readManifest(cwd);
608
+ if (existingManifest) {
609
+ msgs.push({ variant: "warn", text: `${scope} skills.json already exists \u2014 skipping.` });
610
+ } else {
611
+ await writeManifest(
612
+ { name: basename2(cwd), version: "1.0.0", skills: {} },
613
+ cwd
614
+ );
615
+ msgs.push({ variant: "success", text: `Created ${scope} skills.json` });
616
+ }
617
+ const existingConfig = await readConfig(cwd);
618
+ if (existingConfig) {
619
+ msgs.push({ variant: "warn", text: `${scope} konstruct.config.json already exists \u2014 skipping.` });
620
+ msgs.push({
621
+ variant: "info",
622
+ text: options.global ? 'Global configuration created. Use "konstruct add -g <source>" to add global skills.' : 'Run "konstruct add <source>" to add your first skill.'
623
+ });
624
+ setMessages(msgs);
625
+ setPhase("done");
626
+ } else {
627
+ msgs.push({
628
+ variant: "info",
629
+ text: options.global ? "Which AI agents do you want as global defaults?" : "Which AI agents do you use in this project?"
630
+ });
631
+ setMessages(msgs);
632
+ const { ordered, labels } = getAgentLabels();
633
+ setAgentSlugs(ordered);
634
+ setAgentLabels(labels);
635
+ setShowAgentPrompt(true);
636
+ setPhase("select-agents");
637
+ }
638
+ })();
639
+ }
640
+ const onAgentsConfirm = useCallback(async (indices) => {
641
+ const agents = indices.length === 0 ? ["claude"] : indices.map((i) => agentSlugs[i]);
642
+ const config = {
643
+ version: 1,
644
+ agents,
645
+ ...options.global && { global: { defaultAgents: agents } }
646
+ };
647
+ await writeConfig(config, cwd);
648
+ setMessages((prev) => [
649
+ ...prev,
650
+ { variant: "success", text: `Created ${scope} konstruct.config.json (agents: ${agents.join(", ")})` },
651
+ {
652
+ variant: "info",
653
+ text: options.global ? 'Global configuration created. Use "konstruct add -g <source>" to add global skills.' : 'Run "konstruct add <source>" to add your first skill.'
654
+ }
655
+ ]);
656
+ setShowAgentPrompt(false);
657
+ setPhase("done");
658
+ }, [agentSlugs, cwd, scope, options.global]);
659
+ return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", children: [
660
+ /* @__PURE__ */ jsx4(Banner, {}),
661
+ messages.map((m, i) => /* @__PURE__ */ jsx4(StatusMessage, { variant: m.variant, children: m.text }, i)),
662
+ showAgentPrompt && /* @__PURE__ */ jsx4(
663
+ MultiSelect,
664
+ {
665
+ prompt: "Select your preferred AI agent(s)",
666
+ items: agentLabels,
667
+ onConfirm: onAgentsConfirm
668
+ }
669
+ )
670
+ ] });
671
+ }
672
+ async function initCommand(options = {}) {
673
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx4(InitApp, { options }));
674
+ await waitUntilExit();
675
+ }
676
+
677
+ export {
678
+ exists,
679
+ hashDirectory,
680
+ diffHashes,
681
+ parseSource,
682
+ parseSkillEntry,
683
+ readManifest,
684
+ addSkillToManifest,
685
+ removeSkillFromManifest,
686
+ KONSTRUCT_DIR,
687
+ AGENT_REGISTRY,
688
+ readConfig,
689
+ writeConfig,
690
+ getAgentSkillDirs,
691
+ Banner,
692
+ StatusMessage,
693
+ MultiSelect,
694
+ getAgentLabels,
695
+ initCommand
696
+ };