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/LICENSE +21 -0
- package/README.md +77 -0
- package/bin/cli.js +23 -0
- package/dist/chunk-MYTZHNE6.js +696 -0
- package/dist/index.js +1197 -0
- package/dist/init-CJ7EZ75L.js +6 -0
- package/package.json +63 -0
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
|
+
});
|