tenfold 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/bin/tenfold.d.ts +1 -0
- package/dist/bin/tenfold.js +8 -0
- package/dist/chunk-C53TSGQL.js +2814 -0
- package/dist/src/index.d.ts +1431 -0
- package/dist/src/index.js +54 -0
- package/package.json +64 -0
|
@@ -0,0 +1,2814 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Command as Command7 } from "commander";
|
|
3
|
+
|
|
4
|
+
// src/ui/banner.ts
|
|
5
|
+
import figlet from "figlet";
|
|
6
|
+
|
|
7
|
+
// src/ui/theme.ts
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import gradientString from "gradient-string";
|
|
10
|
+
var colors = {
|
|
11
|
+
primary: chalk.hex("#3b82f6"),
|
|
12
|
+
// Blue
|
|
13
|
+
secondary: chalk.hex("#60a5fa"),
|
|
14
|
+
// Light Blue
|
|
15
|
+
success: chalk.hex("#22c55e"),
|
|
16
|
+
// Green
|
|
17
|
+
warning: chalk.hex("#f59e0b"),
|
|
18
|
+
// Amber
|
|
19
|
+
error: chalk.hex("#ef4444"),
|
|
20
|
+
// Red
|
|
21
|
+
info: chalk.hex("#0ea5e9"),
|
|
22
|
+
// Sky
|
|
23
|
+
muted: chalk.dim,
|
|
24
|
+
highlight: chalk.cyan,
|
|
25
|
+
bold: chalk.bold,
|
|
26
|
+
underline: chalk.underline
|
|
27
|
+
};
|
|
28
|
+
var brandGradient = gradientString([
|
|
29
|
+
"#2563eb",
|
|
30
|
+
// Blue 600
|
|
31
|
+
"#3b82f6",
|
|
32
|
+
// Blue 500
|
|
33
|
+
"#60a5fa"
|
|
34
|
+
// Blue 400
|
|
35
|
+
]);
|
|
36
|
+
var typeColors = {
|
|
37
|
+
INSTRUCTION: chalk.hex("#3b82f6"),
|
|
38
|
+
// Blue
|
|
39
|
+
HOOK: chalk.hex("#f59e0b"),
|
|
40
|
+
// Amber
|
|
41
|
+
COMMAND: chalk.hex("#22c55e"),
|
|
42
|
+
// Green
|
|
43
|
+
MCP_SERVER: chalk.hex("#8b5cf6"),
|
|
44
|
+
// Violet
|
|
45
|
+
SKILL: chalk.hex("#ec4899"),
|
|
46
|
+
// Pink
|
|
47
|
+
AGENT: chalk.hex("#14b8a6")
|
|
48
|
+
// Teal
|
|
49
|
+
};
|
|
50
|
+
var statusColors = {
|
|
51
|
+
installed: colors.success,
|
|
52
|
+
available: colors.info,
|
|
53
|
+
outdated: colors.warning,
|
|
54
|
+
conflict: colors.error
|
|
55
|
+
};
|
|
56
|
+
var typeIcons = {
|
|
57
|
+
INSTRUCTION: "\u{1F4DD}",
|
|
58
|
+
// 📝
|
|
59
|
+
HOOK: "\u{1F527}",
|
|
60
|
+
// 🔧
|
|
61
|
+
COMMAND: "\u2318",
|
|
62
|
+
// ⌘
|
|
63
|
+
MCP_SERVER: "\u{1F5A5}",
|
|
64
|
+
// 🖥️
|
|
65
|
+
SKILL: "\u26A1",
|
|
66
|
+
// ⚡
|
|
67
|
+
AGENT: "\u{1F916}"
|
|
68
|
+
// 🤖
|
|
69
|
+
};
|
|
70
|
+
function success(text) {
|
|
71
|
+
return colors.success(text);
|
|
72
|
+
}
|
|
73
|
+
function muted(text) {
|
|
74
|
+
return colors.muted(text);
|
|
75
|
+
}
|
|
76
|
+
function highlight(text) {
|
|
77
|
+
return colors.highlight(text);
|
|
78
|
+
}
|
|
79
|
+
function bold(text) {
|
|
80
|
+
return colors.bold(text);
|
|
81
|
+
}
|
|
82
|
+
function formatInstalls(count) {
|
|
83
|
+
if (count < 1e3) return muted(`${count} installs`);
|
|
84
|
+
if (count < 1e6) return muted(`${(count / 1e3).toFixed(1)}k installs`);
|
|
85
|
+
return muted(`${(count / 1e6).toFixed(1)}M installs`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/ui/banner.ts
|
|
89
|
+
var BANNER_TEXT = "TENFOLD";
|
|
90
|
+
function renderBanner() {
|
|
91
|
+
const ascii = figlet.textSync(BANNER_TEXT, {
|
|
92
|
+
font: "ANSI Shadow",
|
|
93
|
+
horizontalLayout: "default"
|
|
94
|
+
});
|
|
95
|
+
const gradientBanner = brandGradient(ascii);
|
|
96
|
+
const tagline = muted(" The package manager for Claude Code components\n");
|
|
97
|
+
return `
|
|
98
|
+
${gradientBanner}
|
|
99
|
+
${tagline}`;
|
|
100
|
+
}
|
|
101
|
+
function renderWelcome() {
|
|
102
|
+
return `
|
|
103
|
+
${renderBanner()}
|
|
104
|
+
${muted("Get started:")}
|
|
105
|
+
tenfold search "typescript" Search for components
|
|
106
|
+
tenfold browse --type=hook Browse by type
|
|
107
|
+
tenfold install <author/name> Install a component
|
|
108
|
+
|
|
109
|
+
${muted("Learn more:")}
|
|
110
|
+
tenfold --help Show all commands
|
|
111
|
+
https://tenfold.dev/docs Documentation
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/commands/search.ts
|
|
116
|
+
import { Command } from "commander";
|
|
117
|
+
|
|
118
|
+
// src/utils/errors.ts
|
|
119
|
+
var TenfoldError = class extends Error {
|
|
120
|
+
constructor(message, code) {
|
|
121
|
+
super(message);
|
|
122
|
+
this.code = code;
|
|
123
|
+
this.name = "TenfoldError";
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var ApiError = class _ApiError extends TenfoldError {
|
|
127
|
+
constructor(status, message) {
|
|
128
|
+
super(`API Error (${status}): ${message}`, "API_ERROR");
|
|
129
|
+
this.status = status;
|
|
130
|
+
this.name = "ApiError";
|
|
131
|
+
}
|
|
132
|
+
static fromStatus(status, body) {
|
|
133
|
+
let message;
|
|
134
|
+
switch (status) {
|
|
135
|
+
case 400:
|
|
136
|
+
message = "Bad request. Check your input.";
|
|
137
|
+
break;
|
|
138
|
+
case 401:
|
|
139
|
+
message = "Authentication required. Run 'tenfold login' first.";
|
|
140
|
+
break;
|
|
141
|
+
case 403:
|
|
142
|
+
message = "Permission denied. You may not have access to this resource.";
|
|
143
|
+
break;
|
|
144
|
+
case 404:
|
|
145
|
+
message = "Not found. Check the author/slug spelling.";
|
|
146
|
+
break;
|
|
147
|
+
case 429:
|
|
148
|
+
message = "Rate limited. Please wait a moment and try again.";
|
|
149
|
+
break;
|
|
150
|
+
case 500:
|
|
151
|
+
case 502:
|
|
152
|
+
case 503:
|
|
153
|
+
message = "Server error. Please try again later.";
|
|
154
|
+
break;
|
|
155
|
+
default:
|
|
156
|
+
message = body ?? "An unexpected error occurred.";
|
|
157
|
+
}
|
|
158
|
+
return new _ApiError(status, message);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
var NotFoundError = class extends TenfoldError {
|
|
162
|
+
constructor(resourceType, identifier) {
|
|
163
|
+
super(`${resourceType} not found: ${identifier}`, "NOT_FOUND");
|
|
164
|
+
this.resourceType = resourceType;
|
|
165
|
+
this.identifier = identifier;
|
|
166
|
+
this.name = "NotFoundError";
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var ConflictError = class extends TenfoldError {
|
|
170
|
+
constructor(conflicts) {
|
|
171
|
+
super(`${conflicts.length} conflict(s) detected`, "CONFLICT");
|
|
172
|
+
this.conflicts = conflicts;
|
|
173
|
+
this.name = "ConflictError";
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
var AuthRequiredError = class extends TenfoldError {
|
|
177
|
+
constructor(operation) {
|
|
178
|
+
super(
|
|
179
|
+
`Authentication required for: ${operation}. Run 'tenfold login' first.`,
|
|
180
|
+
"AUTH_REQUIRED"
|
|
181
|
+
);
|
|
182
|
+
this.operation = operation;
|
|
183
|
+
this.name = "AuthRequiredError";
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
var ConfigError = class extends TenfoldError {
|
|
187
|
+
constructor(filePath, message) {
|
|
188
|
+
super(`Config error in ${filePath}: ${message}`, "CONFIG_ERROR");
|
|
189
|
+
this.filePath = filePath;
|
|
190
|
+
this.name = "ConfigError";
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
var ValidationError = class extends TenfoldError {
|
|
194
|
+
constructor(message) {
|
|
195
|
+
super(message, "VALIDATION_ERROR");
|
|
196
|
+
this.name = "ValidationError";
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
var NetworkError = class extends TenfoldError {
|
|
200
|
+
constructor(message) {
|
|
201
|
+
super(message, "NETWORK_ERROR");
|
|
202
|
+
this.name = "NetworkError";
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
function formatError(error) {
|
|
206
|
+
if (error instanceof ApiError) {
|
|
207
|
+
return error.message;
|
|
208
|
+
}
|
|
209
|
+
if (error instanceof NotFoundError) {
|
|
210
|
+
return error.message;
|
|
211
|
+
}
|
|
212
|
+
if (error instanceof ConflictError) {
|
|
213
|
+
const conflicts = error.conflicts.map(
|
|
214
|
+
(c) => ` - ${c.type}: ${c.existing.source} vs ${c.incoming.source}`
|
|
215
|
+
).join("\n");
|
|
216
|
+
return `Conflicts detected:
|
|
217
|
+
${conflicts}
|
|
218
|
+
Use --force to override.`;
|
|
219
|
+
}
|
|
220
|
+
if (error instanceof AuthRequiredError) {
|
|
221
|
+
return error.message;
|
|
222
|
+
}
|
|
223
|
+
if (error instanceof ConfigError) {
|
|
224
|
+
return error.message;
|
|
225
|
+
}
|
|
226
|
+
if (error instanceof ValidationError) {
|
|
227
|
+
return error.message;
|
|
228
|
+
}
|
|
229
|
+
if (error instanceof NetworkError) {
|
|
230
|
+
return `Network error: ${error.message}. Check your internet connection.`;
|
|
231
|
+
}
|
|
232
|
+
if (error instanceof TenfoldError) {
|
|
233
|
+
return error.message;
|
|
234
|
+
}
|
|
235
|
+
if (error instanceof Error) {
|
|
236
|
+
return error.message;
|
|
237
|
+
}
|
|
238
|
+
return "An unexpected error occurred.";
|
|
239
|
+
}
|
|
240
|
+
function isRetryable(error) {
|
|
241
|
+
if (error instanceof ApiError) {
|
|
242
|
+
return [429, 500, 502, 503, 504].includes(error.status);
|
|
243
|
+
}
|
|
244
|
+
if (error instanceof NetworkError) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/api/client.ts
|
|
251
|
+
var DEFAULT_BASE_URL = process.env.TENFOLD_API_URL ?? "http://localhost:3000";
|
|
252
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
253
|
+
var MAX_RETRIES = 3;
|
|
254
|
+
var RETRY_DELAY = 1e3;
|
|
255
|
+
var TenfoldApiClient = class {
|
|
256
|
+
baseUrl;
|
|
257
|
+
apiKey;
|
|
258
|
+
timeout;
|
|
259
|
+
constructor(options = {}) {
|
|
260
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
261
|
+
this.apiKey = options.apiKey;
|
|
262
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
263
|
+
}
|
|
264
|
+
// Set API key (for authenticated operations)
|
|
265
|
+
setApiKey(key) {
|
|
266
|
+
this.apiKey = key;
|
|
267
|
+
}
|
|
268
|
+
// Core request method
|
|
269
|
+
async request(router, procedure, input2, options = {}) {
|
|
270
|
+
const isQuery = options.method !== "POST";
|
|
271
|
+
const url = new URL(`/api/trpc/${router}.${procedure}`, this.baseUrl);
|
|
272
|
+
if (isQuery && input2 !== void 0) {
|
|
273
|
+
url.searchParams.set("input", JSON.stringify({ json: input2 }));
|
|
274
|
+
}
|
|
275
|
+
const headers = {
|
|
276
|
+
"Content-Type": "application/json",
|
|
277
|
+
"User-Agent": "tenfold-cli/0.1.0"
|
|
278
|
+
};
|
|
279
|
+
if (this.apiKey) {
|
|
280
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
281
|
+
}
|
|
282
|
+
const controller = new AbortController();
|
|
283
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
284
|
+
try {
|
|
285
|
+
const response = await fetch(url.toString(), {
|
|
286
|
+
method: isQuery ? "GET" : "POST",
|
|
287
|
+
headers,
|
|
288
|
+
signal: controller.signal,
|
|
289
|
+
...input2 !== void 0 && !isQuery && { body: JSON.stringify({ json: input2 }) }
|
|
290
|
+
});
|
|
291
|
+
clearTimeout(timeoutId);
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
const body = await response.text().catch(() => "");
|
|
294
|
+
if (options.retry !== false && isRetryable(ApiError.fromStatus(response.status))) {
|
|
295
|
+
return this.retryRequest(router, procedure, input2, options);
|
|
296
|
+
}
|
|
297
|
+
throw ApiError.fromStatus(response.status, body);
|
|
298
|
+
}
|
|
299
|
+
const data = await response.json();
|
|
300
|
+
if (data.result?.data?.json !== void 0) {
|
|
301
|
+
return data.result.data.json;
|
|
302
|
+
}
|
|
303
|
+
return data;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
clearTimeout(timeoutId);
|
|
306
|
+
if (error instanceof ApiError) {
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
if (error instanceof Error) {
|
|
310
|
+
if (error.name === "AbortError") {
|
|
311
|
+
throw new NetworkError("Request timed out");
|
|
312
|
+
}
|
|
313
|
+
throw new NetworkError(error.message);
|
|
314
|
+
}
|
|
315
|
+
throw new NetworkError("Unknown network error");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Retry with exponential backoff
|
|
319
|
+
async retryRequest(router, procedure, input2, options = {}, attempt = 1) {
|
|
320
|
+
if (attempt > MAX_RETRIES) {
|
|
321
|
+
throw new NetworkError(`Failed after ${MAX_RETRIES} retries`);
|
|
322
|
+
}
|
|
323
|
+
await new Promise(
|
|
324
|
+
(resolve) => setTimeout(resolve, RETRY_DELAY * Math.pow(2, attempt - 1))
|
|
325
|
+
);
|
|
326
|
+
return this.request(router, procedure, input2, {
|
|
327
|
+
...options,
|
|
328
|
+
retry: attempt < MAX_RETRIES
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// ============================================
|
|
332
|
+
// Component Operations
|
|
333
|
+
// ============================================
|
|
334
|
+
async searchComponents(query, limit = 10) {
|
|
335
|
+
const result = await this.request(
|
|
336
|
+
"component",
|
|
337
|
+
"search",
|
|
338
|
+
{ query, limit }
|
|
339
|
+
);
|
|
340
|
+
return result ?? [];
|
|
341
|
+
}
|
|
342
|
+
async getComponent(slug) {
|
|
343
|
+
const result = await this.request(
|
|
344
|
+
"component",
|
|
345
|
+
"get",
|
|
346
|
+
{ slug }
|
|
347
|
+
);
|
|
348
|
+
if (!result) {
|
|
349
|
+
throw new NotFoundError("Component", slug);
|
|
350
|
+
}
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
async getComponentVersion(componentId, version) {
|
|
354
|
+
const result = await this.request(
|
|
355
|
+
"component",
|
|
356
|
+
"getVersion",
|
|
357
|
+
{ componentId, version }
|
|
358
|
+
);
|
|
359
|
+
if (!result) {
|
|
360
|
+
throw new NotFoundError("Component version", `${componentId}@${version}`);
|
|
361
|
+
}
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
async getAttribution(versionId) {
|
|
365
|
+
return this.request("component", "getAttribution", {
|
|
366
|
+
versionId
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
async listComponents(input2 = {}) {
|
|
370
|
+
return this.request("component", "list", input2);
|
|
371
|
+
}
|
|
372
|
+
async getFeaturedComponents(limit = 10) {
|
|
373
|
+
const result = await this.request(
|
|
374
|
+
"component",
|
|
375
|
+
"featured",
|
|
376
|
+
{ limit }
|
|
377
|
+
);
|
|
378
|
+
return result ?? [];
|
|
379
|
+
}
|
|
380
|
+
async getTrendingComponents(limit = 10) {
|
|
381
|
+
const result = await this.request(
|
|
382
|
+
"component",
|
|
383
|
+
"trending",
|
|
384
|
+
{ limit }
|
|
385
|
+
);
|
|
386
|
+
return result ?? [];
|
|
387
|
+
}
|
|
388
|
+
// ============================================
|
|
389
|
+
// Bundle Operations
|
|
390
|
+
// ============================================
|
|
391
|
+
async searchBundles(query, limit = 10) {
|
|
392
|
+
const result = await this.request(
|
|
393
|
+
"bundle",
|
|
394
|
+
"search",
|
|
395
|
+
{ query, limit }
|
|
396
|
+
);
|
|
397
|
+
return result ?? [];
|
|
398
|
+
}
|
|
399
|
+
async getBundle(slug) {
|
|
400
|
+
const result = await this.request("bundle", "get", {
|
|
401
|
+
slug
|
|
402
|
+
});
|
|
403
|
+
if (!result) {
|
|
404
|
+
throw new NotFoundError("Bundle", slug);
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
async listBundles(input2 = {}) {
|
|
409
|
+
return this.request("bundle", "list", input2);
|
|
410
|
+
}
|
|
411
|
+
// ============================================
|
|
412
|
+
// Category Operations
|
|
413
|
+
// ============================================
|
|
414
|
+
async listCategories() {
|
|
415
|
+
const result = await this.request(
|
|
416
|
+
"category",
|
|
417
|
+
"list",
|
|
418
|
+
{}
|
|
419
|
+
);
|
|
420
|
+
return result ?? [];
|
|
421
|
+
}
|
|
422
|
+
// ============================================
|
|
423
|
+
// Installation Tracking
|
|
424
|
+
// ============================================
|
|
425
|
+
async trackComponentInstall(componentSlug, versionId) {
|
|
426
|
+
await this.request(
|
|
427
|
+
"componentInstall",
|
|
428
|
+
"trackComponent",
|
|
429
|
+
{ componentSlug, versionId },
|
|
430
|
+
{ method: "POST" }
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
async trackBundleInstall(bundleSlug) {
|
|
434
|
+
await this.request(
|
|
435
|
+
"componentInstall",
|
|
436
|
+
"trackBundle",
|
|
437
|
+
{ bundleSlug },
|
|
438
|
+
{ method: "POST" }
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
// ============================================
|
|
442
|
+
// Authenticated Operations
|
|
443
|
+
// ============================================
|
|
444
|
+
async forkComponent(input2) {
|
|
445
|
+
if (!this.apiKey) {
|
|
446
|
+
throw new ApiError(401, "Authentication required for fork operation");
|
|
447
|
+
}
|
|
448
|
+
return this.request("component", "fork", input2, {
|
|
449
|
+
method: "POST"
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
var _client = null;
|
|
454
|
+
function getApiClient(options) {
|
|
455
|
+
if (!_client || options) {
|
|
456
|
+
_client = new TenfoldApiClient(options);
|
|
457
|
+
}
|
|
458
|
+
return _client;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/ui/spinner.ts
|
|
462
|
+
import ora from "ora";
|
|
463
|
+
function createSpinner(text, options) {
|
|
464
|
+
return ora({
|
|
465
|
+
text,
|
|
466
|
+
color: options?.color ?? "cyan",
|
|
467
|
+
spinner: "dots"
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
function startSpinner(text, options) {
|
|
471
|
+
const spinner = createSpinner(text, options);
|
|
472
|
+
return spinner.start();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/ui/table.ts
|
|
476
|
+
import Table from "cli-table3";
|
|
477
|
+
var tableStyle = {
|
|
478
|
+
head: ["cyan"],
|
|
479
|
+
border: ["dim"]
|
|
480
|
+
};
|
|
481
|
+
var tableChars = {
|
|
482
|
+
top: "\u2500",
|
|
483
|
+
"top-mid": "\u252C",
|
|
484
|
+
"top-left": "\u250C",
|
|
485
|
+
"top-right": "\u2510",
|
|
486
|
+
bottom: "\u2500",
|
|
487
|
+
"bottom-mid": "\u2534",
|
|
488
|
+
"bottom-left": "\u2514",
|
|
489
|
+
"bottom-right": "\u2518",
|
|
490
|
+
left: "\u2502",
|
|
491
|
+
"left-mid": "\u251C",
|
|
492
|
+
mid: "\u2500",
|
|
493
|
+
"mid-mid": "\u253C",
|
|
494
|
+
right: "\u2502",
|
|
495
|
+
"right-mid": "\u2524",
|
|
496
|
+
middle: "\u2502"
|
|
497
|
+
};
|
|
498
|
+
function searchResultsTable(components) {
|
|
499
|
+
const table = new Table({
|
|
500
|
+
head: ["", "Component", "Type", "Description", "Installs"],
|
|
501
|
+
style: tableStyle,
|
|
502
|
+
chars: tableChars,
|
|
503
|
+
colWidths: [3, 26, 13, 35, 10],
|
|
504
|
+
wordWrap: true
|
|
505
|
+
});
|
|
506
|
+
for (const component of components) {
|
|
507
|
+
const icon = typeIcons[component.type] || "\u{1F4E6}";
|
|
508
|
+
const typeColor = typeColors[component.type] || colors.muted;
|
|
509
|
+
const [author, name] = component.slug.split("/");
|
|
510
|
+
table.push([
|
|
511
|
+
icon,
|
|
512
|
+
`${muted(author + "/")}${highlight(name || "")}`,
|
|
513
|
+
typeColor(component.type),
|
|
514
|
+
component.description.slice(0, 50) + (component.description.length > 50 ? "..." : ""),
|
|
515
|
+
formatInstalls(component.installCount)
|
|
516
|
+
]);
|
|
517
|
+
}
|
|
518
|
+
return table.toString();
|
|
519
|
+
}
|
|
520
|
+
function browseResultsTable(components) {
|
|
521
|
+
const table = new Table({
|
|
522
|
+
head: ["Component", "Author", "Description", "Installs"],
|
|
523
|
+
style: tableStyle,
|
|
524
|
+
chars: tableChars,
|
|
525
|
+
colWidths: [25, 15, 40, 10],
|
|
526
|
+
wordWrap: true
|
|
527
|
+
});
|
|
528
|
+
for (const component of components) {
|
|
529
|
+
table.push([
|
|
530
|
+
highlight(component.slug.split("/")[1] || component.slug),
|
|
531
|
+
muted(`@${component.author.name}`),
|
|
532
|
+
component.description.slice(0, 55) + (component.description.length > 55 ? "..." : ""),
|
|
533
|
+
formatInstalls(component.installCount)
|
|
534
|
+
]);
|
|
535
|
+
}
|
|
536
|
+
return table.toString();
|
|
537
|
+
}
|
|
538
|
+
function installedTable(components) {
|
|
539
|
+
const table = new Table({
|
|
540
|
+
head: ["Component", "Type", "Version", "Installed"],
|
|
541
|
+
style: tableStyle,
|
|
542
|
+
chars: tableChars,
|
|
543
|
+
colWidths: [30, 13, 12, 15]
|
|
544
|
+
});
|
|
545
|
+
for (const component of components) {
|
|
546
|
+
const icon = typeIcons[component.type] || "\u{1F4E6}";
|
|
547
|
+
const typeColor = typeColors[component.type] || colors.muted;
|
|
548
|
+
table.push([
|
|
549
|
+
`${icon} ${highlight(component.slug)}`,
|
|
550
|
+
typeColor(component.type),
|
|
551
|
+
muted(`v${component.version}`),
|
|
552
|
+
muted(formatRelativeTime(component.installedAt))
|
|
553
|
+
]);
|
|
554
|
+
}
|
|
555
|
+
return table.toString();
|
|
556
|
+
}
|
|
557
|
+
function bundleItemsTable(bundle) {
|
|
558
|
+
const table = new Table({
|
|
559
|
+
head: ["", "Component", "Type", "Author"],
|
|
560
|
+
style: tableStyle,
|
|
561
|
+
chars: tableChars,
|
|
562
|
+
colWidths: [3, 30, 13, 15]
|
|
563
|
+
});
|
|
564
|
+
for (const item of bundle.items) {
|
|
565
|
+
const component = item.componentVersion.component;
|
|
566
|
+
const icon = typeIcons[component.type] || "\u{1F4E6}";
|
|
567
|
+
const typeColor = typeColors[component.type] || colors.muted;
|
|
568
|
+
table.push([
|
|
569
|
+
icon,
|
|
570
|
+
highlight(component.slug),
|
|
571
|
+
typeColor(component.type),
|
|
572
|
+
muted(`@${component.author.name}`)
|
|
573
|
+
]);
|
|
574
|
+
}
|
|
575
|
+
return table.toString();
|
|
576
|
+
}
|
|
577
|
+
function installResultTable(results) {
|
|
578
|
+
const table = new Table({
|
|
579
|
+
head: ["Component", "Type", "Status"],
|
|
580
|
+
style: tableStyle,
|
|
581
|
+
chars: tableChars,
|
|
582
|
+
colWidths: [30, 13, 12]
|
|
583
|
+
});
|
|
584
|
+
for (const result of results) {
|
|
585
|
+
const icon = typeIcons[result.type] || "\u{1F4E6}";
|
|
586
|
+
const typeColor = typeColors[result.type] || colors.muted;
|
|
587
|
+
const statusIcon = result.status === "Added" ? colors.success("\u2714") : result.status === "Skipped" ? colors.warning("\u25CB") : colors.error("\u2718");
|
|
588
|
+
table.push([
|
|
589
|
+
`${icon} ${highlight(result.component)}`,
|
|
590
|
+
typeColor(result.type),
|
|
591
|
+
`${statusIcon} ${result.status}`
|
|
592
|
+
]);
|
|
593
|
+
}
|
|
594
|
+
return table.toString();
|
|
595
|
+
}
|
|
596
|
+
function attributionTable(attributions) {
|
|
597
|
+
const table = new Table({
|
|
598
|
+
style: tableStyle,
|
|
599
|
+
chars: tableChars,
|
|
600
|
+
colWidths: [20, 10]
|
|
601
|
+
});
|
|
602
|
+
for (const attr of attributions) {
|
|
603
|
+
table.push([muted(`@${attr.authorName}`), `${attr.percentage.toFixed(0)}%`]);
|
|
604
|
+
}
|
|
605
|
+
return table.toString();
|
|
606
|
+
}
|
|
607
|
+
function formatRelativeTime(date) {
|
|
608
|
+
const d = new Date(date);
|
|
609
|
+
const now = /* @__PURE__ */ new Date();
|
|
610
|
+
const diffMs = now.getTime() - d.getTime();
|
|
611
|
+
const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
612
|
+
if (diffDays === 0) return "today";
|
|
613
|
+
if (diffDays === 1) return "yesterday";
|
|
614
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
615
|
+
if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`;
|
|
616
|
+
if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`;
|
|
617
|
+
return `${Math.floor(diffDays / 365)}y ago`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/commands/search.ts
|
|
621
|
+
function createSearchCommand() {
|
|
622
|
+
return new Command("search").description("Search for components").argument("<query>", "Search query").option("-t, --type <type>", "Filter by type (hook, skill, mcp_server, etc.)").option("-l, --limit <n>", "Number of results", "10").action(async (query, options) => {
|
|
623
|
+
const api = getApiClient();
|
|
624
|
+
const spinner = startSpinner(`Searching for "${query}"...`);
|
|
625
|
+
try {
|
|
626
|
+
const limit = parseInt(options.limit, 10);
|
|
627
|
+
const results = await api.searchComponents(query, limit);
|
|
628
|
+
spinner.stop();
|
|
629
|
+
if (results.length === 0) {
|
|
630
|
+
console.log(muted("\n No results found.\n"));
|
|
631
|
+
console.log(muted(" Try a different search term or browse by type:"));
|
|
632
|
+
console.log(muted(" tenfold browse --type=hook\n"));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
console.log(bold(`
|
|
636
|
+
\u{1F50D} Results for "${query}"
|
|
637
|
+
`));
|
|
638
|
+
console.log(searchResultsTable(results));
|
|
639
|
+
console.log();
|
|
640
|
+
if (results.length >= limit) {
|
|
641
|
+
console.log(
|
|
642
|
+
muted(` Showing first ${limit} results. Use --limit to see more.
|
|
643
|
+
`)
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
} catch (error) {
|
|
647
|
+
spinner.fail("Search failed");
|
|
648
|
+
console.error(colors.error(`
|
|
649
|
+
${formatError(error)}
|
|
650
|
+
`));
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/commands/browse.ts
|
|
657
|
+
import { Command as Command2 } from "commander";
|
|
658
|
+
var VALID_TYPES = [
|
|
659
|
+
"instruction",
|
|
660
|
+
"hook",
|
|
661
|
+
"command",
|
|
662
|
+
"mcp_server",
|
|
663
|
+
"skill",
|
|
664
|
+
"agent"
|
|
665
|
+
];
|
|
666
|
+
function createBrowseCommand() {
|
|
667
|
+
return new Command2("browse").description("Browse components by type or category").option("-t, --type <type>", "Filter by type (hook, skill, mcp_server, etc.)").option("-c, --category <category>", "Filter by category").option("-l, --limit <n>", "Number of results", "20").option("--featured", "Show only featured components").action(
|
|
668
|
+
async (options) => {
|
|
669
|
+
const api = getApiClient();
|
|
670
|
+
if (options.type) {
|
|
671
|
+
const normalizedType = options.type.toLowerCase().replace("-", "_");
|
|
672
|
+
if (!VALID_TYPES.includes(normalizedType)) {
|
|
673
|
+
console.error(
|
|
674
|
+
colors.error(`
|
|
675
|
+
Invalid type: ${options.type}
|
|
676
|
+
`)
|
|
677
|
+
);
|
|
678
|
+
console.log(muted(" Valid types:"));
|
|
679
|
+
for (const t of VALID_TYPES) {
|
|
680
|
+
const icon = typeIcons[t.toUpperCase()] || "\u{1F4E6}";
|
|
681
|
+
console.log(muted(` ${icon} ${t}`));
|
|
682
|
+
}
|
|
683
|
+
console.log();
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const spinner = startSpinner("Loading components...");
|
|
688
|
+
try {
|
|
689
|
+
const limit = parseInt(options.limit, 10);
|
|
690
|
+
const typeFilter = options.type?.toUpperCase().replace("-", "_");
|
|
691
|
+
const result = await api.listComponents({
|
|
692
|
+
limit,
|
|
693
|
+
type: typeFilter,
|
|
694
|
+
categorySlug: options.category,
|
|
695
|
+
featured: options.featured
|
|
696
|
+
});
|
|
697
|
+
spinner.stop();
|
|
698
|
+
if (result.components.length === 0) {
|
|
699
|
+
console.log(muted("\n No components found.\n"));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
let header = "\n ";
|
|
703
|
+
if (options.type) {
|
|
704
|
+
const icon = typeIcons[typeFilter || ""] || "\u{1F4E6}";
|
|
705
|
+
header += `${icon} ${bold(options.type.toUpperCase())} Components`;
|
|
706
|
+
} else if (options.category) {
|
|
707
|
+
header += `\u{1F4C1} ${bold(options.category)} Category`;
|
|
708
|
+
} else if (options.featured) {
|
|
709
|
+
header += `\u2B50 ${bold("Featured")} Components`;
|
|
710
|
+
} else {
|
|
711
|
+
header += bold("All Components");
|
|
712
|
+
}
|
|
713
|
+
console.log(header + "\n");
|
|
714
|
+
console.log(browseResultsTable(result.components));
|
|
715
|
+
console.log();
|
|
716
|
+
if (result.hasMore) {
|
|
717
|
+
console.log(
|
|
718
|
+
muted(
|
|
719
|
+
` Showing ${result.components.length} of ${result.total}. Use --limit to see more.
|
|
720
|
+
`
|
|
721
|
+
)
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
} catch (error) {
|
|
725
|
+
spinner.fail("Failed to load components");
|
|
726
|
+
console.error(colors.error(`
|
|
727
|
+
${formatError(error)}
|
|
728
|
+
`));
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/commands/install.ts
|
|
736
|
+
import { Command as Command3 } from "commander";
|
|
737
|
+
import { Listr } from "listr2";
|
|
738
|
+
|
|
739
|
+
// src/config/installed.ts
|
|
740
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
741
|
+
import { existsSync } from "fs";
|
|
742
|
+
import { dirname } from "path";
|
|
743
|
+
|
|
744
|
+
// src/types/config.ts
|
|
745
|
+
import { z } from "zod";
|
|
746
|
+
var TenfoldMetadataSchema = z.object({
|
|
747
|
+
componentId: z.string(),
|
|
748
|
+
versionId: z.string(),
|
|
749
|
+
version: z.string(),
|
|
750
|
+
author: z.string(),
|
|
751
|
+
slug: z.string(),
|
|
752
|
+
installedAt: z.string()
|
|
753
|
+
});
|
|
754
|
+
var TargetFileSchema = z.object({
|
|
755
|
+
type: z.enum([
|
|
756
|
+
"claude_md",
|
|
757
|
+
"settings_json",
|
|
758
|
+
"mcp_json",
|
|
759
|
+
"skill_dir",
|
|
760
|
+
"command_file"
|
|
761
|
+
]),
|
|
762
|
+
path: z.string(),
|
|
763
|
+
section: z.string().optional(),
|
|
764
|
+
key: z.string().optional()
|
|
765
|
+
});
|
|
766
|
+
var InstalledComponentSchema = z.object({
|
|
767
|
+
componentId: z.string(),
|
|
768
|
+
versionId: z.string(),
|
|
769
|
+
author: z.string(),
|
|
770
|
+
slug: z.string(),
|
|
771
|
+
type: z.enum([
|
|
772
|
+
"INSTRUCTION",
|
|
773
|
+
"HOOK",
|
|
774
|
+
"COMMAND",
|
|
775
|
+
"MCP_SERVER",
|
|
776
|
+
"SKILL",
|
|
777
|
+
"AGENT"
|
|
778
|
+
]),
|
|
779
|
+
version: z.string(),
|
|
780
|
+
installedAt: z.string(),
|
|
781
|
+
updatedAt: z.string().optional(),
|
|
782
|
+
targetFiles: z.array(TargetFileSchema),
|
|
783
|
+
attribution: z.array(
|
|
784
|
+
z.object({
|
|
785
|
+
authorId: z.string(),
|
|
786
|
+
authorName: z.string(),
|
|
787
|
+
percentage: z.number()
|
|
788
|
+
})
|
|
789
|
+
).optional()
|
|
790
|
+
});
|
|
791
|
+
var InstalledManifestSchema = z.object({
|
|
792
|
+
version: z.literal("1.0.0"),
|
|
793
|
+
components: z.array(InstalledComponentSchema),
|
|
794
|
+
lastUpdatedAt: z.string()
|
|
795
|
+
});
|
|
796
|
+
var HookDefinitionSchema = z.object({
|
|
797
|
+
type: z.literal("command"),
|
|
798
|
+
command: z.string()
|
|
799
|
+
});
|
|
800
|
+
var HookEntrySchema = z.object({
|
|
801
|
+
matcher: z.string(),
|
|
802
|
+
hooks: z.array(HookDefinitionSchema),
|
|
803
|
+
_tenfold: TenfoldMetadataSchema.optional()
|
|
804
|
+
});
|
|
805
|
+
var ClaudeSettingsSchema = z.object({
|
|
806
|
+
hooks: z.object({
|
|
807
|
+
PreToolUse: z.array(HookEntrySchema).optional(),
|
|
808
|
+
PostToolUse: z.array(HookEntrySchema).optional(),
|
|
809
|
+
Stop: z.array(HookEntrySchema).optional(),
|
|
810
|
+
Start: z.array(HookEntrySchema).optional(),
|
|
811
|
+
Subagent: z.array(HookEntrySchema).optional()
|
|
812
|
+
}).optional()
|
|
813
|
+
});
|
|
814
|
+
var McpServerEntrySchema = z.object({
|
|
815
|
+
command: z.string(),
|
|
816
|
+
args: z.array(z.string()).optional(),
|
|
817
|
+
env: z.record(z.string()).optional(),
|
|
818
|
+
_tenfold: TenfoldMetadataSchema.optional()
|
|
819
|
+
});
|
|
820
|
+
var McpConfigSchema = z.object({
|
|
821
|
+
mcpServers: z.record(McpServerEntrySchema).optional()
|
|
822
|
+
});
|
|
823
|
+
var CredentialsSchema = z.object({
|
|
824
|
+
apiKey: z.string().optional(),
|
|
825
|
+
userId: z.string().optional(),
|
|
826
|
+
createdAt: z.string().optional()
|
|
827
|
+
});
|
|
828
|
+
function createEmptyManifest() {
|
|
829
|
+
return {
|
|
830
|
+
version: "1.0.0",
|
|
831
|
+
components: [],
|
|
832
|
+
lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/utils/paths.ts
|
|
837
|
+
import { homedir } from "os";
|
|
838
|
+
import { join } from "path";
|
|
839
|
+
function getHomeDir() {
|
|
840
|
+
return homedir();
|
|
841
|
+
}
|
|
842
|
+
function getClaudeDir() {
|
|
843
|
+
return join(getHomeDir(), ".claude");
|
|
844
|
+
}
|
|
845
|
+
function getTenfoldDir() {
|
|
846
|
+
return join(getHomeDir(), ".tenfold");
|
|
847
|
+
}
|
|
848
|
+
function getClaudeMdPath() {
|
|
849
|
+
return join(getClaudeDir(), "CLAUDE.md");
|
|
850
|
+
}
|
|
851
|
+
function getSettingsPath() {
|
|
852
|
+
return join(getClaudeDir(), "settings.json");
|
|
853
|
+
}
|
|
854
|
+
function getCommandsDir() {
|
|
855
|
+
return join(getClaudeDir(), "commands");
|
|
856
|
+
}
|
|
857
|
+
function getPluginsDir() {
|
|
858
|
+
return join(getClaudeDir(), "plugins");
|
|
859
|
+
}
|
|
860
|
+
function getMcpConfigPath() {
|
|
861
|
+
return join(getHomeDir(), ".mcp.json");
|
|
862
|
+
}
|
|
863
|
+
function getInstalledManifestPath() {
|
|
864
|
+
return join(getTenfoldDir(), "installed.json");
|
|
865
|
+
}
|
|
866
|
+
function isValidSlug(slug) {
|
|
867
|
+
const slugRegex = /^[a-z0-9_-]+\/[a-z0-9_-]+$/i;
|
|
868
|
+
return slugRegex.test(slug);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/config/installed.ts
|
|
872
|
+
var InstalledManager = class {
|
|
873
|
+
manifestPath;
|
|
874
|
+
manifest = null;
|
|
875
|
+
constructor(manifestPath) {
|
|
876
|
+
this.manifestPath = manifestPath ?? getInstalledManifestPath();
|
|
877
|
+
}
|
|
878
|
+
// Load manifest from disk
|
|
879
|
+
async load() {
|
|
880
|
+
if (this.manifest) {
|
|
881
|
+
return this.manifest;
|
|
882
|
+
}
|
|
883
|
+
if (!existsSync(this.manifestPath)) {
|
|
884
|
+
this.manifest = createEmptyManifest();
|
|
885
|
+
return this.manifest;
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
const content = await readFile(this.manifestPath, "utf-8");
|
|
889
|
+
const parsed = JSON.parse(content);
|
|
890
|
+
const validated = InstalledManifestSchema.parse(parsed);
|
|
891
|
+
this.manifest = validated;
|
|
892
|
+
return validated;
|
|
893
|
+
} catch (error) {
|
|
894
|
+
if (error instanceof SyntaxError) {
|
|
895
|
+
throw new ConfigError(this.manifestPath, "Invalid JSON");
|
|
896
|
+
}
|
|
897
|
+
throw new ConfigError(
|
|
898
|
+
this.manifestPath,
|
|
899
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
// Save manifest to disk
|
|
904
|
+
async save() {
|
|
905
|
+
if (!this.manifest) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
this.manifest.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
909
|
+
const dir = dirname(this.manifestPath);
|
|
910
|
+
if (!existsSync(dir)) {
|
|
911
|
+
await mkdir(dir, { recursive: true });
|
|
912
|
+
}
|
|
913
|
+
await writeFile(
|
|
914
|
+
this.manifestPath,
|
|
915
|
+
JSON.stringify(this.manifest, null, 2),
|
|
916
|
+
"utf-8"
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
// Get all installed components
|
|
920
|
+
async getAll() {
|
|
921
|
+
const manifest = await this.load();
|
|
922
|
+
return manifest.components;
|
|
923
|
+
}
|
|
924
|
+
// Get installed component by slug
|
|
925
|
+
async get(slug) {
|
|
926
|
+
const manifest = await this.load();
|
|
927
|
+
return manifest.components.find((c) => c.slug === slug) ?? null;
|
|
928
|
+
}
|
|
929
|
+
// Check if component is installed
|
|
930
|
+
async isInstalled(slug) {
|
|
931
|
+
const component = await this.get(slug);
|
|
932
|
+
return component !== null;
|
|
933
|
+
}
|
|
934
|
+
// Get installed version
|
|
935
|
+
async getInstalledVersion(slug) {
|
|
936
|
+
const component = await this.get(slug);
|
|
937
|
+
return component?.version ?? null;
|
|
938
|
+
}
|
|
939
|
+
// Track a new installation
|
|
940
|
+
async track(component, targetFiles, attribution) {
|
|
941
|
+
const manifest = await this.load();
|
|
942
|
+
manifest.components = manifest.components.filter(
|
|
943
|
+
(c) => c.slug !== component.slug
|
|
944
|
+
);
|
|
945
|
+
manifest.components.push({
|
|
946
|
+
componentId: component.componentId,
|
|
947
|
+
versionId: component.versionId,
|
|
948
|
+
author: component.author,
|
|
949
|
+
slug: component.slug,
|
|
950
|
+
type: component.type,
|
|
951
|
+
version: component.version,
|
|
952
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
953
|
+
targetFiles,
|
|
954
|
+
attribution: attribution?.map((a) => ({
|
|
955
|
+
authorId: a.authorId,
|
|
956
|
+
authorName: a.authorName,
|
|
957
|
+
percentage: a.percentage
|
|
958
|
+
}))
|
|
959
|
+
});
|
|
960
|
+
await this.save();
|
|
961
|
+
}
|
|
962
|
+
// Update an existing installation
|
|
963
|
+
async update(slug, newVersion, targetFiles) {
|
|
964
|
+
const manifest = await this.load();
|
|
965
|
+
const index = manifest.components.findIndex((c) => c.slug === slug);
|
|
966
|
+
if (index === -1) {
|
|
967
|
+
throw new ConfigError(this.manifestPath, `Component not installed: ${slug}`);
|
|
968
|
+
}
|
|
969
|
+
manifest.components[index] = {
|
|
970
|
+
...manifest.components[index],
|
|
971
|
+
versionId: newVersion.versionId,
|
|
972
|
+
version: newVersion.version,
|
|
973
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
974
|
+
targetFiles
|
|
975
|
+
};
|
|
976
|
+
await this.save();
|
|
977
|
+
}
|
|
978
|
+
// Remove an installation
|
|
979
|
+
async remove(slug) {
|
|
980
|
+
const manifest = await this.load();
|
|
981
|
+
const index = manifest.components.findIndex((c) => c.slug === slug);
|
|
982
|
+
if (index === -1) {
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
const [removed] = manifest.components.splice(index, 1);
|
|
986
|
+
await this.save();
|
|
987
|
+
return removed ?? null;
|
|
988
|
+
}
|
|
989
|
+
// Get components by type
|
|
990
|
+
async getByType(type) {
|
|
991
|
+
const manifest = await this.load();
|
|
992
|
+
return manifest.components.filter((c) => c.type === type);
|
|
993
|
+
}
|
|
994
|
+
// Get components that need updates (compare with latest versions)
|
|
995
|
+
async getOutdated(latestVersions) {
|
|
996
|
+
const manifest = await this.load();
|
|
997
|
+
return manifest.components.filter((c) => {
|
|
998
|
+
const latest = latestVersions.get(c.slug);
|
|
999
|
+
return latest && latest !== c.version;
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
// Clear all installed components (for testing)
|
|
1003
|
+
async clear() {
|
|
1004
|
+
this.manifest = createEmptyManifest();
|
|
1005
|
+
await this.save();
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
var _manager = null;
|
|
1009
|
+
function getInstalledManager() {
|
|
1010
|
+
if (!_manager) {
|
|
1011
|
+
_manager = new InstalledManager();
|
|
1012
|
+
}
|
|
1013
|
+
return _manager;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/config/settings.ts
|
|
1017
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1018
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1019
|
+
import { dirname as dirname2 } from "path";
|
|
1020
|
+
var SettingsManager = class {
|
|
1021
|
+
settingsPath;
|
|
1022
|
+
settings = null;
|
|
1023
|
+
constructor(settingsPath) {
|
|
1024
|
+
this.settingsPath = settingsPath ?? getSettingsPath();
|
|
1025
|
+
}
|
|
1026
|
+
// Load settings from disk
|
|
1027
|
+
async load() {
|
|
1028
|
+
if (this.settings) {
|
|
1029
|
+
return this.settings;
|
|
1030
|
+
}
|
|
1031
|
+
if (!existsSync2(this.settingsPath)) {
|
|
1032
|
+
this.settings = { hooks: {} };
|
|
1033
|
+
return this.settings;
|
|
1034
|
+
}
|
|
1035
|
+
try {
|
|
1036
|
+
const content = await readFile2(this.settingsPath, "utf-8");
|
|
1037
|
+
if (!content.trim()) {
|
|
1038
|
+
this.settings = { hooks: {} };
|
|
1039
|
+
return this.settings;
|
|
1040
|
+
}
|
|
1041
|
+
this.settings = JSON.parse(content);
|
|
1042
|
+
return this.settings;
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
if (error instanceof SyntaxError) {
|
|
1045
|
+
throw new ConfigError(this.settingsPath, "Invalid JSON");
|
|
1046
|
+
}
|
|
1047
|
+
throw new ConfigError(
|
|
1048
|
+
this.settingsPath,
|
|
1049
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
// Save settings to disk
|
|
1054
|
+
async save() {
|
|
1055
|
+
if (!this.settings) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const dir = dirname2(this.settingsPath);
|
|
1059
|
+
if (!existsSync2(dir)) {
|
|
1060
|
+
await mkdir2(dir, { recursive: true });
|
|
1061
|
+
}
|
|
1062
|
+
await writeFile2(
|
|
1063
|
+
this.settingsPath,
|
|
1064
|
+
JSON.stringify(this.settings, null, 2),
|
|
1065
|
+
"utf-8"
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
// Install a hook
|
|
1069
|
+
async installHook(hookContent, metadata) {
|
|
1070
|
+
const settings = await this.load();
|
|
1071
|
+
if (!settings.hooks) {
|
|
1072
|
+
settings.hooks = {};
|
|
1073
|
+
}
|
|
1074
|
+
const event = hookContent.event;
|
|
1075
|
+
if (!settings.hooks[event]) {
|
|
1076
|
+
settings.hooks[event] = [];
|
|
1077
|
+
}
|
|
1078
|
+
const hookEntry = {
|
|
1079
|
+
matcher: hookContent.matcher || "*",
|
|
1080
|
+
hooks: [
|
|
1081
|
+
{
|
|
1082
|
+
type: "command",
|
|
1083
|
+
command: `tenfold run-hook ${metadata.slug}`
|
|
1084
|
+
}
|
|
1085
|
+
],
|
|
1086
|
+
_tenfold: metadata
|
|
1087
|
+
};
|
|
1088
|
+
settings.hooks[event].push(hookEntry);
|
|
1089
|
+
await this.save();
|
|
1090
|
+
return {
|
|
1091
|
+
type: "settings_json",
|
|
1092
|
+
path: this.settingsPath,
|
|
1093
|
+
key: `hooks.${event}`
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
// Uninstall a hook by slug
|
|
1097
|
+
async uninstallHook(slug) {
|
|
1098
|
+
const settings = await this.load();
|
|
1099
|
+
if (!settings.hooks) {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
let removed = false;
|
|
1103
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
1104
|
+
const hooks = settings.hooks[event];
|
|
1105
|
+
if (!hooks) continue;
|
|
1106
|
+
const initialLength = hooks.length;
|
|
1107
|
+
settings.hooks[event] = hooks.filter(
|
|
1108
|
+
(h) => h._tenfold?.slug !== slug
|
|
1109
|
+
);
|
|
1110
|
+
if (settings.hooks[event].length < initialLength) {
|
|
1111
|
+
removed = true;
|
|
1112
|
+
}
|
|
1113
|
+
if (settings.hooks[event].length === 0) {
|
|
1114
|
+
delete settings.hooks[event];
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
if (removed) {
|
|
1118
|
+
await this.save();
|
|
1119
|
+
}
|
|
1120
|
+
return removed;
|
|
1121
|
+
}
|
|
1122
|
+
// Check if a hook is installed
|
|
1123
|
+
async isHookInstalled(slug) {
|
|
1124
|
+
const settings = await this.load();
|
|
1125
|
+
if (!settings.hooks) {
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
1129
|
+
const hooks = settings.hooks[event];
|
|
1130
|
+
if (!hooks) continue;
|
|
1131
|
+
if (hooks.some((h) => h._tenfold?.slug === slug)) {
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
// Get all installed hooks with tenfold metadata
|
|
1138
|
+
async getInstalledHooks() {
|
|
1139
|
+
const settings = await this.load();
|
|
1140
|
+
const result = [];
|
|
1141
|
+
if (!settings.hooks) {
|
|
1142
|
+
return result;
|
|
1143
|
+
}
|
|
1144
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
1145
|
+
const hooks = settings.hooks[event];
|
|
1146
|
+
if (!hooks) continue;
|
|
1147
|
+
for (const entry of hooks) {
|
|
1148
|
+
if (entry._tenfold) {
|
|
1149
|
+
result.push({ event, entry });
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return result;
|
|
1154
|
+
}
|
|
1155
|
+
// Check for hook conflicts (same event + matcher)
|
|
1156
|
+
async detectConflict(hookContent) {
|
|
1157
|
+
const settings = await this.load();
|
|
1158
|
+
if (!settings.hooks) {
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
const event = hookContent.event;
|
|
1162
|
+
const hooks = settings.hooks[event];
|
|
1163
|
+
if (!hooks) {
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
const matcher = hookContent.matcher || "*";
|
|
1167
|
+
for (const entry of hooks) {
|
|
1168
|
+
if (entry.matcher === matcher && entry._tenfold) {
|
|
1169
|
+
return entry;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
// Replace an existing hook
|
|
1175
|
+
async replaceHook(oldSlug, hookContent, metadata) {
|
|
1176
|
+
await this.uninstallHook(oldSlug);
|
|
1177
|
+
return this.installHook(hookContent, metadata);
|
|
1178
|
+
}
|
|
1179
|
+
// Get hook definition for run-hook command
|
|
1180
|
+
async getHookDefinition(slug) {
|
|
1181
|
+
const settings = await this.load();
|
|
1182
|
+
if (!settings.hooks) {
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
1186
|
+
const hooks = settings.hooks[event];
|
|
1187
|
+
if (!hooks) continue;
|
|
1188
|
+
const entry = hooks.find((h) => h._tenfold?.slug === slug);
|
|
1189
|
+
if (entry && entry._tenfold) {
|
|
1190
|
+
return {
|
|
1191
|
+
name: slug.split("/")[1] || slug,
|
|
1192
|
+
event,
|
|
1193
|
+
matcher: entry.matcher,
|
|
1194
|
+
command: entry.hooks[0]?.command || ""
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
var _manager2 = null;
|
|
1202
|
+
function getSettingsManager() {
|
|
1203
|
+
if (!_manager2) {
|
|
1204
|
+
_manager2 = new SettingsManager();
|
|
1205
|
+
}
|
|
1206
|
+
return _manager2;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// src/config/mcp.ts
|
|
1210
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
1211
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1212
|
+
var McpManager = class {
|
|
1213
|
+
configPath;
|
|
1214
|
+
config = null;
|
|
1215
|
+
constructor(configPath) {
|
|
1216
|
+
this.configPath = configPath ?? getMcpConfigPath();
|
|
1217
|
+
}
|
|
1218
|
+
// Load config from disk
|
|
1219
|
+
async load() {
|
|
1220
|
+
if (this.config) {
|
|
1221
|
+
return this.config;
|
|
1222
|
+
}
|
|
1223
|
+
if (!existsSync3(this.configPath)) {
|
|
1224
|
+
this.config = { mcpServers: {} };
|
|
1225
|
+
return this.config;
|
|
1226
|
+
}
|
|
1227
|
+
try {
|
|
1228
|
+
const content = await readFile3(this.configPath, "utf-8");
|
|
1229
|
+
if (!content.trim()) {
|
|
1230
|
+
this.config = { mcpServers: {} };
|
|
1231
|
+
return this.config;
|
|
1232
|
+
}
|
|
1233
|
+
this.config = JSON.parse(content);
|
|
1234
|
+
return this.config;
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
if (error instanceof SyntaxError) {
|
|
1237
|
+
throw new ConfigError(this.configPath, "Invalid JSON");
|
|
1238
|
+
}
|
|
1239
|
+
throw new ConfigError(
|
|
1240
|
+
this.configPath,
|
|
1241
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
// Save config to disk
|
|
1246
|
+
async save() {
|
|
1247
|
+
if (!this.config) {
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
await writeFile3(
|
|
1251
|
+
this.configPath,
|
|
1252
|
+
JSON.stringify(this.config, null, 2),
|
|
1253
|
+
"utf-8"
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
// Install an MCP server
|
|
1257
|
+
async installServer(serverContent, metadata) {
|
|
1258
|
+
const config = await this.load();
|
|
1259
|
+
if (!config.mcpServers) {
|
|
1260
|
+
config.mcpServers = {};
|
|
1261
|
+
}
|
|
1262
|
+
const serverEntry = {
|
|
1263
|
+
command: serverContent.command,
|
|
1264
|
+
args: serverContent.args,
|
|
1265
|
+
env: serverContent.env,
|
|
1266
|
+
_tenfold: metadata
|
|
1267
|
+
};
|
|
1268
|
+
config.mcpServers[serverContent.name] = serverEntry;
|
|
1269
|
+
await this.save();
|
|
1270
|
+
return {
|
|
1271
|
+
type: "mcp_json",
|
|
1272
|
+
path: this.configPath,
|
|
1273
|
+
key: `mcpServers.${serverContent.name}`
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
// Uninstall an MCP server by name
|
|
1277
|
+
async uninstallServer(name) {
|
|
1278
|
+
const config = await this.load();
|
|
1279
|
+
if (!config.mcpServers || !config.mcpServers[name]) {
|
|
1280
|
+
return false;
|
|
1281
|
+
}
|
|
1282
|
+
delete config.mcpServers[name];
|
|
1283
|
+
await this.save();
|
|
1284
|
+
return true;
|
|
1285
|
+
}
|
|
1286
|
+
// Uninstall by slug
|
|
1287
|
+
async uninstallBySlug(slug) {
|
|
1288
|
+
const config = await this.load();
|
|
1289
|
+
if (!config.mcpServers) {
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
let removed = false;
|
|
1293
|
+
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
1294
|
+
if (entry._tenfold?.slug === slug) {
|
|
1295
|
+
delete config.mcpServers[name];
|
|
1296
|
+
removed = true;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (removed) {
|
|
1300
|
+
await this.save();
|
|
1301
|
+
}
|
|
1302
|
+
return removed;
|
|
1303
|
+
}
|
|
1304
|
+
// Check if a server name exists
|
|
1305
|
+
async hasServer(name) {
|
|
1306
|
+
const config = await this.load();
|
|
1307
|
+
return !!(config.mcpServers && config.mcpServers[name]);
|
|
1308
|
+
}
|
|
1309
|
+
// Check if a slug is installed
|
|
1310
|
+
async isInstalled(slug) {
|
|
1311
|
+
const config = await this.load();
|
|
1312
|
+
if (!config.mcpServers) {
|
|
1313
|
+
return false;
|
|
1314
|
+
}
|
|
1315
|
+
return Object.values(config.mcpServers).some(
|
|
1316
|
+
(entry) => entry._tenfold?.slug === slug
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
// Get all installed MCP servers with tenfold metadata
|
|
1320
|
+
async getInstalledServers() {
|
|
1321
|
+
const config = await this.load();
|
|
1322
|
+
const result = [];
|
|
1323
|
+
if (!config.mcpServers) {
|
|
1324
|
+
return result;
|
|
1325
|
+
}
|
|
1326
|
+
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
1327
|
+
if (entry._tenfold) {
|
|
1328
|
+
result.push({ name, entry });
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return result;
|
|
1332
|
+
}
|
|
1333
|
+
// Detect name conflict
|
|
1334
|
+
async detectConflict(serverContent) {
|
|
1335
|
+
const config = await this.load();
|
|
1336
|
+
if (!config.mcpServers) {
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
const existing = config.mcpServers[serverContent.name];
|
|
1340
|
+
if (existing) {
|
|
1341
|
+
return existing;
|
|
1342
|
+
}
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
// Replace an existing server
|
|
1346
|
+
async replaceServer(name, serverContent, metadata) {
|
|
1347
|
+
await this.uninstallServer(name);
|
|
1348
|
+
return this.installServer(serverContent, metadata);
|
|
1349
|
+
}
|
|
1350
|
+
// Rename a server (for conflict resolution)
|
|
1351
|
+
async renameServer(oldName, newName) {
|
|
1352
|
+
const config = await this.load();
|
|
1353
|
+
if (!config.mcpServers || !config.mcpServers[oldName]) {
|
|
1354
|
+
return false;
|
|
1355
|
+
}
|
|
1356
|
+
const entry = config.mcpServers[oldName];
|
|
1357
|
+
delete config.mcpServers[oldName];
|
|
1358
|
+
config.mcpServers[newName] = entry;
|
|
1359
|
+
await this.save();
|
|
1360
|
+
return true;
|
|
1361
|
+
}
|
|
1362
|
+
// Get server entry by slug
|
|
1363
|
+
async getBySlug(slug) {
|
|
1364
|
+
const config = await this.load();
|
|
1365
|
+
if (!config.mcpServers) {
|
|
1366
|
+
return null;
|
|
1367
|
+
}
|
|
1368
|
+
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
1369
|
+
if (entry._tenfold?.slug === slug) {
|
|
1370
|
+
return { name, entry };
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
var _manager3 = null;
|
|
1377
|
+
function getMcpManager() {
|
|
1378
|
+
if (!_manager3) {
|
|
1379
|
+
_manager3 = new McpManager();
|
|
1380
|
+
}
|
|
1381
|
+
return _manager3;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// src/config/claude-md.ts
|
|
1385
|
+
import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
|
|
1386
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1387
|
+
import { dirname as dirname3 } from "path";
|
|
1388
|
+
var SECTION_START = (slug) => `<!-- tenfold:start ${slug} -->`;
|
|
1389
|
+
var SECTION_END = (slug) => `<!-- tenfold:end ${slug} -->`;
|
|
1390
|
+
var ClaudeMdManager = class {
|
|
1391
|
+
mdPath;
|
|
1392
|
+
content = null;
|
|
1393
|
+
constructor(mdPath) {
|
|
1394
|
+
this.mdPath = mdPath ?? getClaudeMdPath();
|
|
1395
|
+
}
|
|
1396
|
+
// Load content from disk
|
|
1397
|
+
async load() {
|
|
1398
|
+
if (this.content !== null) {
|
|
1399
|
+
return this.content;
|
|
1400
|
+
}
|
|
1401
|
+
if (!existsSync4(this.mdPath)) {
|
|
1402
|
+
this.content = "";
|
|
1403
|
+
return this.content;
|
|
1404
|
+
}
|
|
1405
|
+
try {
|
|
1406
|
+
this.content = await readFile4(this.mdPath, "utf-8");
|
|
1407
|
+
return this.content;
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
throw new ConfigError(
|
|
1410
|
+
this.mdPath,
|
|
1411
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
// Save content to disk
|
|
1416
|
+
async save() {
|
|
1417
|
+
if (this.content === null) {
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const dir = dirname3(this.mdPath);
|
|
1421
|
+
if (!existsSync4(dir)) {
|
|
1422
|
+
await mkdir3(dir, { recursive: true });
|
|
1423
|
+
}
|
|
1424
|
+
await writeFile4(this.mdPath, this.content, "utf-8");
|
|
1425
|
+
}
|
|
1426
|
+
// Install instruction content
|
|
1427
|
+
async install(instructionContent, metadata, strategy = "section") {
|
|
1428
|
+
let content = await this.load();
|
|
1429
|
+
const paragraphs = instructionContent.paragraphs.join("\n\n");
|
|
1430
|
+
const authorName = metadata.author;
|
|
1431
|
+
switch (strategy) {
|
|
1432
|
+
case "replace":
|
|
1433
|
+
this.content = this.wrapWithSection(paragraphs, metadata.slug, authorName);
|
|
1434
|
+
break;
|
|
1435
|
+
case "append":
|
|
1436
|
+
if (content && !content.endsWith("\n\n")) {
|
|
1437
|
+
content += content.endsWith("\n") ? "\n" : "\n\n";
|
|
1438
|
+
}
|
|
1439
|
+
this.content = content + this.wrapWithSection(paragraphs, metadata.slug, authorName);
|
|
1440
|
+
break;
|
|
1441
|
+
case "section":
|
|
1442
|
+
default:
|
|
1443
|
+
content = this.removeSection(content, metadata.slug);
|
|
1444
|
+
if (content && !content.endsWith("\n\n")) {
|
|
1445
|
+
content += content.endsWith("\n") ? "\n" : "\n\n";
|
|
1446
|
+
}
|
|
1447
|
+
this.content = content + this.wrapWithSection(paragraphs, metadata.slug, authorName);
|
|
1448
|
+
break;
|
|
1449
|
+
}
|
|
1450
|
+
await this.save();
|
|
1451
|
+
return {
|
|
1452
|
+
type: "claude_md",
|
|
1453
|
+
path: this.mdPath,
|
|
1454
|
+
section: metadata.slug
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
// Uninstall by removing section
|
|
1458
|
+
async uninstall(slug) {
|
|
1459
|
+
const content = await this.load();
|
|
1460
|
+
if (!this.hasSection(content, slug)) {
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
this.content = this.removeSection(content, slug);
|
|
1464
|
+
await this.save();
|
|
1465
|
+
return true;
|
|
1466
|
+
}
|
|
1467
|
+
// Check if a section exists
|
|
1468
|
+
hasSection(content, slug) {
|
|
1469
|
+
return content.includes(SECTION_START(slug)) && content.includes(SECTION_END(slug));
|
|
1470
|
+
}
|
|
1471
|
+
// Remove a section
|
|
1472
|
+
removeSection(content, slug) {
|
|
1473
|
+
const startMarker = SECTION_START(slug);
|
|
1474
|
+
const endMarker = SECTION_END(slug);
|
|
1475
|
+
const startIdx = content.indexOf(startMarker);
|
|
1476
|
+
if (startIdx === -1) {
|
|
1477
|
+
return content;
|
|
1478
|
+
}
|
|
1479
|
+
const endIdx = content.indexOf(endMarker);
|
|
1480
|
+
if (endIdx === -1) {
|
|
1481
|
+
return content;
|
|
1482
|
+
}
|
|
1483
|
+
const before = content.slice(0, startIdx);
|
|
1484
|
+
let after = content.slice(endIdx + endMarker.length);
|
|
1485
|
+
while (after.startsWith("\n")) {
|
|
1486
|
+
after = after.slice(1);
|
|
1487
|
+
}
|
|
1488
|
+
return before.trimEnd() + (after ? "\n\n" + after : "");
|
|
1489
|
+
}
|
|
1490
|
+
// Wrap content with section markers
|
|
1491
|
+
wrapWithSection(content, slug, authorName) {
|
|
1492
|
+
const displayName = slug.split("/")[1] || slug;
|
|
1493
|
+
return `${SECTION_START(slug)}
|
|
1494
|
+
## ${this.titleCase(displayName)} (by @${authorName})
|
|
1495
|
+
|
|
1496
|
+
${content}
|
|
1497
|
+
|
|
1498
|
+
${SECTION_END(slug)}`;
|
|
1499
|
+
}
|
|
1500
|
+
// Get all installed sections
|
|
1501
|
+
async getInstalledSections() {
|
|
1502
|
+
const content = await this.load();
|
|
1503
|
+
const sections = [];
|
|
1504
|
+
const regex = /<!-- tenfold:start ([^\s]+) -->/g;
|
|
1505
|
+
let match;
|
|
1506
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1507
|
+
sections.push(match[1]);
|
|
1508
|
+
}
|
|
1509
|
+
return sections;
|
|
1510
|
+
}
|
|
1511
|
+
// Check if section exists (for conflict detection)
|
|
1512
|
+
async detectConflict(slug) {
|
|
1513
|
+
const content = await this.load();
|
|
1514
|
+
return this.hasSection(content, slug);
|
|
1515
|
+
}
|
|
1516
|
+
// Get section content
|
|
1517
|
+
async getSectionContent(slug) {
|
|
1518
|
+
const content = await this.load();
|
|
1519
|
+
const startMarker = SECTION_START(slug);
|
|
1520
|
+
const endMarker = SECTION_END(slug);
|
|
1521
|
+
const startIdx = content.indexOf(startMarker);
|
|
1522
|
+
if (startIdx === -1) {
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
const endIdx = content.indexOf(endMarker);
|
|
1526
|
+
if (endIdx === -1) {
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
return content.slice(startIdx + startMarker.length, endIdx).trim();
|
|
1530
|
+
}
|
|
1531
|
+
// Helper: title case
|
|
1532
|
+
titleCase(str) {
|
|
1533
|
+
return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
var _manager4 = null;
|
|
1537
|
+
function getClaudeMdManager() {
|
|
1538
|
+
if (!_manager4) {
|
|
1539
|
+
_manager4 = new ClaudeMdManager();
|
|
1540
|
+
}
|
|
1541
|
+
return _manager4;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// src/config/commands.ts
|
|
1545
|
+
import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir4, unlink, readdir } from "fs/promises";
|
|
1546
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1547
|
+
import { join as join2, basename } from "path";
|
|
1548
|
+
var FRONTMATTER_START = "---";
|
|
1549
|
+
var TENFOLD_MARKER = "_tenfold:";
|
|
1550
|
+
var CommandsManager = class {
|
|
1551
|
+
commandsDir;
|
|
1552
|
+
constructor(commandsDir) {
|
|
1553
|
+
this.commandsDir = commandsDir ?? getCommandsDir();
|
|
1554
|
+
}
|
|
1555
|
+
// Ensure directory exists
|
|
1556
|
+
async ensureDir() {
|
|
1557
|
+
if (!existsSync5(this.commandsDir)) {
|
|
1558
|
+
await mkdir4(this.commandsDir, { recursive: true });
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
// Install a command
|
|
1562
|
+
async install(commandContent, metadata) {
|
|
1563
|
+
await this.ensureDir();
|
|
1564
|
+
const commandPath = join2(this.commandsDir, `${commandContent.name}.md`);
|
|
1565
|
+
const frontmatter = [
|
|
1566
|
+
FRONTMATTER_START,
|
|
1567
|
+
`description: ${commandContent.description}`,
|
|
1568
|
+
`${TENFOLD_MARKER} ${JSON.stringify(metadata)}`
|
|
1569
|
+
];
|
|
1570
|
+
if (commandContent.arguments && commandContent.arguments.length > 0) {
|
|
1571
|
+
frontmatter.push("arguments:");
|
|
1572
|
+
for (const arg of commandContent.arguments) {
|
|
1573
|
+
frontmatter.push(` - name: ${arg.name}`);
|
|
1574
|
+
frontmatter.push(` type: ${arg.type}`);
|
|
1575
|
+
frontmatter.push(` required: ${arg.required}`);
|
|
1576
|
+
if (arg.description) {
|
|
1577
|
+
frontmatter.push(` description: ${arg.description}`);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
frontmatter.push(FRONTMATTER_START);
|
|
1582
|
+
const content = frontmatter.join("\n") + "\n\n" + commandContent.content;
|
|
1583
|
+
await writeFile5(commandPath, content, "utf-8");
|
|
1584
|
+
return {
|
|
1585
|
+
type: "command_file",
|
|
1586
|
+
path: commandPath
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
// Uninstall a command by slug
|
|
1590
|
+
async uninstall(slug) {
|
|
1591
|
+
const installed = await this.getInstalledCommands();
|
|
1592
|
+
const command = installed.find((c) => c.metadata?.slug === slug);
|
|
1593
|
+
if (!command) {
|
|
1594
|
+
return false;
|
|
1595
|
+
}
|
|
1596
|
+
const commandPath = join2(this.commandsDir, `${command.name}.md`);
|
|
1597
|
+
if (existsSync5(commandPath)) {
|
|
1598
|
+
await unlink(commandPath);
|
|
1599
|
+
return true;
|
|
1600
|
+
}
|
|
1601
|
+
return false;
|
|
1602
|
+
}
|
|
1603
|
+
// Uninstall by command name
|
|
1604
|
+
async uninstallByName(name) {
|
|
1605
|
+
const commandPath = join2(this.commandsDir, `${name}.md`);
|
|
1606
|
+
if (existsSync5(commandPath)) {
|
|
1607
|
+
await unlink(commandPath);
|
|
1608
|
+
return true;
|
|
1609
|
+
}
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
// Check if a command name exists
|
|
1613
|
+
async hasCommand(name) {
|
|
1614
|
+
const commandPath = join2(this.commandsDir, `${name}.md`);
|
|
1615
|
+
return existsSync5(commandPath);
|
|
1616
|
+
}
|
|
1617
|
+
// Check if a slug is installed
|
|
1618
|
+
async isInstalled(slug) {
|
|
1619
|
+
const installed = await this.getInstalledCommands();
|
|
1620
|
+
return installed.some((c) => c.metadata?.slug === slug);
|
|
1621
|
+
}
|
|
1622
|
+
// Get all installed commands with tenfold metadata
|
|
1623
|
+
async getInstalledCommands() {
|
|
1624
|
+
if (!existsSync5(this.commandsDir)) {
|
|
1625
|
+
return [];
|
|
1626
|
+
}
|
|
1627
|
+
const files = await readdir(this.commandsDir);
|
|
1628
|
+
const result = [];
|
|
1629
|
+
for (const file of files) {
|
|
1630
|
+
if (!file.endsWith(".md")) continue;
|
|
1631
|
+
const commandPath = join2(this.commandsDir, file);
|
|
1632
|
+
const content = await readFile5(commandPath, "utf-8");
|
|
1633
|
+
const metadata = this.extractMetadata(content);
|
|
1634
|
+
result.push({
|
|
1635
|
+
name: basename(file, ".md"),
|
|
1636
|
+
path: commandPath,
|
|
1637
|
+
metadata
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
return result;
|
|
1641
|
+
}
|
|
1642
|
+
// Extract tenfold metadata from file content
|
|
1643
|
+
extractMetadata(content) {
|
|
1644
|
+
const lines = content.split("\n");
|
|
1645
|
+
let inFrontmatter = false;
|
|
1646
|
+
for (const line of lines) {
|
|
1647
|
+
if (line.trim() === FRONTMATTER_START) {
|
|
1648
|
+
if (inFrontmatter) break;
|
|
1649
|
+
inFrontmatter = true;
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
if (inFrontmatter && line.startsWith(TENFOLD_MARKER)) {
|
|
1653
|
+
try {
|
|
1654
|
+
const jsonStr = line.slice(TENFOLD_MARKER.length).trim();
|
|
1655
|
+
return JSON.parse(jsonStr);
|
|
1656
|
+
} catch {
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
// Detect name conflict
|
|
1664
|
+
async detectConflict(commandContent) {
|
|
1665
|
+
const commandPath = join2(this.commandsDir, `${commandContent.name}.md`);
|
|
1666
|
+
if (!existsSync5(commandPath)) {
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
const content = await readFile5(commandPath, "utf-8");
|
|
1670
|
+
return this.extractMetadata(content);
|
|
1671
|
+
}
|
|
1672
|
+
// Replace an existing command
|
|
1673
|
+
async replace(commandContent, metadata) {
|
|
1674
|
+
await this.uninstallByName(commandContent.name);
|
|
1675
|
+
return this.install(commandContent, metadata);
|
|
1676
|
+
}
|
|
1677
|
+
// Rename a command (for conflict resolution)
|
|
1678
|
+
async rename(oldName, newName) {
|
|
1679
|
+
const oldPath = join2(this.commandsDir, `${oldName}.md`);
|
|
1680
|
+
const newPath = join2(this.commandsDir, `${newName}.md`);
|
|
1681
|
+
if (!existsSync5(oldPath)) {
|
|
1682
|
+
return false;
|
|
1683
|
+
}
|
|
1684
|
+
const content = await readFile5(oldPath, "utf-8");
|
|
1685
|
+
await writeFile5(newPath, content, "utf-8");
|
|
1686
|
+
await unlink(oldPath);
|
|
1687
|
+
return true;
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
var _manager5 = null;
|
|
1691
|
+
function getCommandsManager() {
|
|
1692
|
+
if (!_manager5) {
|
|
1693
|
+
_manager5 = new CommandsManager();
|
|
1694
|
+
}
|
|
1695
|
+
return _manager5;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// src/config/skills.ts
|
|
1699
|
+
import { writeFile as writeFile6, mkdir as mkdir5, rm, readdir as readdir2, readFile as readFile6 } from "fs/promises";
|
|
1700
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1701
|
+
import { join as join3 } from "path";
|
|
1702
|
+
var METADATA_FILE = ".tenfold.json";
|
|
1703
|
+
var SkillsManager = class {
|
|
1704
|
+
pluginsDir;
|
|
1705
|
+
constructor(pluginsDir) {
|
|
1706
|
+
this.pluginsDir = pluginsDir ?? getPluginsDir();
|
|
1707
|
+
}
|
|
1708
|
+
// Get skill directory for a slug
|
|
1709
|
+
getSkillDirForSlug(slug) {
|
|
1710
|
+
const dirName = slug.replace("/", "-");
|
|
1711
|
+
return join3(this.pluginsDir, dirName, "skills");
|
|
1712
|
+
}
|
|
1713
|
+
// Install a skill
|
|
1714
|
+
async installSkill(skillContent, metadata) {
|
|
1715
|
+
const skillDir = this.getSkillDirForSlug(metadata.slug);
|
|
1716
|
+
const skillPath = join3(skillDir, skillContent.name);
|
|
1717
|
+
if (!existsSync6(skillPath)) {
|
|
1718
|
+
await mkdir5(skillPath, { recursive: true });
|
|
1719
|
+
}
|
|
1720
|
+
const promptPath = join3(skillPath, "prompt.md");
|
|
1721
|
+
await writeFile6(promptPath, skillContent.prompt, "utf-8");
|
|
1722
|
+
if (skillContent.description) {
|
|
1723
|
+
const descPath = join3(skillPath, "description.txt");
|
|
1724
|
+
await writeFile6(descPath, skillContent.description, "utf-8");
|
|
1725
|
+
}
|
|
1726
|
+
const metadataPath = join3(skillPath, METADATA_FILE);
|
|
1727
|
+
await writeFile6(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
1728
|
+
return {
|
|
1729
|
+
type: "skill_dir",
|
|
1730
|
+
path: skillPath
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
// Install an agent (similar to skill but with system prompt)
|
|
1734
|
+
async installAgent(agentContent, metadata) {
|
|
1735
|
+
const agentDir = this.getSkillDirForSlug(metadata.slug);
|
|
1736
|
+
const agentPath = join3(agentDir, agentContent.name);
|
|
1737
|
+
if (!existsSync6(agentPath)) {
|
|
1738
|
+
await mkdir5(agentPath, { recursive: true });
|
|
1739
|
+
}
|
|
1740
|
+
const promptPath = join3(agentPath, "system-prompt.md");
|
|
1741
|
+
await writeFile6(promptPath, agentContent.systemPrompt, "utf-8");
|
|
1742
|
+
if (agentContent.description) {
|
|
1743
|
+
const descPath = join3(agentPath, "description.txt");
|
|
1744
|
+
await writeFile6(descPath, agentContent.description, "utf-8");
|
|
1745
|
+
}
|
|
1746
|
+
if (agentContent.tools && agentContent.tools.length > 0) {
|
|
1747
|
+
const toolsPath = join3(agentPath, "tools.json");
|
|
1748
|
+
await writeFile6(
|
|
1749
|
+
toolsPath,
|
|
1750
|
+
JSON.stringify(agentContent.tools, null, 2),
|
|
1751
|
+
"utf-8"
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
const metadataPath = join3(agentPath, METADATA_FILE);
|
|
1755
|
+
await writeFile6(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
1756
|
+
return {
|
|
1757
|
+
type: "skill_dir",
|
|
1758
|
+
path: agentPath
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
// Uninstall by slug (removes entire plugin directory)
|
|
1762
|
+
async uninstall(slug) {
|
|
1763
|
+
const dirName = slug.replace("/", "-");
|
|
1764
|
+
const pluginDir = join3(this.pluginsDir, dirName);
|
|
1765
|
+
if (!existsSync6(pluginDir)) {
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
await rm(pluginDir, { recursive: true, force: true });
|
|
1769
|
+
return true;
|
|
1770
|
+
}
|
|
1771
|
+
// Check if a slug is installed
|
|
1772
|
+
async isInstalled(slug) {
|
|
1773
|
+
const dirName = slug.replace("/", "-");
|
|
1774
|
+
const pluginDir = join3(this.pluginsDir, dirName);
|
|
1775
|
+
return existsSync6(pluginDir);
|
|
1776
|
+
}
|
|
1777
|
+
// Get all installed skills/agents with metadata
|
|
1778
|
+
async getInstalledSkills() {
|
|
1779
|
+
if (!existsSync6(this.pluginsDir)) {
|
|
1780
|
+
return [];
|
|
1781
|
+
}
|
|
1782
|
+
const result = [];
|
|
1783
|
+
const pluginDirs = await readdir2(this.pluginsDir);
|
|
1784
|
+
for (const dir of pluginDirs) {
|
|
1785
|
+
const pluginPath = join3(this.pluginsDir, dir);
|
|
1786
|
+
const skillsPath = join3(pluginPath, "skills");
|
|
1787
|
+
if (!existsSync6(skillsPath)) continue;
|
|
1788
|
+
const skillDirs = await readdir2(skillsPath);
|
|
1789
|
+
for (const skillDir of skillDirs) {
|
|
1790
|
+
const skillPath = join3(skillsPath, skillDir);
|
|
1791
|
+
const metadataPath = join3(skillPath, METADATA_FILE);
|
|
1792
|
+
let metadata = null;
|
|
1793
|
+
if (existsSync6(metadataPath)) {
|
|
1794
|
+
try {
|
|
1795
|
+
const content = await readFile6(metadataPath, "utf-8");
|
|
1796
|
+
metadata = JSON.parse(content);
|
|
1797
|
+
} catch {
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
const hasSystemPrompt = existsSync6(join3(skillPath, "system-prompt.md"));
|
|
1801
|
+
const type = hasSystemPrompt ? "agent" : "skill";
|
|
1802
|
+
result.push({
|
|
1803
|
+
slug: dir.replace("-", "/"),
|
|
1804
|
+
path: skillPath,
|
|
1805
|
+
metadata,
|
|
1806
|
+
type
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
return result;
|
|
1811
|
+
}
|
|
1812
|
+
// Detect conflict (check if skill/agent name already exists)
|
|
1813
|
+
async detectConflict(content, slug) {
|
|
1814
|
+
const skillDir = this.getSkillDirForSlug(slug);
|
|
1815
|
+
const skillPath = join3(skillDir, content.name);
|
|
1816
|
+
const metadataPath = join3(skillPath, METADATA_FILE);
|
|
1817
|
+
if (!existsSync6(metadataPath)) {
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
try {
|
|
1821
|
+
const metadataContent = await readFile6(metadataPath, "utf-8");
|
|
1822
|
+
return JSON.parse(metadataContent);
|
|
1823
|
+
} catch {
|
|
1824
|
+
return null;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
// Replace existing skill/agent
|
|
1828
|
+
async replace(content, metadata, type) {
|
|
1829
|
+
const skillDir = this.getSkillDirForSlug(metadata.slug);
|
|
1830
|
+
const skillPath = join3(skillDir, content.name);
|
|
1831
|
+
if (existsSync6(skillPath)) {
|
|
1832
|
+
await rm(skillPath, { recursive: true, force: true });
|
|
1833
|
+
}
|
|
1834
|
+
if (type === "agent") {
|
|
1835
|
+
return this.installAgent(content, metadata);
|
|
1836
|
+
}
|
|
1837
|
+
return this.installSkill(content, metadata);
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
var _manager6 = null;
|
|
1841
|
+
function getSkillsManager() {
|
|
1842
|
+
if (!_manager6) {
|
|
1843
|
+
_manager6 = new SkillsManager();
|
|
1844
|
+
}
|
|
1845
|
+
return _manager6;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// src/config/manager.ts
|
|
1849
|
+
var ConfigManager = class {
|
|
1850
|
+
installed;
|
|
1851
|
+
settings;
|
|
1852
|
+
mcp;
|
|
1853
|
+
claudeMd;
|
|
1854
|
+
commands;
|
|
1855
|
+
skills;
|
|
1856
|
+
constructor() {
|
|
1857
|
+
this.installed = getInstalledManager();
|
|
1858
|
+
this.settings = getSettingsManager();
|
|
1859
|
+
this.mcp = getMcpManager();
|
|
1860
|
+
this.claudeMd = getClaudeMdManager();
|
|
1861
|
+
this.commands = getCommandsManager();
|
|
1862
|
+
this.skills = getSkillsManager();
|
|
1863
|
+
}
|
|
1864
|
+
// Create metadata from component and version
|
|
1865
|
+
createMetadata(component, version) {
|
|
1866
|
+
return {
|
|
1867
|
+
componentId: component.id,
|
|
1868
|
+
versionId: version.id,
|
|
1869
|
+
version: version.version,
|
|
1870
|
+
author: component.author.name,
|
|
1871
|
+
slug: `${component.author.githubUsername}/${component.slug}`,
|
|
1872
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
// Detect conflicts for a component version
|
|
1876
|
+
async detectConflicts(component, version) {
|
|
1877
|
+
const conflicts = [];
|
|
1878
|
+
const content = version.content;
|
|
1879
|
+
const slug = `${component.author.githubUsername}/${component.slug}`;
|
|
1880
|
+
switch (component.type) {
|
|
1881
|
+
case "HOOK": {
|
|
1882
|
+
const hookContent = content;
|
|
1883
|
+
const existing = await this.settings.detectConflict(hookContent);
|
|
1884
|
+
if (existing && existing._tenfold?.slug !== slug) {
|
|
1885
|
+
conflicts.push({
|
|
1886
|
+
type: "hook_duplicate",
|
|
1887
|
+
severity: "blocking",
|
|
1888
|
+
existing: {
|
|
1889
|
+
source: existing._tenfold?.slug || "unknown",
|
|
1890
|
+
version: existing._tenfold?.version || "unknown"
|
|
1891
|
+
},
|
|
1892
|
+
incoming: {
|
|
1893
|
+
source: slug,
|
|
1894
|
+
version: version.version
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
break;
|
|
1899
|
+
}
|
|
1900
|
+
case "MCP_SERVER": {
|
|
1901
|
+
const mcpContent = content;
|
|
1902
|
+
const existing = await this.mcp.detectConflict(mcpContent);
|
|
1903
|
+
if (existing && existing._tenfold?.slug !== slug) {
|
|
1904
|
+
conflicts.push({
|
|
1905
|
+
type: "mcp_server_name",
|
|
1906
|
+
severity: "blocking",
|
|
1907
|
+
existing: {
|
|
1908
|
+
source: existing._tenfold?.slug || "user-defined",
|
|
1909
|
+
version: existing._tenfold?.version || "unknown"
|
|
1910
|
+
},
|
|
1911
|
+
incoming: {
|
|
1912
|
+
source: slug,
|
|
1913
|
+
version: version.version
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
break;
|
|
1918
|
+
}
|
|
1919
|
+
case "INSTRUCTION": {
|
|
1920
|
+
const hasConflict = await this.claudeMd.detectConflict(slug);
|
|
1921
|
+
if (hasConflict) {
|
|
1922
|
+
conflicts.push({
|
|
1923
|
+
type: "claude_md_section",
|
|
1924
|
+
severity: "warning",
|
|
1925
|
+
existing: {
|
|
1926
|
+
source: slug,
|
|
1927
|
+
version: "current"
|
|
1928
|
+
},
|
|
1929
|
+
incoming: {
|
|
1930
|
+
source: slug,
|
|
1931
|
+
version: version.version
|
|
1932
|
+
}
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
break;
|
|
1936
|
+
}
|
|
1937
|
+
case "COMMAND": {
|
|
1938
|
+
const cmdContent = content;
|
|
1939
|
+
const existing = await this.commands.detectConflict(cmdContent);
|
|
1940
|
+
if (existing && existing.slug !== slug) {
|
|
1941
|
+
conflicts.push({
|
|
1942
|
+
type: "command_slug",
|
|
1943
|
+
severity: "blocking",
|
|
1944
|
+
existing: {
|
|
1945
|
+
source: existing.slug,
|
|
1946
|
+
version: existing.version
|
|
1947
|
+
},
|
|
1948
|
+
incoming: {
|
|
1949
|
+
source: slug,
|
|
1950
|
+
version: version.version
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
case "SKILL":
|
|
1957
|
+
case "AGENT": {
|
|
1958
|
+
const skillContent = content;
|
|
1959
|
+
const existing = await this.skills.detectConflict(skillContent, slug);
|
|
1960
|
+
if (existing && existing.slug !== slug) {
|
|
1961
|
+
conflicts.push({
|
|
1962
|
+
type: "command_slug",
|
|
1963
|
+
// Reuse type for skills
|
|
1964
|
+
severity: "blocking",
|
|
1965
|
+
existing: {
|
|
1966
|
+
source: existing.slug,
|
|
1967
|
+
version: existing.version
|
|
1968
|
+
},
|
|
1969
|
+
incoming: {
|
|
1970
|
+
source: slug,
|
|
1971
|
+
version: version.version
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
break;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return conflicts;
|
|
1979
|
+
}
|
|
1980
|
+
// Install a component
|
|
1981
|
+
async installComponent(component, version, options = {}) {
|
|
1982
|
+
const metadata = this.createMetadata(component, version);
|
|
1983
|
+
const slug = metadata.slug;
|
|
1984
|
+
const content = version.content;
|
|
1985
|
+
const targetFiles = [];
|
|
1986
|
+
if (options.dryRun) {
|
|
1987
|
+
return {
|
|
1988
|
+
success: true,
|
|
1989
|
+
component: {
|
|
1990
|
+
slug,
|
|
1991
|
+
type: component.type,
|
|
1992
|
+
version: version.version,
|
|
1993
|
+
author: component.author.name
|
|
1994
|
+
},
|
|
1995
|
+
targetFiles: []
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
switch (component.type) {
|
|
1999
|
+
case "HOOK": {
|
|
2000
|
+
const hookContent = content;
|
|
2001
|
+
const target = await this.settings.installHook(hookContent, metadata);
|
|
2002
|
+
targetFiles.push(target);
|
|
2003
|
+
break;
|
|
2004
|
+
}
|
|
2005
|
+
case "MCP_SERVER": {
|
|
2006
|
+
const mcpContent = content;
|
|
2007
|
+
const target = await this.mcp.installServer(mcpContent, metadata);
|
|
2008
|
+
targetFiles.push(target);
|
|
2009
|
+
break;
|
|
2010
|
+
}
|
|
2011
|
+
case "INSTRUCTION": {
|
|
2012
|
+
const instructionContent = content;
|
|
2013
|
+
const target = await this.claudeMd.install(
|
|
2014
|
+
instructionContent,
|
|
2015
|
+
metadata,
|
|
2016
|
+
options.claudeMdStrategy || "section"
|
|
2017
|
+
);
|
|
2018
|
+
targetFiles.push(target);
|
|
2019
|
+
break;
|
|
2020
|
+
}
|
|
2021
|
+
case "COMMAND": {
|
|
2022
|
+
const cmdContent = content;
|
|
2023
|
+
const target = await this.commands.install(cmdContent, metadata);
|
|
2024
|
+
targetFiles.push(target);
|
|
2025
|
+
break;
|
|
2026
|
+
}
|
|
2027
|
+
case "SKILL": {
|
|
2028
|
+
const skillContent = content;
|
|
2029
|
+
const target = await this.skills.installSkill(skillContent, metadata);
|
|
2030
|
+
targetFiles.push(target);
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
2033
|
+
case "AGENT": {
|
|
2034
|
+
const agentContent = content;
|
|
2035
|
+
const target = await this.skills.installAgent(agentContent, metadata);
|
|
2036
|
+
targetFiles.push(target);
|
|
2037
|
+
break;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
await this.installed.track(
|
|
2041
|
+
{
|
|
2042
|
+
componentId: component.id,
|
|
2043
|
+
versionId: version.id,
|
|
2044
|
+
author: component.author.name,
|
|
2045
|
+
slug,
|
|
2046
|
+
type: component.type,
|
|
2047
|
+
version: version.version
|
|
2048
|
+
},
|
|
2049
|
+
targetFiles,
|
|
2050
|
+
version.attributionSnapshot?.attributions
|
|
2051
|
+
);
|
|
2052
|
+
return {
|
|
2053
|
+
success: true,
|
|
2054
|
+
component: {
|
|
2055
|
+
slug,
|
|
2056
|
+
type: component.type,
|
|
2057
|
+
version: version.version,
|
|
2058
|
+
author: component.author.name
|
|
2059
|
+
},
|
|
2060
|
+
targetFiles
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
// Uninstall a component
|
|
2064
|
+
async uninstallComponent(slug) {
|
|
2065
|
+
const installed = await this.installed.get(slug);
|
|
2066
|
+
if (!installed) {
|
|
2067
|
+
return false;
|
|
2068
|
+
}
|
|
2069
|
+
switch (installed.type) {
|
|
2070
|
+
case "HOOK":
|
|
2071
|
+
await this.settings.uninstallHook(slug);
|
|
2072
|
+
break;
|
|
2073
|
+
case "MCP_SERVER":
|
|
2074
|
+
await this.mcp.uninstallBySlug(slug);
|
|
2075
|
+
break;
|
|
2076
|
+
case "INSTRUCTION":
|
|
2077
|
+
await this.claudeMd.uninstall(slug);
|
|
2078
|
+
break;
|
|
2079
|
+
case "COMMAND":
|
|
2080
|
+
await this.commands.uninstall(slug);
|
|
2081
|
+
break;
|
|
2082
|
+
case "SKILL":
|
|
2083
|
+
case "AGENT":
|
|
2084
|
+
await this.skills.uninstall(slug);
|
|
2085
|
+
break;
|
|
2086
|
+
}
|
|
2087
|
+
await this.installed.remove(slug);
|
|
2088
|
+
return true;
|
|
2089
|
+
}
|
|
2090
|
+
// Get all installed components
|
|
2091
|
+
async getInstalledComponents() {
|
|
2092
|
+
return this.installed.getAll();
|
|
2093
|
+
}
|
|
2094
|
+
// Check if component is installed
|
|
2095
|
+
async isInstalled(slug) {
|
|
2096
|
+
return this.installed.isInstalled(slug);
|
|
2097
|
+
}
|
|
2098
|
+
// Get installed version
|
|
2099
|
+
async getInstalledVersion(slug) {
|
|
2100
|
+
return this.installed.getInstalledVersion(slug);
|
|
2101
|
+
}
|
|
2102
|
+
// Get hook definition for run-hook command
|
|
2103
|
+
async getHookDefinition(slug) {
|
|
2104
|
+
return this.settings.getHookDefinition(slug);
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
var _manager7 = null;
|
|
2108
|
+
function getConfigManager() {
|
|
2109
|
+
if (!_manager7) {
|
|
2110
|
+
_manager7 = new ConfigManager();
|
|
2111
|
+
}
|
|
2112
|
+
return _manager7;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// src/conflict/detector.ts
|
|
2116
|
+
var ConflictDetector = class {
|
|
2117
|
+
// Detect all conflicts for a component version
|
|
2118
|
+
async detect(component, version) {
|
|
2119
|
+
const configManager = getConfigManager();
|
|
2120
|
+
const conflicts = await configManager.detectConflicts(component, version);
|
|
2121
|
+
const blocking = conflicts.filter((c) => c.severity === "blocking");
|
|
2122
|
+
const warnings = conflicts.filter((c) => c.severity === "warning");
|
|
2123
|
+
return {
|
|
2124
|
+
hasConflicts: conflicts.length > 0,
|
|
2125
|
+
blocking,
|
|
2126
|
+
warnings
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
// Check if any conflicts require user intervention
|
|
2130
|
+
hasBlockingConflicts(result) {
|
|
2131
|
+
return result.blocking.length > 0;
|
|
2132
|
+
}
|
|
2133
|
+
// Format conflict for display
|
|
2134
|
+
formatConflict(conflict) {
|
|
2135
|
+
const typeLabels = {
|
|
2136
|
+
mcp_server_name: "MCP Server name collision",
|
|
2137
|
+
hook_duplicate: "Hook event+matcher conflict",
|
|
2138
|
+
command_slug: "Command name collision",
|
|
2139
|
+
claude_md_section: "CLAUDE.md section exists"
|
|
2140
|
+
};
|
|
2141
|
+
return `${typeLabels[conflict.type] || conflict.type}: ${conflict.existing.source} vs ${conflict.incoming.source}`;
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
var _detector = null;
|
|
2145
|
+
function getConflictDetector() {
|
|
2146
|
+
if (!_detector) {
|
|
2147
|
+
_detector = new ConflictDetector();
|
|
2148
|
+
}
|
|
2149
|
+
return _detector;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// src/ui/prompt.ts
|
|
2153
|
+
import { select, confirm, input } from "@inquirer/prompts";
|
|
2154
|
+
async function confirmPrompt(message, defaultValue = false) {
|
|
2155
|
+
return confirm({
|
|
2156
|
+
message,
|
|
2157
|
+
default: defaultValue
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
async function selectPrompt(message, choices) {
|
|
2161
|
+
return select({
|
|
2162
|
+
message,
|
|
2163
|
+
choices: choices.map((c) => ({
|
|
2164
|
+
name: c.name,
|
|
2165
|
+
value: c.value,
|
|
2166
|
+
description: c.description
|
|
2167
|
+
}))
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
async function inputPrompt(message, options) {
|
|
2171
|
+
return input({
|
|
2172
|
+
message,
|
|
2173
|
+
default: options?.default,
|
|
2174
|
+
validate: options?.validate
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
async function conflictResolutionPrompt(existing, incoming) {
|
|
2178
|
+
console.log(colors.warning("\n \u26A0 Conflict detected!"));
|
|
2179
|
+
console.log(muted(` Current: ${existing}`));
|
|
2180
|
+
console.log(muted(` Incoming: ${incoming}
|
|
2181
|
+
`));
|
|
2182
|
+
return selectPrompt(
|
|
2183
|
+
"How would you like to resolve this?",
|
|
2184
|
+
[
|
|
2185
|
+
{ name: `Replace with ${incoming}`, value: "replace" },
|
|
2186
|
+
{ name: `Keep ${existing} (skip)`, value: "keep" },
|
|
2187
|
+
{ name: "Rename incoming", value: "rename" },
|
|
2188
|
+
{ name: "View diff", value: "diff" }
|
|
2189
|
+
]
|
|
2190
|
+
);
|
|
2191
|
+
}
|
|
2192
|
+
async function claudeMdStrategyPrompt() {
|
|
2193
|
+
return selectPrompt(
|
|
2194
|
+
"How should CLAUDE.md content be added?",
|
|
2195
|
+
[
|
|
2196
|
+
{
|
|
2197
|
+
name: "Append to existing CLAUDE.md",
|
|
2198
|
+
value: "append",
|
|
2199
|
+
description: "Add content at the end of the file"
|
|
2200
|
+
},
|
|
2201
|
+
{
|
|
2202
|
+
name: "Replace existing CLAUDE.md",
|
|
2203
|
+
value: "replace",
|
|
2204
|
+
description: "Overwrite the entire file"
|
|
2205
|
+
},
|
|
2206
|
+
{
|
|
2207
|
+
name: "Add as separate section with source marker",
|
|
2208
|
+
value: "section",
|
|
2209
|
+
description: "Add with markers for easy identification"
|
|
2210
|
+
}
|
|
2211
|
+
]
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
async function installBundlePrompt(bundleName, itemCount) {
|
|
2215
|
+
return confirmPrompt(
|
|
2216
|
+
`Install all ${itemCount} components from ${bundleName}?`,
|
|
2217
|
+
true
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
async function renamePrompt(currentName, suggestedName) {
|
|
2221
|
+
return inputPrompt(`Enter new name (current: ${currentName}):`, {
|
|
2222
|
+
default: suggestedName,
|
|
2223
|
+
validate: (value) => {
|
|
2224
|
+
if (!value || value.length < 1) {
|
|
2225
|
+
return "Name cannot be empty";
|
|
2226
|
+
}
|
|
2227
|
+
if (!/^[a-z0-9_-]+$/i.test(value)) {
|
|
2228
|
+
return "Name can only contain letters, numbers, hyphens, and underscores";
|
|
2229
|
+
}
|
|
2230
|
+
return true;
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// src/ui/box.ts
|
|
2236
|
+
import boxen from "boxen";
|
|
2237
|
+
var defaultBoxOptions = {
|
|
2238
|
+
padding: 1,
|
|
2239
|
+
margin: { top: 1, bottom: 1, left: 2, right: 2 },
|
|
2240
|
+
borderStyle: "round"
|
|
2241
|
+
};
|
|
2242
|
+
function installHeaderBox(component, version) {
|
|
2243
|
+
const icon = typeIcons[component.type] || "\u{1F4E6}";
|
|
2244
|
+
const content = [
|
|
2245
|
+
`${icon} Installing: ${brandGradient(component.slug)} v${version.version}`,
|
|
2246
|
+
colors.muted(` Author: ${component.author.name} (@${component.author.githubUsername})`)
|
|
2247
|
+
].join("\n");
|
|
2248
|
+
return boxen(content, {
|
|
2249
|
+
...defaultBoxOptions,
|
|
2250
|
+
borderColor: "magenta"
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
function bundleInstallHeaderBox(bundle) {
|
|
2254
|
+
const content = [
|
|
2255
|
+
`\u{1F4E6} Installing bundle: ${brandGradient(bundle.slug)}`,
|
|
2256
|
+
colors.muted(` Author: ${bundle.author.name} (@${bundle.author.githubUsername})`)
|
|
2257
|
+
].join("\n");
|
|
2258
|
+
return boxen(content, {
|
|
2259
|
+
...defaultBoxOptions,
|
|
2260
|
+
borderColor: "magenta"
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
function conflictBox(existing, incoming) {
|
|
2264
|
+
const content = [
|
|
2265
|
+
colors.warning("\u26A0 Conflict detected!"),
|
|
2266
|
+
"",
|
|
2267
|
+
`Current: ${existing.source} v${existing.version}`,
|
|
2268
|
+
`Incoming: ${incoming.source} v${incoming.version}`
|
|
2269
|
+
].join("\n");
|
|
2270
|
+
return boxen(content, {
|
|
2271
|
+
...defaultBoxOptions,
|
|
2272
|
+
borderColor: "yellow"
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// src/conflict/resolver.ts
|
|
2277
|
+
var ConflictResolver = class {
|
|
2278
|
+
// Resolve a single conflict interactively
|
|
2279
|
+
async resolve(conflict) {
|
|
2280
|
+
console.log(
|
|
2281
|
+
conflictBox(conflict.existing, conflict.incoming)
|
|
2282
|
+
);
|
|
2283
|
+
const resolution = await conflictResolutionPrompt(
|
|
2284
|
+
`${conflict.existing.source} v${conflict.existing.version}`,
|
|
2285
|
+
`${conflict.incoming.source} v${conflict.incoming.version}`
|
|
2286
|
+
);
|
|
2287
|
+
if (resolution === "diff") {
|
|
2288
|
+
await this.showDiff(conflict);
|
|
2289
|
+
return this.resolve(conflict);
|
|
2290
|
+
}
|
|
2291
|
+
if (resolution === "rename") {
|
|
2292
|
+
const suggestedName = this.suggestRename(conflict);
|
|
2293
|
+
const newName = await renamePrompt(
|
|
2294
|
+
conflict.incoming.source.split("/")[1] || conflict.incoming.source,
|
|
2295
|
+
suggestedName
|
|
2296
|
+
);
|
|
2297
|
+
return { conflict, resolution, newName };
|
|
2298
|
+
}
|
|
2299
|
+
return { conflict, resolution };
|
|
2300
|
+
}
|
|
2301
|
+
// Resolve all conflicts
|
|
2302
|
+
async resolveAll(conflicts) {
|
|
2303
|
+
const resolved = [];
|
|
2304
|
+
for (const conflict of conflicts) {
|
|
2305
|
+
const result = await this.resolve(conflict);
|
|
2306
|
+
resolved.push(result);
|
|
2307
|
+
}
|
|
2308
|
+
return resolved;
|
|
2309
|
+
}
|
|
2310
|
+
// Show diff between existing and incoming
|
|
2311
|
+
async showDiff(conflict) {
|
|
2312
|
+
console.log(muted("\n Diff view:"));
|
|
2313
|
+
console.log(muted(` - Current: ${conflict.existing.source} v${conflict.existing.version}`));
|
|
2314
|
+
console.log(muted(` + Incoming: ${conflict.incoming.source} v${conflict.incoming.version}`));
|
|
2315
|
+
console.log(muted("\n (Detailed diff not yet implemented)\n"));
|
|
2316
|
+
}
|
|
2317
|
+
// Suggest a new name for rename resolution
|
|
2318
|
+
suggestRename(conflict) {
|
|
2319
|
+
const incomingName = conflict.incoming.source.split("/")[1] || conflict.incoming.source;
|
|
2320
|
+
const author = conflict.incoming.source.split("/")[0] || "custom";
|
|
2321
|
+
return `${incomingName}-${author}`;
|
|
2322
|
+
}
|
|
2323
|
+
// Check if resolution is to skip installation
|
|
2324
|
+
isSkip(resolution) {
|
|
2325
|
+
return resolution === "keep";
|
|
2326
|
+
}
|
|
2327
|
+
// Check if resolution is to replace
|
|
2328
|
+
isReplace(resolution) {
|
|
2329
|
+
return resolution === "replace";
|
|
2330
|
+
}
|
|
2331
|
+
// Check if resolution is to rename
|
|
2332
|
+
isRename(resolution) {
|
|
2333
|
+
return resolution === "rename";
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
var _resolver = null;
|
|
2337
|
+
function getConflictResolver() {
|
|
2338
|
+
if (!_resolver) {
|
|
2339
|
+
_resolver = new ConflictResolver();
|
|
2340
|
+
}
|
|
2341
|
+
return _resolver;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
// src/ui/links.ts
|
|
2345
|
+
import terminalLink from "terminal-link";
|
|
2346
|
+
var BASE_URL = process.env.TENFOLD_WEB_URL ?? "https://tenfold.dev";
|
|
2347
|
+
function link(text, url) {
|
|
2348
|
+
return terminalLink(text, url, {
|
|
2349
|
+
fallback: (text2, url2) => `${text2} (${muted(url2)})`
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
function componentLink(slug) {
|
|
2353
|
+
const url = `${BASE_URL}/components/${slug}`;
|
|
2354
|
+
return link(colors.info("View online \u2192"), url);
|
|
2355
|
+
}
|
|
2356
|
+
function bundleLink(slug) {
|
|
2357
|
+
const url = `${BASE_URL}/bundles/${slug}`;
|
|
2358
|
+
return link(colors.info("View online \u2192"), url);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// src/commands/install.ts
|
|
2362
|
+
function createInstallCommand() {
|
|
2363
|
+
return new Command3("install").description("Install a component or bundle").argument("<slug>", "Component or bundle to install (author/name)").option("-f, --force", "Override conflicts without prompting").option("--dry-run", "Show what would be installed without making changes").option("-v, --version <version>", "Install specific version").action(
|
|
2364
|
+
async (slug, options) => {
|
|
2365
|
+
if (!isValidSlug(slug)) {
|
|
2366
|
+
console.error(
|
|
2367
|
+
colors.error(
|
|
2368
|
+
`
|
|
2369
|
+
Invalid slug format: "${slug}". Expected "author/name".
|
|
2370
|
+
`
|
|
2371
|
+
)
|
|
2372
|
+
);
|
|
2373
|
+
process.exit(1);
|
|
2374
|
+
}
|
|
2375
|
+
const api = getApiClient();
|
|
2376
|
+
const configManager = getConfigManager();
|
|
2377
|
+
const detector = getConflictDetector();
|
|
2378
|
+
const resolver = getConflictResolver();
|
|
2379
|
+
let isBundle = false;
|
|
2380
|
+
let component = null;
|
|
2381
|
+
let bundle = null;
|
|
2382
|
+
try {
|
|
2383
|
+
component = await api.getComponent(slug);
|
|
2384
|
+
} catch (error) {
|
|
2385
|
+
if (error instanceof NotFoundError) {
|
|
2386
|
+
try {
|
|
2387
|
+
bundle = await api.getBundle(slug);
|
|
2388
|
+
isBundle = true;
|
|
2389
|
+
} catch {
|
|
2390
|
+
console.error(colors.error(`
|
|
2391
|
+
Not found: ${slug}
|
|
2392
|
+
`));
|
|
2393
|
+
const similar = await api.searchComponents(
|
|
2394
|
+
slug.split("/")[1] || slug,
|
|
2395
|
+
3
|
|
2396
|
+
);
|
|
2397
|
+
if (similar.length > 0) {
|
|
2398
|
+
console.log(muted(" Did you mean:"));
|
|
2399
|
+
for (const s of similar) {
|
|
2400
|
+
console.log(muted(` - ${s.author.githubUsername}/${s.slug}`));
|
|
2401
|
+
}
|
|
2402
|
+
console.log();
|
|
2403
|
+
}
|
|
2404
|
+
process.exit(1);
|
|
2405
|
+
}
|
|
2406
|
+
} else {
|
|
2407
|
+
throw error;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
if (isBundle && bundle) {
|
|
2411
|
+
await installBundle(bundle, api, configManager, options);
|
|
2412
|
+
} else if (component) {
|
|
2413
|
+
await installComponent(
|
|
2414
|
+
component,
|
|
2415
|
+
api,
|
|
2416
|
+
configManager,
|
|
2417
|
+
detector,
|
|
2418
|
+
resolver,
|
|
2419
|
+
options
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2425
|
+
async function installComponent(component, api, configManager, detector, resolver, options) {
|
|
2426
|
+
const version = component.versions[0];
|
|
2427
|
+
if (!version) {
|
|
2428
|
+
console.error(colors.error(`
|
|
2429
|
+
No versions available for ${component.slug}
|
|
2430
|
+
`));
|
|
2431
|
+
process.exit(1);
|
|
2432
|
+
}
|
|
2433
|
+
console.log(installHeaderBox(component, version));
|
|
2434
|
+
let strategy = "section";
|
|
2435
|
+
if (component.type === "INSTRUCTION" && !options.force) {
|
|
2436
|
+
strategy = await claudeMdStrategyPrompt();
|
|
2437
|
+
}
|
|
2438
|
+
const tasks = new Listr(
|
|
2439
|
+
[
|
|
2440
|
+
{
|
|
2441
|
+
title: "Checking for conflicts",
|
|
2442
|
+
task: async (ctx) => {
|
|
2443
|
+
const result = await detector.detect(component, version);
|
|
2444
|
+
ctx.conflicts = result;
|
|
2445
|
+
if (result.blocking.length > 0 && !options.force) {
|
|
2446
|
+
throw new Error("CONFLICTS_DETECTED");
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
},
|
|
2450
|
+
{
|
|
2451
|
+
title: options.dryRun ? "Dry run - no changes made" : "Installing component",
|
|
2452
|
+
skip: (ctx) => {
|
|
2453
|
+
if (ctx.conflicts?.blocking.length && !options.force) {
|
|
2454
|
+
return "Blocked by conflicts";
|
|
2455
|
+
}
|
|
2456
|
+
return false;
|
|
2457
|
+
},
|
|
2458
|
+
task: async (ctx) => {
|
|
2459
|
+
if (options.dryRun) {
|
|
2460
|
+
ctx.installed = false;
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
const result = await configManager.installComponent(
|
|
2464
|
+
component,
|
|
2465
|
+
version,
|
|
2466
|
+
{ claudeMdStrategy: strategy }
|
|
2467
|
+
);
|
|
2468
|
+
ctx.installed = result.success;
|
|
2469
|
+
ctx.results = [
|
|
2470
|
+
{
|
|
2471
|
+
component: component.slug,
|
|
2472
|
+
type: component.type,
|
|
2473
|
+
status: "Added"
|
|
2474
|
+
}
|
|
2475
|
+
];
|
|
2476
|
+
}
|
|
2477
|
+
},
|
|
2478
|
+
{
|
|
2479
|
+
title: "Tracking installation",
|
|
2480
|
+
skip: () => !!options.dryRun,
|
|
2481
|
+
task: async () => {
|
|
2482
|
+
try {
|
|
2483
|
+
await api.trackComponentInstall(
|
|
2484
|
+
`${component.author.githubUsername}/${component.slug}`,
|
|
2485
|
+
version.id
|
|
2486
|
+
);
|
|
2487
|
+
} catch {
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
],
|
|
2492
|
+
{
|
|
2493
|
+
exitOnError: false,
|
|
2494
|
+
rendererOptions: {
|
|
2495
|
+
collapseErrors: false
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
);
|
|
2499
|
+
try {
|
|
2500
|
+
const ctx = await tasks.run();
|
|
2501
|
+
if (ctx.conflicts?.blocking.length && !options.force) {
|
|
2502
|
+
console.log();
|
|
2503
|
+
for (const conflict of ctx.conflicts.blocking) {
|
|
2504
|
+
const resolved = await resolver.resolve(conflict);
|
|
2505
|
+
if (resolver.isSkip(resolved.resolution)) {
|
|
2506
|
+
console.log(muted(` Skipped: ${component.slug}
|
|
2507
|
+
`));
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
if (resolver.isReplace(resolved.resolution)) {
|
|
2511
|
+
await configManager.uninstallComponent(conflict.existing.source);
|
|
2512
|
+
const result = await configManager.installComponent(
|
|
2513
|
+
component,
|
|
2514
|
+
version,
|
|
2515
|
+
{ claudeMdStrategy: strategy }
|
|
2516
|
+
);
|
|
2517
|
+
ctx.installed = result.success;
|
|
2518
|
+
ctx.results = [
|
|
2519
|
+
{
|
|
2520
|
+
component: component.slug,
|
|
2521
|
+
type: component.type,
|
|
2522
|
+
status: "Replaced"
|
|
2523
|
+
}
|
|
2524
|
+
];
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
if (ctx.installed && ctx.results) {
|
|
2529
|
+
console.log(installResultTable(ctx.results));
|
|
2530
|
+
console.log();
|
|
2531
|
+
const attribution = version.attributionSnapshot?.attributions;
|
|
2532
|
+
if (attribution && attribution.length > 1) {
|
|
2533
|
+
console.log(muted(" Attribution:"));
|
|
2534
|
+
console.log(attributionTable(attribution));
|
|
2535
|
+
console.log();
|
|
2536
|
+
}
|
|
2537
|
+
console.log(
|
|
2538
|
+
` ${componentLink(`${component.author.githubUsername}/${component.slug}`)}
|
|
2539
|
+
`
|
|
2540
|
+
);
|
|
2541
|
+
}
|
|
2542
|
+
} catch (error) {
|
|
2543
|
+
if (error.message !== "CONFLICTS_DETECTED") {
|
|
2544
|
+
console.error(colors.error(`
|
|
2545
|
+
${formatError(error)}
|
|
2546
|
+
`));
|
|
2547
|
+
process.exit(1);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
async function installBundle(bundle, api, configManager, options) {
|
|
2552
|
+
console.log(bundleInstallHeaderBox(bundle));
|
|
2553
|
+
console.log(muted(" Components included:\n"));
|
|
2554
|
+
console.log(bundleItemsTable(bundle));
|
|
2555
|
+
console.log();
|
|
2556
|
+
if (!options.force) {
|
|
2557
|
+
const confirm2 = await installBundlePrompt(
|
|
2558
|
+
bundle.displayName,
|
|
2559
|
+
bundle.items.length
|
|
2560
|
+
);
|
|
2561
|
+
if (!confirm2) {
|
|
2562
|
+
console.log(muted("\n Installation cancelled.\n"));
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
const results = [];
|
|
2567
|
+
for (const item of bundle.items) {
|
|
2568
|
+
const component = item.componentVersion.component;
|
|
2569
|
+
const version = item.componentVersion;
|
|
2570
|
+
try {
|
|
2571
|
+
if (!options.dryRun) {
|
|
2572
|
+
await configManager.installComponent(component, version);
|
|
2573
|
+
}
|
|
2574
|
+
results.push({
|
|
2575
|
+
component: `${component.author.githubUsername}/${component.slug}`,
|
|
2576
|
+
type: component.type,
|
|
2577
|
+
status: options.dryRun ? "Would add" : "Added"
|
|
2578
|
+
});
|
|
2579
|
+
} catch {
|
|
2580
|
+
results.push({
|
|
2581
|
+
component: `${component.author.githubUsername}/${component.slug}`,
|
|
2582
|
+
type: component.type,
|
|
2583
|
+
status: "Failed"
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
if (!options.dryRun) {
|
|
2588
|
+
try {
|
|
2589
|
+
await api.trackBundleInstall(
|
|
2590
|
+
`${bundle.author.githubUsername}/${bundle.slug}`
|
|
2591
|
+
);
|
|
2592
|
+
} catch {
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
console.log(success(`
|
|
2596
|
+
${options.dryRun ? "Would install" : "Installed"} bundle: ${bundle.slug}
|
|
2597
|
+
`));
|
|
2598
|
+
console.log(installResultTable(results));
|
|
2599
|
+
console.log();
|
|
2600
|
+
console.log(
|
|
2601
|
+
` ${bundleLink(`${bundle.author.githubUsername}/${bundle.slug}`)}
|
|
2602
|
+
`
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// src/commands/list.ts
|
|
2607
|
+
import { Command as Command4 } from "commander";
|
|
2608
|
+
function createListCommand() {
|
|
2609
|
+
return new Command4("list").alias("ls").description("List installed components").option("-t, --type <type>", "Filter by type (hook, skill, mcp_server, etc.)").action(async (options) => {
|
|
2610
|
+
const manager = getInstalledManager();
|
|
2611
|
+
try {
|
|
2612
|
+
let components = await manager.getAll();
|
|
2613
|
+
if (options.type) {
|
|
2614
|
+
const typeFilter = options.type.toUpperCase().replace("-", "_");
|
|
2615
|
+
components = components.filter((c) => c.type === typeFilter);
|
|
2616
|
+
}
|
|
2617
|
+
if (components.length === 0) {
|
|
2618
|
+
if (options.type) {
|
|
2619
|
+
console.log(
|
|
2620
|
+
muted(`
|
|
2621
|
+
No ${options.type} components installed.
|
|
2622
|
+
`)
|
|
2623
|
+
);
|
|
2624
|
+
} else {
|
|
2625
|
+
console.log(muted("\n No components installed.\n"));
|
|
2626
|
+
console.log(muted(" Get started:"));
|
|
2627
|
+
console.log(muted(" tenfold search <query>"));
|
|
2628
|
+
console.log(muted(" tenfold install <author>/<component>\n"));
|
|
2629
|
+
}
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
console.log(bold("\n Installed Components\n"));
|
|
2633
|
+
console.log(installedTable(components));
|
|
2634
|
+
console.log();
|
|
2635
|
+
console.log(muted(` ${components.length} component(s) installed
|
|
2636
|
+
`));
|
|
2637
|
+
} catch (error) {
|
|
2638
|
+
console.error(colors.error(`
|
|
2639
|
+
${formatError(error)}
|
|
2640
|
+
`));
|
|
2641
|
+
process.exit(1);
|
|
2642
|
+
}
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// src/commands/uninstall.ts
|
|
2647
|
+
import { Command as Command5 } from "commander";
|
|
2648
|
+
function createUninstallCommand() {
|
|
2649
|
+
return new Command5("uninstall").alias("rm").description("Uninstall a component").argument("<slug>", "Component to uninstall (author/name)").option("-f, --force", "Skip confirmation prompt").action(async (slug, options) => {
|
|
2650
|
+
if (!isValidSlug(slug)) {
|
|
2651
|
+
console.error(
|
|
2652
|
+
colors.error(
|
|
2653
|
+
`
|
|
2654
|
+
Invalid slug format: "${slug}". Expected "author/name".
|
|
2655
|
+
`
|
|
2656
|
+
)
|
|
2657
|
+
);
|
|
2658
|
+
process.exit(1);
|
|
2659
|
+
}
|
|
2660
|
+
const configManager = getConfigManager();
|
|
2661
|
+
const isInstalled = await configManager.isInstalled(slug);
|
|
2662
|
+
if (!isInstalled) {
|
|
2663
|
+
console.log(muted(`
|
|
2664
|
+
Component not installed: ${slug}
|
|
2665
|
+
`));
|
|
2666
|
+
process.exit(1);
|
|
2667
|
+
}
|
|
2668
|
+
if (!options.force) {
|
|
2669
|
+
const confirmed = await confirmPrompt(
|
|
2670
|
+
`Uninstall ${slug}?`,
|
|
2671
|
+
false
|
|
2672
|
+
);
|
|
2673
|
+
if (!confirmed) {
|
|
2674
|
+
console.log(muted("\n Cancelled.\n"));
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
const spinner = startSpinner(`Uninstalling ${slug}...`);
|
|
2679
|
+
try {
|
|
2680
|
+
const removed = await configManager.uninstallComponent(slug);
|
|
2681
|
+
if (removed) {
|
|
2682
|
+
spinner.succeed(`Uninstalled ${slug}`);
|
|
2683
|
+
console.log();
|
|
2684
|
+
} else {
|
|
2685
|
+
spinner.fail(`Failed to uninstall ${slug}`);
|
|
2686
|
+
process.exit(1);
|
|
2687
|
+
}
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
spinner.fail("Uninstall failed");
|
|
2690
|
+
console.error(colors.error(`
|
|
2691
|
+
${formatError(error)}
|
|
2692
|
+
`));
|
|
2693
|
+
process.exit(1);
|
|
2694
|
+
}
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
// src/commands/run-hook.ts
|
|
2699
|
+
import { Command as Command6 } from "commander";
|
|
2700
|
+
import { spawn } from "child_process";
|
|
2701
|
+
function createRunHookCommand() {
|
|
2702
|
+
return new Command6("run-hook").description("Execute an installed hook").argument("<slug>", "Hook to execute (author/name)").option("--timeout <ms>", "Execution timeout in milliseconds", "30000").action(async (slug, options) => {
|
|
2703
|
+
if (!isValidSlug(slug)) {
|
|
2704
|
+
console.error(
|
|
2705
|
+
colors.error(
|
|
2706
|
+
`Invalid slug format: "${slug}". Expected "author/name".`
|
|
2707
|
+
)
|
|
2708
|
+
);
|
|
2709
|
+
process.exit(1);
|
|
2710
|
+
}
|
|
2711
|
+
const configManager = getConfigManager();
|
|
2712
|
+
const hookDef = await configManager.getHookDefinition(slug);
|
|
2713
|
+
if (!hookDef) {
|
|
2714
|
+
console.error(colors.error(`Hook not installed: ${slug}`));
|
|
2715
|
+
console.error(muted(`Install it first: tenfold install ${slug}`));
|
|
2716
|
+
process.exit(1);
|
|
2717
|
+
}
|
|
2718
|
+
const timeout = parseInt(options.timeout, 10);
|
|
2719
|
+
const child = spawn(hookDef.command, [], {
|
|
2720
|
+
stdio: "inherit",
|
|
2721
|
+
shell: true,
|
|
2722
|
+
timeout
|
|
2723
|
+
});
|
|
2724
|
+
child.on("exit", (code) => {
|
|
2725
|
+
process.exit(code ?? 0);
|
|
2726
|
+
});
|
|
2727
|
+
child.on("error", (error) => {
|
|
2728
|
+
console.error(colors.error(`Hook execution failed: ${error.message}`));
|
|
2729
|
+
process.exit(1);
|
|
2730
|
+
});
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
// src/types/component.ts
|
|
2735
|
+
function isHookContent(content) {
|
|
2736
|
+
return "event" in content && "command" in content;
|
|
2737
|
+
}
|
|
2738
|
+
function isMcpServerContent(content) {
|
|
2739
|
+
return "command" in content && !("event" in content) && !("prompt" in content);
|
|
2740
|
+
}
|
|
2741
|
+
function isInstructionContent(content) {
|
|
2742
|
+
return "paragraphs" in content;
|
|
2743
|
+
}
|
|
2744
|
+
function isCommandContent(content) {
|
|
2745
|
+
return "content" in content && "name" in content && !("prompt" in content);
|
|
2746
|
+
}
|
|
2747
|
+
function isSkillContent(content) {
|
|
2748
|
+
return "prompt" in content && !("systemPrompt" in content);
|
|
2749
|
+
}
|
|
2750
|
+
function isAgentContent(content) {
|
|
2751
|
+
return "systemPrompt" in content;
|
|
2752
|
+
}
|
|
2753
|
+
var COMPONENT_ICONS = {
|
|
2754
|
+
INSTRUCTION: "\u{1F4DD}",
|
|
2755
|
+
HOOK: "\u{1F527}",
|
|
2756
|
+
COMMAND: "\u2318",
|
|
2757
|
+
MCP_SERVER: "\u{1F5A5}",
|
|
2758
|
+
SKILL: "\u26A1",
|
|
2759
|
+
AGENT: "\u{1F916}"
|
|
2760
|
+
};
|
|
2761
|
+
function getComponentIcon(type) {
|
|
2762
|
+
return COMPONENT_ICONS[type] || "\u{1F4E6}";
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// src/types/api.ts
|
|
2766
|
+
var DEFAULT_API_CONFIG = {
|
|
2767
|
+
baseUrl: process.env.TENFOLD_API_URL ?? "https://api.tenfold.dev",
|
|
2768
|
+
timeout: 3e4
|
|
2769
|
+
};
|
|
2770
|
+
|
|
2771
|
+
// src/index.ts
|
|
2772
|
+
var VERSION = "0.1.0";
|
|
2773
|
+
function createProgram() {
|
|
2774
|
+
const program = new Command7().name("tenfold").description("Package manager for Claude Code components").version(VERSION, "-V, --version", "Display version number");
|
|
2775
|
+
program.addHelpText("beforeAll", renderBanner());
|
|
2776
|
+
program.addCommand(createSearchCommand());
|
|
2777
|
+
program.addCommand(createBrowseCommand());
|
|
2778
|
+
program.addCommand(createInstallCommand());
|
|
2779
|
+
program.addCommand(createListCommand());
|
|
2780
|
+
program.addCommand(createUninstallCommand());
|
|
2781
|
+
program.addCommand(createRunHookCommand());
|
|
2782
|
+
program.action(() => {
|
|
2783
|
+
console.log(renderWelcome());
|
|
2784
|
+
});
|
|
2785
|
+
return program;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
export {
|
|
2789
|
+
TenfoldApiClient,
|
|
2790
|
+
getApiClient,
|
|
2791
|
+
TenfoldMetadataSchema,
|
|
2792
|
+
TargetFileSchema,
|
|
2793
|
+
InstalledComponentSchema,
|
|
2794
|
+
InstalledManifestSchema,
|
|
2795
|
+
HookDefinitionSchema,
|
|
2796
|
+
HookEntrySchema,
|
|
2797
|
+
ClaudeSettingsSchema,
|
|
2798
|
+
McpServerEntrySchema,
|
|
2799
|
+
McpConfigSchema,
|
|
2800
|
+
CredentialsSchema,
|
|
2801
|
+
createEmptyManifest,
|
|
2802
|
+
ConfigManager,
|
|
2803
|
+
getConfigManager,
|
|
2804
|
+
isHookContent,
|
|
2805
|
+
isMcpServerContent,
|
|
2806
|
+
isInstructionContent,
|
|
2807
|
+
isCommandContent,
|
|
2808
|
+
isSkillContent,
|
|
2809
|
+
isAgentContent,
|
|
2810
|
+
COMPONENT_ICONS,
|
|
2811
|
+
getComponentIcon,
|
|
2812
|
+
DEFAULT_API_CONFIG,
|
|
2813
|
+
createProgram
|
|
2814
|
+
};
|