@backcap/cli 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1356 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const citty = require('citty');
5
+ const pkgTypes = require('pkg-types');
6
+ const result = require('@backcap/shared/result');
7
+ const promises = require('node:fs/promises');
8
+ const pathe = require('pathe');
9
+ const config = require('@backcap/shared/schemas/config');
10
+ const clack = require('@clack/prompts');
11
+ const consola = require('consola');
12
+ const ofetch = require('ofetch');
13
+ const registry = require('@backcap/shared/schemas/registry');
14
+ const registryItem = require('@backcap/shared/schemas/registry-item');
15
+ const node_fs = require('node:fs');
16
+ const node_child_process = require('node:child_process');
17
+
18
+ function _interopNamespaceCompat(e) {
19
+ if (e && typeof e === 'object' && 'default' in e) return e;
20
+ const n = Object.create(null);
21
+ if (e) {
22
+ for (const k in e) {
23
+ n[k] = e[k];
24
+ }
25
+ }
26
+ n.default = e;
27
+ return n;
28
+ }
29
+
30
+ const clack__namespace = /*#__PURE__*/_interopNamespaceCompat(clack);
31
+
32
+ var __defProp$3 = Object.defineProperty;
33
+ var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
34
+ var __publicField$3 = (obj, key, value) => {
35
+ __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
36
+ return value;
37
+ };
38
+ class ConfigError extends Error {
39
+ constructor(message, code, cause) {
40
+ super(message);
41
+ __publicField$3(this, "code");
42
+ __publicField$3(this, "cause");
43
+ this.name = "ConfigError";
44
+ this.code = code;
45
+ this.cause = cause;
46
+ }
47
+ }
48
+ class ValidationError extends ConfigError {
49
+ constructor(zodError) {
50
+ const formatted = zodError.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
51
+ super(
52
+ `Config validation failed:
53
+ ${formatted}`,
54
+ "VALIDATION_ERROR",
55
+ zodError
56
+ );
57
+ this.name = "ValidationError";
58
+ }
59
+ }
60
+ class DetectionError extends Error {
61
+ constructor(field) {
62
+ super(`Could not auto-detect ${field}`);
63
+ __publicField$3(this, "field");
64
+ this.name = "DetectionError";
65
+ this.field = field;
66
+ }
67
+ }
68
+
69
+ const FRAMEWORK_MAP = [
70
+ { pkg: "next", id: "nextjs" },
71
+ { pkg: "@nestjs/core", id: "nestjs" },
72
+ { pkg: "fastify", id: "fastify" },
73
+ { pkg: "hono", id: "hono" },
74
+ { pkg: "express", id: "express" }
75
+ ];
76
+ async function detectFramework(cwd) {
77
+ try {
78
+ const pkg = await pkgTypes.readPackageJSON(cwd);
79
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
80
+ for (const { pkg: name, id } of FRAMEWORK_MAP) {
81
+ if (name in deps) {
82
+ return result.Result.ok(id);
83
+ }
84
+ }
85
+ } catch {
86
+ }
87
+ return result.Result.fail(new DetectionError("framework"));
88
+ }
89
+
90
+ const LOCKFILE_MAP = [
91
+ { file: "bun.lockb", id: "bun" },
92
+ { file: "pnpm-lock.yaml", id: "pnpm" },
93
+ { file: "yarn.lock", id: "yarn" },
94
+ { file: "package-lock.json", id: "npm" }
95
+ ];
96
+ async function fileExists(path) {
97
+ try {
98
+ await promises.stat(path);
99
+ return true;
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+ async function detectPackageManager(cwd) {
105
+ for (const { file, id } of LOCKFILE_MAP) {
106
+ if (await fileExists(pathe.join(cwd, file))) {
107
+ return result.Result.ok(id);
108
+ }
109
+ }
110
+ return result.Result.fail(new DetectionError("packageManager"));
111
+ }
112
+
113
+ async function configExists(cwd) {
114
+ try {
115
+ await promises.stat(pathe.join(cwd, "backcap.json"));
116
+ return true;
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+ async function loadConfig(cwd) {
122
+ try {
123
+ const raw = await promises.readFile(pathe.join(cwd, "backcap.json"), "utf-8");
124
+ const parsed = JSON.parse(raw);
125
+ const result$1 = config.configSchema.safeParse(parsed);
126
+ if (!result$1.success) {
127
+ return result.Result.fail(new ValidationError(result$1.error));
128
+ }
129
+ return result.Result.ok(result$1.data);
130
+ } catch (err) {
131
+ return result.Result.fail(
132
+ new ConfigError(
133
+ `Failed to load backcap.json: ${err instanceof Error ? err.message : String(err)}`,
134
+ "LOAD_ERROR",
135
+ err
136
+ )
137
+ );
138
+ }
139
+ }
140
+ function normalizePaths(config) {
141
+ return {
142
+ ...config,
143
+ paths: {
144
+ capabilities: pathe.normalize(config.paths.capabilities),
145
+ adapters: pathe.normalize(config.paths.adapters),
146
+ bridges: pathe.normalize(config.paths.bridges),
147
+ skills: pathe.normalize(config.paths.skills),
148
+ shared: pathe.normalize(config.paths.shared)
149
+ }
150
+ };
151
+ }
152
+ async function writeConfig(config, cwd) {
153
+ try {
154
+ const normalized = normalizePaths(config);
155
+ const content = JSON.stringify(normalized, null, 2) + "\n";
156
+ await promises.writeFile(pathe.join(cwd, "backcap.json"), content, "utf-8");
157
+ return result.Result.ok(void 0);
158
+ } catch (err) {
159
+ return result.Result.fail(
160
+ new ConfigError(
161
+ `Failed to write backcap.json: ${err instanceof Error ? err.message : String(err)}`,
162
+ "WRITE_ERROR",
163
+ err
164
+ )
165
+ );
166
+ }
167
+ }
168
+
169
+ function buildDefaultConfig(framework, pm) {
170
+ return {
171
+ framework,
172
+ packageManager: pm,
173
+ paths: {
174
+ capabilities: "src/capabilities",
175
+ adapters: "src/adapters",
176
+ bridges: "src/bridges",
177
+ skills: ".claude/skills",
178
+ shared: "src/shared"
179
+ },
180
+ installed: { capabilities: [], bridges: [] }
181
+ };
182
+ }
183
+
184
+ function intro() {
185
+ clack__namespace.intro("backcap init");
186
+ }
187
+ function outro(msg) {
188
+ clack__namespace.outro(msg);
189
+ }
190
+ function fail(msg) {
191
+ clack__namespace.cancel(msg);
192
+ process.exit(1);
193
+ }
194
+ async function promptFramework() {
195
+ const value = await clack__namespace.select({
196
+ message: "Which framework are you using?",
197
+ options: [
198
+ { value: "nextjs", label: "Next.js" },
199
+ { value: "express", label: "Express" },
200
+ { value: "fastify", label: "Fastify" },
201
+ { value: "nestjs", label: "NestJS" },
202
+ { value: "hono", label: "Hono" }
203
+ ]
204
+ });
205
+ if (clack__namespace.isCancel(value)) {
206
+ process.exit(0);
207
+ }
208
+ return value;
209
+ }
210
+ async function promptPackageManager() {
211
+ const value = await clack__namespace.select({
212
+ message: "Which package manager are you using?",
213
+ options: [
214
+ { value: "npm", label: "npm" },
215
+ { value: "pnpm", label: "pnpm" },
216
+ { value: "yarn", label: "yarn" },
217
+ { value: "bun", label: "bun" }
218
+ ]
219
+ });
220
+ if (clack__namespace.isCancel(value)) {
221
+ process.exit(0);
222
+ }
223
+ return value;
224
+ }
225
+ async function promptOverwriteConfirm(existingConfig) {
226
+ const value = await clack__namespace.confirm({
227
+ message: `A backcap.json already exists (framework: ${existingConfig.framework}, packageManager: ${existingConfig.packageManager}). Overwrite?`
228
+ });
229
+ if (clack__namespace.isCancel(value)) {
230
+ process.exit(0);
231
+ }
232
+ return value;
233
+ }
234
+
235
+ const log = consola.createConsola({
236
+ fancy: true,
237
+ defaults: {
238
+ tag: "backcap"
239
+ }
240
+ });
241
+
242
+ const init = citty.defineCommand({
243
+ meta: {
244
+ name: "init",
245
+ description: "Initialize a Backcap project in the current directory"
246
+ },
247
+ async run() {
248
+ const cwd = process.cwd();
249
+ intro();
250
+ const frameworkResult = await detectFramework(cwd);
251
+ let framework = frameworkResult.isOk() ? frameworkResult.unwrap() : await promptFramework();
252
+ if (frameworkResult.isOk()) {
253
+ log.info(`Detected framework: ${framework}`);
254
+ }
255
+ const pmResult = await detectPackageManager(cwd);
256
+ let pm = pmResult.isOk() ? pmResult.unwrap() : await promptPackageManager();
257
+ if (pmResult.isOk()) {
258
+ log.info(`Detected package manager: ${pm}`);
259
+ }
260
+ if (await configExists(cwd)) {
261
+ const existingResult = await loadConfig(cwd);
262
+ if (existingResult.isOk()) {
263
+ const shouldOverwrite = await promptOverwriteConfirm(
264
+ existingResult.unwrap()
265
+ );
266
+ if (!shouldOverwrite) {
267
+ outro("Kept existing backcap.json unchanged.");
268
+ return;
269
+ }
270
+ }
271
+ }
272
+ const config = buildDefaultConfig(framework, pm);
273
+ const writeResult = await writeConfig(config, cwd);
274
+ if (writeResult.isFail()) {
275
+ fail(writeResult.unwrapError().message);
276
+ return;
277
+ }
278
+ outro("backcap.json created successfully!");
279
+ }
280
+ });
281
+
282
+ class RegistryError extends Error {
283
+ constructor(message) {
284
+ super(message);
285
+ this.name = "RegistryError";
286
+ }
287
+ }
288
+
289
+ const FALLBACK_URL = "https://raw.githubusercontent.com/backcap/registry/main/dist/registry.json";
290
+ async function fetchRegistry(primaryUrl) {
291
+ let data;
292
+ try {
293
+ data = await ofetch.ofetch(primaryUrl, { timeout: 1e3 });
294
+ } catch (e) {
295
+ if (e instanceof ofetch.FetchError) {
296
+ log.warn("Primary registry unavailable. Using fallback.");
297
+ try {
298
+ data = await ofetch.ofetch(FALLBACK_URL, { timeout: 3e3 });
299
+ } catch {
300
+ throw new RegistryError(
301
+ `Unable to reach registry. Check your internet connection or try again later.
302
+ Primary: ${primaryUrl}
303
+ Fallback: ${FALLBACK_URL}`
304
+ );
305
+ }
306
+ } else {
307
+ throw e;
308
+ }
309
+ }
310
+ const result = registry.registrySchema.safeParse(data);
311
+ if (!result.success) {
312
+ throw new RegistryError(
313
+ "Registry response is invalid. This may indicate a version mismatch. Try updating: npx backcap@latest list"
314
+ );
315
+ }
316
+ return result.data;
317
+ }
318
+
319
+ function pad(str, len) {
320
+ return str.length >= len ? str.slice(0, len) : str + " ".repeat(len - str.length);
321
+ }
322
+ function truncate(str, maxLen) {
323
+ return str.length > maxLen ? str.slice(0, maxLen - 3) + "..." : str;
324
+ }
325
+ function renderCapabilityTable(items, installed) {
326
+ const capabilities = items.filter((i) => i.type === "capability");
327
+ const COL = { name: 20, version: 10, description: 50, installed: 12 };
328
+ const header = pad("Name", COL.name) + pad("Version", COL.version) + pad("Description", COL.description) + pad("Installed", COL.installed);
329
+ const separator = "-".repeat(COL.name + COL.version + COL.description + COL.installed);
330
+ const rows = capabilities.map((cap) => {
331
+ const installedMark = installed.has(cap.name) ? "\u2713" : "\u2014";
332
+ const version = cap.version ?? "\u2014";
333
+ return pad(cap.name, COL.name) + pad(version, COL.version) + pad(truncate(cap.description, COL.description - 2), COL.description) + installedMark;
334
+ });
335
+ const footer = `
336
+ ${capabilities.length} capabilities available`;
337
+ return [header, separator, ...rows, footer].join("\n") + "\n";
338
+ }
339
+
340
+ const DEFAULT_REGISTRY_URL$2 = "https://backcap.dev/registry.json";
341
+ const list = citty.defineCommand({
342
+ meta: {
343
+ name: "list",
344
+ description: "Browse available capabilities from the registry"
345
+ },
346
+ async run() {
347
+ const cwd = process.cwd();
348
+ let installed = /* @__PURE__ */ new Set();
349
+ let registryUrl = DEFAULT_REGISTRY_URL$2;
350
+ if (await configExists(cwd)) {
351
+ const configResult = await loadConfig(cwd);
352
+ if (configResult.isOk()) {
353
+ const config = configResult.unwrap();
354
+ const caps = config.installed?.capabilities ?? [];
355
+ installed = new Set(caps.map((c) => c.name));
356
+ }
357
+ } else {
358
+ log.info("Run `backcap init` to configure your project.");
359
+ }
360
+ log.start("Fetching registry...");
361
+ try {
362
+ const registry = await fetchRegistry(registryUrl);
363
+ const items = registry.items ?? [];
364
+ log.success("Registry loaded");
365
+ const table = renderCapabilityTable(items, installed);
366
+ process.stdout.write(table);
367
+ } catch (e) {
368
+ log.error(e.message);
369
+ process.exit(1);
370
+ }
371
+ }
372
+ });
373
+
374
+ const KNOWN_ADAPTERS = [
375
+ { npmPackage: "@prisma/client", adapterSuffix: "prisma", category: "persistence" },
376
+ { npmPackage: "express", adapterSuffix: "express", category: "http" },
377
+ { npmPackage: "fastify", adapterSuffix: "fastify", category: "http" },
378
+ { npmPackage: "@nestjs/core", adapterSuffix: "nestjs", category: "http" },
379
+ { npmPackage: "hono", adapterSuffix: "hono", category: "http" }
380
+ ];
381
+ async function detectAdapters(cwd, capabilityName) {
382
+ try {
383
+ const pkg = await pkgTypes.readPackageJSON(cwd);
384
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
385
+ return KNOWN_ADAPTERS.map((mapping) => ({
386
+ name: `${capabilityName}-${mapping.adapterSuffix}`,
387
+ category: mapping.category,
388
+ detected: mapping.npmPackage in deps
389
+ })).filter((a) => a.detected);
390
+ } catch {
391
+ return [];
392
+ }
393
+ }
394
+
395
+ function detectPM(cwd) {
396
+ if (node_fs.existsSync(pathe.join(cwd, "pnpm-lock.yaml")))
397
+ return "pnpm";
398
+ if (node_fs.existsSync(pathe.join(cwd, "yarn.lock")))
399
+ return "yarn";
400
+ if (node_fs.existsSync(pathe.join(cwd, "bun.lockb")))
401
+ return "bun";
402
+ return "npm";
403
+ }
404
+ function buildInstallCommand(pm, deps, dev = false) {
405
+ const installCmd = { npm: "install", pnpm: "add", yarn: "add", bun: "add" }[pm];
406
+ const devFlag = { npm: "--save-dev", pnpm: "-D", yarn: "--dev", bun: "-d" }[pm];
407
+ return [pm, installCmd, ...dev ? [devFlag] : [], ...deps];
408
+ }
409
+
410
+ function resolveSharedPath(fileDest, capabilityRoot) {
411
+ const sharedDir = pathe.join(capabilityRoot, "shared");
412
+ return pathe.relative(pathe.dirname(fileDest), sharedDir);
413
+ }
414
+ function applyTemplateMarkers(content, markers) {
415
+ return Object.entries(markers).reduce(
416
+ (acc, [key, value]) => acc.replaceAll(`{{${key}}}`, value),
417
+ content
418
+ );
419
+ }
420
+
421
+ async function writeCapabilityFiles(files, options) {
422
+ const writtenPaths = [];
423
+ for (const file of files) {
424
+ const destPath = pathe.normalize(pathe.join(options.capabilityRoot, file.path));
425
+ const dir = pathe.dirname(destPath);
426
+ await promises.mkdir(dir, { recursive: true });
427
+ const sharedPath = resolveSharedPath(destPath, options.capabilityRoot);
428
+ const fileMarkers = { ...options.markers, shared_path: sharedPath };
429
+ const content = applyTemplateMarkers(file.content, fileMarkers);
430
+ await promises.writeFile(destPath, content, "utf-8");
431
+ writtenPaths.push(destPath);
432
+ }
433
+ return writtenPaths;
434
+ }
435
+
436
+ async function installDeps(pm, deps, cwd, dev = false) {
437
+ if (deps.length === 0)
438
+ return;
439
+ const [cmd, ...args] = buildInstallCommand(pm, deps, dev);
440
+ if (!cmd)
441
+ return;
442
+ return new Promise((resolve, reject) => {
443
+ const child = node_child_process.spawn(cmd, args, { cwd, stdio: "pipe" });
444
+ child.on("close", (code) => {
445
+ if (code === 0)
446
+ resolve();
447
+ else
448
+ reject(new Error(`${cmd} exited with code ${code}`));
449
+ });
450
+ child.on("error", reject);
451
+ });
452
+ }
453
+
454
+ function ensureInstalled(config) {
455
+ if (!config.installed || typeof config.installed !== "object" || Array.isArray(config.installed)) {
456
+ config.installed = { capabilities: [], bridges: [] };
457
+ }
458
+ const installed = config.installed;
459
+ if (!Array.isArray(installed.capabilities)) {
460
+ installed.capabilities = [];
461
+ }
462
+ if (!Array.isArray(installed.bridges)) {
463
+ installed.bridges = [];
464
+ }
465
+ return installed;
466
+ }
467
+ async function updateConfigCapability(cwd, entry) {
468
+ const configPath = pathe.join(cwd, "backcap.json");
469
+ const { readFile } = await import('node:fs/promises');
470
+ const raw = await readFile(configPath, "utf-8");
471
+ const config = JSON.parse(raw);
472
+ const installed = ensureInstalled(config);
473
+ const capEntry = {
474
+ name: entry.name,
475
+ version: entry.version,
476
+ adapters: entry.adapters
477
+ };
478
+ if (entry.partial) {
479
+ capEntry.partial = true;
480
+ }
481
+ installed.capabilities.push(capEntry);
482
+ const tmpPath = configPath + ".tmp";
483
+ await promises.writeFile(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
484
+ await promises.rename(tmpPath, configPath);
485
+ }
486
+ async function updateConfigBridge(cwd, entry) {
487
+ const configPath = pathe.join(cwd, "backcap.json");
488
+ const { readFile } = await import('node:fs/promises');
489
+ const raw = await readFile(configPath, "utf-8");
490
+ const config = JSON.parse(raw);
491
+ const installed = ensureInstalled(config);
492
+ installed.bridges.push({ name: entry.name, version: entry.version });
493
+ const tmpPath = configPath + ".tmp";
494
+ await promises.writeFile(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
495
+ await promises.rename(tmpPath, configPath);
496
+ }
497
+
498
+ var __defProp$2 = Object.defineProperty;
499
+ var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
500
+ var __publicField$2 = (obj, key, value) => {
501
+ __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
502
+ return value;
503
+ };
504
+ class ConflictDetectionError extends Error {
505
+ constructor(message, filePath, suggestion, cause) {
506
+ super(message);
507
+ __publicField$2(this, "filePath");
508
+ __publicField$2(this, "suggestion");
509
+ __publicField$2(this, "cause");
510
+ this.name = "ConflictDetectionError";
511
+ this.filePath = filePath;
512
+ this.suggestion = suggestion;
513
+ this.cause = cause;
514
+ }
515
+ }
516
+
517
+ async function detectConflicts(targetDir, incomingFiles) {
518
+ const resolvedTarget = pathe.resolve(targetDir);
519
+ const files = [];
520
+ for (const incoming of incomingFiles) {
521
+ const filePath = pathe.resolve(pathe.join(resolvedTarget, incoming.relativePath));
522
+ const rel = pathe.relative(resolvedTarget, filePath);
523
+ if (rel.startsWith("..") || rel.startsWith("/")) {
524
+ throw new ConflictDetectionError(
525
+ `Path traversal detected: "${incoming.relativePath}" resolves outside target directory`,
526
+ incoming.relativePath,
527
+ "Ensure all file paths in the capability are relative and do not contain '..' segments."
528
+ );
529
+ }
530
+ try {
531
+ const existingContent = await promises.readFile(filePath, "utf-8");
532
+ if (existingContent === incoming.content) {
533
+ files.push({
534
+ relativePath: incoming.relativePath,
535
+ status: "identical",
536
+ existingContent,
537
+ incomingContent: incoming.content
538
+ });
539
+ } else {
540
+ files.push({
541
+ relativePath: incoming.relativePath,
542
+ status: "modified",
543
+ existingContent,
544
+ incomingContent: incoming.content
545
+ });
546
+ }
547
+ } catch (err) {
548
+ const error = err;
549
+ if (error.code === "ENOENT") {
550
+ files.push({
551
+ relativePath: incoming.relativePath,
552
+ status: "new",
553
+ incomingContent: incoming.content
554
+ });
555
+ } else if (error.code === "EACCES") {
556
+ throw new ConflictDetectionError(
557
+ `Permission denied reading "${incoming.relativePath}"`,
558
+ incoming.relativePath,
559
+ "Check file permissions or run the command with appropriate privileges.",
560
+ error
561
+ );
562
+ } else {
563
+ throw new ConflictDetectionError(
564
+ `Failed to read "${incoming.relativePath}": ${error.message}`,
565
+ incoming.relativePath,
566
+ "Check that the file is accessible and the disk is not full.",
567
+ error
568
+ );
569
+ }
570
+ }
571
+ }
572
+ const hasConflicts = files.some((f) => f.status === "modified");
573
+ return { hasConflicts, files };
574
+ }
575
+
576
+ const MAX_DIFF_LINES = 50;
577
+ function groupByStatus(files) {
578
+ const newFiles = [];
579
+ const modifiedFiles = [];
580
+ const identicalFiles = [];
581
+ for (const file of files) {
582
+ if (file.status === "new")
583
+ newFiles.push(file);
584
+ else if (file.status === "modified")
585
+ modifiedFiles.push(file);
586
+ else
587
+ identicalFiles.push(file);
588
+ }
589
+ return { newFiles, modifiedFiles, identicalFiles };
590
+ }
591
+ function computeLineDiff(existing, incoming) {
592
+ const existingLines = existing.split("\n");
593
+ const incomingLines = incoming.split("\n");
594
+ const lines = [];
595
+ const maxLen = Math.max(existingLines.length, incomingLines.length);
596
+ for (let i = 0; i < maxLen; i++) {
597
+ const existLine = i < existingLines.length ? existingLines[i] : void 0;
598
+ const incomLine = i < incomingLines.length ? incomingLines[i] : void 0;
599
+ if (existLine === incomLine)
600
+ continue;
601
+ if (existLine !== void 0 && incomLine !== void 0) {
602
+ lines.push(`- ${existLine}`);
603
+ lines.push(`+ ${incomLine}`);
604
+ } else if (existLine !== void 0) {
605
+ lines.push(`- ${existLine}`);
606
+ } else if (incomLine !== void 0) {
607
+ lines.push(`+ ${incomLine}`);
608
+ }
609
+ }
610
+ return lines;
611
+ }
612
+ function renderConflictSummary(report) {
613
+ const { newFiles, modifiedFiles, identicalFiles } = groupByStatus(report.files);
614
+ const sections = [];
615
+ if (newFiles.length > 0) {
616
+ sections.push(
617
+ `New files (${newFiles.length}):
618
+ ${newFiles.map((f) => ` + ${f.relativePath}`).join("\n")}`
619
+ );
620
+ }
621
+ if (modifiedFiles.length > 0) {
622
+ sections.push(
623
+ `Modified files (${modifiedFiles.length}):
624
+ ${modifiedFiles.map((f) => ` ~ ${f.relativePath}`).join("\n")}`
625
+ );
626
+ }
627
+ if (identicalFiles.length > 0) {
628
+ sections.push(
629
+ `Identical files (${identicalFiles.length}):
630
+ ${identicalFiles.map((f) => ` = ${f.relativePath}`).join("\n")}`
631
+ );
632
+ }
633
+ clack__namespace.note(sections.join("\n\n"), "Conflict Report");
634
+ }
635
+ function renderDetailedDiffs(report) {
636
+ const modifiedFiles = report.files.filter((f) => f.status === "modified");
637
+ for (const file of modifiedFiles) {
638
+ if (!file.existingContent)
639
+ continue;
640
+ const diffLines = computeLineDiff(file.existingContent, file.incomingContent);
641
+ const truncated = diffLines.length > MAX_DIFF_LINES;
642
+ const displayed = truncated ? diffLines.slice(0, MAX_DIFF_LINES) : diffLines;
643
+ let body = displayed.join("\n");
644
+ if (truncated) {
645
+ body += `
646
+ ...${diffLines.length - MAX_DIFF_LINES} more lines`;
647
+ }
648
+ clack__namespace.note(body, file.relativePath);
649
+ }
650
+ }
651
+
652
+ class InstallCancelledError extends Error {
653
+ constructor() {
654
+ super("Installation cancelled by user.");
655
+ this.name = "InstallCancelledError";
656
+ }
657
+ }
658
+ async function selectiveInstall(report, skillFiles) {
659
+ const options = report.files.map((file) => {
660
+ const isSkill = skillFiles.has(file.relativePath);
661
+ const statusLabel = file.status === "new" ? "new" : file.status === "modified" ? "modified" : "identical";
662
+ return {
663
+ value: file.relativePath,
664
+ label: `${file.relativePath} (${statusLabel})`,
665
+ hint: isSkill ? "always installed" : void 0
666
+ };
667
+ });
668
+ const initialValues = report.files.filter((f) => f.status !== "modified").map((f) => f.relativePath);
669
+ for (const skillPath of skillFiles) {
670
+ if (!initialValues.includes(skillPath)) {
671
+ initialValues.push(skillPath);
672
+ }
673
+ }
674
+ const selected = await clack__namespace.multiselect({
675
+ message: "Select files to install:",
676
+ options,
677
+ initialValues
678
+ });
679
+ if (clack__namespace.isCancel(selected)) {
680
+ throw new InstallCancelledError();
681
+ }
682
+ const selectedSet = new Set(selected);
683
+ for (const skillPath of skillFiles) {
684
+ selectedSet.add(skillPath);
685
+ }
686
+ const installed = [];
687
+ const skipped = [];
688
+ const alwaysInstalled = [];
689
+ for (const file of report.files) {
690
+ if (skillFiles.has(file.relativePath)) {
691
+ alwaysInstalled.push(file.relativePath);
692
+ } else if (selectedSet.has(file.relativePath)) {
693
+ installed.push(file.relativePath);
694
+ } else {
695
+ skipped.push(file.relativePath);
696
+ }
697
+ }
698
+ return { installed, skipped, alwaysInstalled };
699
+ }
700
+
701
+ function resolveSkillFiles(capabilityJson) {
702
+ const skillFiles = /* @__PURE__ */ new Set();
703
+ if (capabilityJson.skills) {
704
+ for (const skillPath of capabilityJson.skills) {
705
+ skillFiles.add(skillPath);
706
+ }
707
+ }
708
+ if (capabilityJson.files) {
709
+ for (const file of capabilityJson.files) {
710
+ const filename = file.path.split("/").pop() ?? "";
711
+ if (filename.toLowerCase() === "skill.md") {
712
+ skillFiles.add(file.path);
713
+ }
714
+ }
715
+ }
716
+ return skillFiles;
717
+ }
718
+
719
+ async function installSkill(options) {
720
+ const { skillsPath, capabilityName, skillFiles, coreSkillFiles, templateValues, onConflict } = options;
721
+ await installCoreSkillIfAbsent(skillsPath, coreSkillFiles, templateValues);
722
+ if (skillFiles.length === 0)
723
+ return;
724
+ const skillDirName = `backcap-${capabilityName}`;
725
+ const skillDir = pathe.join(skillsPath, skillDirName);
726
+ const existing = await skillDirExists(skillDir);
727
+ if (existing && onConflict) {
728
+ const action = await onConflict(skillDirName);
729
+ if (action === "skip")
730
+ return;
731
+ if (action === "merge") {
732
+ await mergeSkillDir(skillDir, skillFiles, templateValues);
733
+ return;
734
+ }
735
+ }
736
+ await writeSkillFiles(skillDir, skillFiles, templateValues);
737
+ }
738
+ async function skillDirExists(dir) {
739
+ try {
740
+ const s = await promises.stat(dir);
741
+ return s.isDirectory();
742
+ } catch {
743
+ return false;
744
+ }
745
+ }
746
+ async function mergeSkillDir(skillDir, files, templateValues) {
747
+ for (const file of files) {
748
+ const destPath = pathe.join(skillDir, file.path);
749
+ const content = applyTemplateMarkers(file.content, templateValues);
750
+ try {
751
+ const existing = await promises.readFile(destPath, "utf-8");
752
+ const merged = mergeSkillFiles(existing, content);
753
+ if (merged !== existing) {
754
+ await promises.writeFile(destPath, merged, "utf-8");
755
+ }
756
+ } catch {
757
+ const dir = pathe.dirname(destPath);
758
+ await promises.mkdir(dir, { recursive: true });
759
+ await promises.writeFile(destPath, content, "utf-8");
760
+ }
761
+ }
762
+ }
763
+ async function installCoreSkillIfAbsent(skillsPath, coreFiles, templateValues) {
764
+ if (coreFiles.length === 0)
765
+ return;
766
+ const coreSkillPath = pathe.join(skillsPath, "backcap-core", "SKILL.md");
767
+ try {
768
+ await promises.readFile(coreSkillPath, "utf-8");
769
+ return;
770
+ } catch {
771
+ }
772
+ const coreDir = pathe.join(skillsPath, "backcap-core");
773
+ await writeSkillFiles(coreDir, coreFiles, templateValues);
774
+ }
775
+ async function writeSkillFiles(targetDir, files, templateValues) {
776
+ for (const file of files) {
777
+ const destPath = pathe.join(targetDir, file.path);
778
+ const dir = pathe.dirname(destPath);
779
+ await promises.mkdir(dir, { recursive: true });
780
+ const content = applyTemplateMarkers(file.content, templateValues);
781
+ await promises.writeFile(destPath, content, "utf-8");
782
+ }
783
+ }
784
+ function extractSkillFiles(files) {
785
+ return files.filter((f) => f.type === "skill" || f.path.startsWith("skills/")).filter((f) => typeof f.content === "string").map((f) => ({
786
+ path: f.path.replace(/^skills\//, ""),
787
+ content: f.content
788
+ }));
789
+ }
790
+ function resolveSkillsPath(config) {
791
+ return config.paths.skills ?? ".claude/skills";
792
+ }
793
+ function mergeSkillFiles(existing, incoming) {
794
+ const existingSections = parseSections(existing);
795
+ const incomingSections = parseSections(incoming);
796
+ const existingNames = new Set(existingSections.map((s) => s.name));
797
+ const newSections = incomingSections.filter((s) => !existingNames.has(s.name));
798
+ if (newSections.length === 0)
799
+ return existing;
800
+ return existing.trimEnd() + "\n\n" + newSections.map((s) => s.content).join("\n\n") + "\n";
801
+ }
802
+ function parseSections(content) {
803
+ const sections = [];
804
+ const lines = content.split("\n");
805
+ let currentName = "";
806
+ let currentLines = [];
807
+ for (const line of lines) {
808
+ const match = line.match(/^## (.+)$/);
809
+ if (match) {
810
+ if (currentName) {
811
+ sections.push({ name: currentName, content: currentLines.join("\n") });
812
+ }
813
+ currentName = match[1].trim();
814
+ currentLines = [line];
815
+ } else if (currentName) {
816
+ currentLines.push(line);
817
+ }
818
+ }
819
+ if (currentName) {
820
+ sections.push({ name: currentName, content: currentLines.join("\n") });
821
+ }
822
+ return sections;
823
+ }
824
+
825
+ function reportInstallResult(result) {
826
+ const sections = [];
827
+ if (result.installed.length > 0) {
828
+ sections.push(
829
+ `Installed (${result.installed.length}):
830
+ ${result.installed.map((f) => ` + ${f}`).join("\n")}`
831
+ );
832
+ }
833
+ if (result.alwaysInstalled.length > 0) {
834
+ sections.push(
835
+ `Always installed (${result.alwaysInstalled.length}):
836
+ ${result.alwaysInstalled.map((f) => ` * ${f}`).join("\n")}`
837
+ );
838
+ }
839
+ if (result.skipped.length > 0) {
840
+ sections.push(
841
+ `Skipped (${result.skipped.length}):
842
+ ${result.skipped.map((f) => ` - ${f}`).join("\n")}`
843
+ );
844
+ }
845
+ clack__namespace.note(sections.join("\n\n"), "Install Summary");
846
+ const totalWritten = result.installed.length + result.alwaysInstalled.length;
847
+ clack__namespace.outro(`${totalWritten} files installed, ${result.skipped.length} skipped`);
848
+ }
849
+
850
+ async function promptAdapterSelection(available, detected) {
851
+ const value = await clack__namespace.multiselect({
852
+ message: "Which adapters do you want to install? (detected from package.json)",
853
+ options: available.map((a) => ({
854
+ value: a.name,
855
+ label: `${a.name} (${a.category})`
856
+ })),
857
+ initialValues: detected
858
+ });
859
+ if (clack__namespace.isCancel(value)) {
860
+ clack__namespace.cancel("Installation cancelled.");
861
+ process.exit(0);
862
+ }
863
+ return value;
864
+ }
865
+ async function promptInstallConfirm(capabilityName) {
866
+ const value = await clack__namespace.confirm({
867
+ message: `Install ${capabilityName} with the above configuration?`
868
+ });
869
+ if (clack__namespace.isCancel(value)) {
870
+ clack__namespace.cancel("Installation cancelled.");
871
+ process.exit(0);
872
+ }
873
+ return value;
874
+ }
875
+ async function promptConflictResolution() {
876
+ const value = await clack__namespace.select({
877
+ message: "Conflicts detected. How would you like to proceed?",
878
+ options: [
879
+ { value: "compare_and_continue", label: "Compare and continue (overwrite all)" },
880
+ { value: "selective", label: "Select files individually" },
881
+ { value: "different_path", label: "Choose a different path" },
882
+ { value: "abort", label: "Abort installation" }
883
+ ]
884
+ });
885
+ if (clack__namespace.isCancel(value)) {
886
+ clack__namespace.cancel("Installation cancelled.");
887
+ process.exit(0);
888
+ }
889
+ return value;
890
+ }
891
+ async function promptSkillConflict(skillName) {
892
+ const value = await clack__namespace.select({
893
+ message: `Skill "${skillName}" already exists. How would you like to proceed?`,
894
+ options: [
895
+ { value: "merge", label: "Merge (add missing sections)" },
896
+ { value: "overwrite", label: "Overwrite existing skill" },
897
+ { value: "skip", label: "Skip skill installation" }
898
+ ]
899
+ });
900
+ if (clack__namespace.isCancel(value)) {
901
+ clack__namespace.cancel("Installation cancelled.");
902
+ process.exit(0);
903
+ }
904
+ return value;
905
+ }
906
+ async function promptNewPath() {
907
+ const value = await clack__namespace.text({
908
+ message: "Enter a new target path for the capability:",
909
+ validate: (input) => {
910
+ if (!input || input.trim().length === 0) {
911
+ return "Path cannot be empty";
912
+ }
913
+ }
914
+ });
915
+ if (clack__namespace.isCancel(value)) {
916
+ clack__namespace.cancel("Installation cancelled.");
917
+ process.exit(0);
918
+ }
919
+ return value;
920
+ }
921
+
922
+ var __defProp$1 = Object.defineProperty;
923
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
924
+ var __publicField$1 = (obj, key, value) => {
925
+ __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
926
+ return value;
927
+ };
928
+ class FileWriteError extends Error {
929
+ constructor(message, filePath, suggestion, cause) {
930
+ super(message);
931
+ __publicField$1(this, "filePath");
932
+ __publicField$1(this, "suggestion");
933
+ __publicField$1(this, "cause");
934
+ this.name = "FileWriteError";
935
+ this.filePath = filePath;
936
+ this.suggestion = suggestion;
937
+ this.cause = cause;
938
+ }
939
+ }
940
+
941
+ var __defProp = Object.defineProperty;
942
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
943
+ var __publicField = (obj, key, value) => {
944
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
945
+ return value;
946
+ };
947
+ class MissingDependencyError extends Error {
948
+ constructor(missingCapabilities) {
949
+ super(`Required capabilities not installed: ${missingCapabilities.join(", ")}`);
950
+ __publicField(this, "missingCapabilities");
951
+ __publicField(this, "suggestion");
952
+ this.name = "MissingDependencyError";
953
+ this.missingCapabilities = missingCapabilities;
954
+ this.suggestion = `Run: ${missingCapabilities.map((c) => `backcap add ${c}`).join(" && ")}`;
955
+ }
956
+ }
957
+
958
+ const DEFAULT_REGISTRY_URL$1 = "https://backcap.dev";
959
+ const add = citty.defineCommand({
960
+ meta: {
961
+ name: "add",
962
+ description: "Install a capability or bridge from the registry"
963
+ },
964
+ args: {
965
+ capability: {
966
+ type: "positional",
967
+ required: true,
968
+ description: "Capability or bridge name to install"
969
+ }
970
+ },
971
+ async run({ args }) {
972
+ const cwd = process.cwd();
973
+ const itemName = args.capability;
974
+ intro();
975
+ if (!await configExists(cwd)) {
976
+ fail("No backcap.json found. Run `backcap init` first.");
977
+ return;
978
+ }
979
+ const configResult = await loadConfig(cwd);
980
+ if (configResult.isFail()) {
981
+ fail(configResult.unwrapError().message);
982
+ return;
983
+ }
984
+ const config = configResult.unwrap();
985
+ log.info(`Fetching ${itemName}...`);
986
+ let itemData;
987
+ let fetchedFromBridges = false;
988
+ try {
989
+ itemData = await ofetch.ofetch(`${DEFAULT_REGISTRY_URL$1}/dist/${itemName}.json`, {
990
+ timeout: 5e3
991
+ });
992
+ } catch {
993
+ try {
994
+ itemData = await ofetch.ofetch(`${DEFAULT_REGISTRY_URL$1}/dist/bridges/${itemName}.json`, {
995
+ timeout: 5e3
996
+ });
997
+ fetchedFromBridges = true;
998
+ } catch {
999
+ fail(`Could not fetch "${itemName}" from registry.`);
1000
+ return;
1001
+ }
1002
+ }
1003
+ const parsed = registryItem.registryItemSchema.safeParse(itemData);
1004
+ if (!parsed.success) {
1005
+ fail("Invalid data received from registry.");
1006
+ return;
1007
+ }
1008
+ const item = parsed.data;
1009
+ const itemVersion = item.version;
1010
+ const itemType = item.type;
1011
+ if (itemType === "bridge" || fetchedFromBridges) {
1012
+ await installBridge(cwd, config, item, itemVersion);
1013
+ return;
1014
+ }
1015
+ const capabilityName = itemName;
1016
+ const skillFiles = resolveSkillFiles(item);
1017
+ const availableAdapters = await detectAdapters(cwd, capabilityName);
1018
+ let selectedAdapters = [];
1019
+ if (availableAdapters.length > 0) {
1020
+ selectedAdapters = await promptAdapterSelection(
1021
+ availableAdapters.map((a) => ({ name: a.name, category: a.category })),
1022
+ availableAdapters.filter((a) => a.detected).map((a) => a.name)
1023
+ );
1024
+ }
1025
+ const files = item.files;
1026
+ const filesToWrite = files.filter((f) => typeof f.content === "string");
1027
+ const markers = {
1028
+ capabilities_path: config.paths.capabilities,
1029
+ adapters_path: config.paths.adapters,
1030
+ bridges_path: config.paths.bridges,
1031
+ skills_path: config.paths.skills
1032
+ };
1033
+ let capRoot = pathe.normalize(pathe.join(cwd, config.paths.capabilities, capabilityName));
1034
+ const incomingFiles = filesToWrite.map((f) => ({
1035
+ relativePath: f.path,
1036
+ content: f.content
1037
+ }));
1038
+ let useSelectiveInstall = false;
1039
+ let resolved = false;
1040
+ while (!resolved) {
1041
+ let report;
1042
+ try {
1043
+ report = await detectConflicts(capRoot, incomingFiles);
1044
+ } catch (err) {
1045
+ if (err instanceof ConflictDetectionError) {
1046
+ fail(`Conflict detection failed for ${err.filePath}: ${err.message}
1047
+ ${err.suggestion}`);
1048
+ return;
1049
+ }
1050
+ throw err;
1051
+ }
1052
+ if (report.files.every((f) => f.status === "identical")) {
1053
+ log.info("All files are identical. No changes needed.");
1054
+ outro("Nothing to update.");
1055
+ return;
1056
+ }
1057
+ if (!report.hasConflicts) {
1058
+ resolved = true;
1059
+ break;
1060
+ }
1061
+ renderConflictSummary(report);
1062
+ const action = await promptConflictResolution();
1063
+ if (action === "abort") {
1064
+ outro("Installation cancelled. No files were written.");
1065
+ return;
1066
+ }
1067
+ if (action === "different_path") {
1068
+ const newPath = await promptNewPath();
1069
+ capRoot = pathe.normalize(pathe.join(cwd, newPath, capabilityName));
1070
+ continue;
1071
+ }
1072
+ if (action === "selective") {
1073
+ try {
1074
+ const installResult = await selectiveInstall(report, skillFiles);
1075
+ const selectedPaths = /* @__PURE__ */ new Set([...installResult.installed, ...installResult.alwaysInstalled]);
1076
+ const selectedFiles = filesToWrite.filter((f) => selectedPaths.has(f.path));
1077
+ await writeCapabilityFiles(selectedFiles, { capabilityRoot: capRoot, markers });
1078
+ reportInstallResult(installResult);
1079
+ useSelectiveInstall = true;
1080
+ resolved = true;
1081
+ const version = itemVersion ?? "1.0.0";
1082
+ await updateConfigCapability(cwd, {
1083
+ name: capabilityName,
1084
+ version,
1085
+ adapters: selectedAdapters,
1086
+ partial: installResult.skipped.length > 0
1087
+ });
1088
+ } catch (err) {
1089
+ if (err instanceof InstallCancelledError) {
1090
+ outro("Installation cancelled. No files were written.");
1091
+ return;
1092
+ }
1093
+ if (err instanceof FileWriteError) {
1094
+ fail(`File write failed for ${err.filePath}: ${err.message}
1095
+ ${err.suggestion}`);
1096
+ return;
1097
+ }
1098
+ throw err;
1099
+ }
1100
+ break;
1101
+ }
1102
+ renderDetailedDiffs(report);
1103
+ resolved = true;
1104
+ }
1105
+ if (!useSelectiveInstall) {
1106
+ const confirmed = await promptInstallConfirm(capabilityName);
1107
+ if (!confirmed) {
1108
+ outro("Installation cancelled.");
1109
+ return;
1110
+ }
1111
+ await writeCapabilityFiles(filesToWrite, { capabilityRoot: capRoot, markers });
1112
+ log.success(`Capability files written to ${capRoot}`);
1113
+ const version = itemVersion ?? "1.0.0";
1114
+ await updateConfigCapability(cwd, {
1115
+ name: capabilityName,
1116
+ version,
1117
+ adapters: selectedAdapters
1118
+ });
1119
+ }
1120
+ for (const adapterName of selectedAdapters) {
1121
+ log.info(`Fetching adapter ${adapterName}...`);
1122
+ try {
1123
+ const adapterData = await ofetch.ofetch(`${DEFAULT_REGISTRY_URL$1}/dist/${adapterName}.json`, {
1124
+ timeout: 5e3
1125
+ });
1126
+ const adapterParsed = registryItem.registryItemSchema.safeParse(adapterData);
1127
+ if (!adapterParsed.success) {
1128
+ log.warn(`Invalid adapter data for "${adapterName}", skipping.`);
1129
+ continue;
1130
+ }
1131
+ const adapterItem = adapterParsed.data;
1132
+ const adapterFiles = adapterItem.files.filter((f) => typeof f.content === "string");
1133
+ const adapterType = adapterName.replace(`${capabilityName}-`, "");
1134
+ const category = adapterType === "prisma" ? "persistence" : "http";
1135
+ const adapterRoot = pathe.normalize(pathe.join(cwd, config.paths.adapters, category, adapterType, capabilityName));
1136
+ await writeCapabilityFiles(adapterFiles, { capabilityRoot: adapterRoot, markers });
1137
+ log.success(`Adapter files written to ${adapterRoot}`);
1138
+ } catch {
1139
+ log.warn(`Could not fetch adapter "${adapterName}", skipping.`);
1140
+ }
1141
+ }
1142
+ const skillsPath = pathe.normalize(pathe.join(cwd, resolveSkillsPath(config)));
1143
+ const capSkillFiles = extractSkillFiles(files);
1144
+ if (capSkillFiles.length > 0) {
1145
+ let coreSkillFiles = [];
1146
+ try {
1147
+ const coreData = await ofetch.ofetch(`${DEFAULT_REGISTRY_URL$1}/dist/skills/backcap-core.json`, {
1148
+ timeout: 5e3
1149
+ });
1150
+ const coreParsed = registryItem.registryItemSchema.safeParse(coreData);
1151
+ if (coreParsed.success) {
1152
+ coreSkillFiles = extractSkillFiles(
1153
+ coreParsed.data.files
1154
+ );
1155
+ }
1156
+ } catch {
1157
+ }
1158
+ await installSkill({
1159
+ skillsPath,
1160
+ capabilityName,
1161
+ skillFiles: capSkillFiles,
1162
+ coreSkillFiles,
1163
+ templateValues: markers,
1164
+ onConflict: promptSkillConflict
1165
+ });
1166
+ log.success(`Skill installed to ${skillsPath}/backcap-${capabilityName}/`);
1167
+ }
1168
+ const pm = detectPM(cwd);
1169
+ const npmDeps = item.dependencies ? Object.keys(item.dependencies) : [];
1170
+ const devDeps = item.peerDependencies ? Object.keys(item.peerDependencies) : [];
1171
+ if (npmDeps.length > 0) {
1172
+ log.info(`Installing dependencies: ${npmDeps.join(", ")}`);
1173
+ await installDeps(pm, npmDeps, cwd);
1174
+ }
1175
+ if (devDeps.length > 0) {
1176
+ log.info(`Installing dev dependencies: ${devDeps.join(", ")}`);
1177
+ await installDeps(pm, devDeps, cwd, true);
1178
+ }
1179
+ if (!useSelectiveInstall) {
1180
+ const version = itemVersion ?? "1.0.0";
1181
+ const lines = [
1182
+ `${capabilityName} v${version} installed successfully!`,
1183
+ "",
1184
+ ` Capability: ${capRoot}`
1185
+ ];
1186
+ if (selectedAdapters.length > 0) {
1187
+ lines.push(` Adapters: ${selectedAdapters.join(", ")}`);
1188
+ }
1189
+ lines.push("", " Next steps:");
1190
+ lines.push(` 1. Review the installed files in ${config.paths.capabilities}/${capabilityName}/`);
1191
+ lines.push(" 2. Run the test suite to verify: npx vitest run");
1192
+ lines.push(" 3. Check available bridges: backcap bridges");
1193
+ outro(lines.join("\n"));
1194
+ }
1195
+ }
1196
+ });
1197
+ async function installBridge(cwd, config, item, itemVersion) {
1198
+ const bridgeName = item.name;
1199
+ const version = itemVersion ?? "0.1.0";
1200
+ const installedBridgeNames = new Set(config.installed.bridges.map((b) => b.name));
1201
+ if (installedBridgeNames.has(bridgeName)) {
1202
+ log.info(`Bridge "${bridgeName}" is already installed.`);
1203
+ outro("Nothing to update.");
1204
+ return;
1205
+ }
1206
+ const requiredDeps = Array.isArray(item.dependencies) ? item.dependencies : item.dependencies ? Object.keys(item.dependencies) : [];
1207
+ if (requiredDeps.length > 0) {
1208
+ const installedCapNames = new Set(config.installed.capabilities.map((c) => c.name));
1209
+ const missing = requiredDeps.filter((dep) => !installedCapNames.has(dep));
1210
+ if (missing.length > 0) {
1211
+ const err = new MissingDependencyError(missing);
1212
+ fail(`${err.message}
1213
+ ${err.suggestion}`);
1214
+ return;
1215
+ }
1216
+ }
1217
+ const files = item.files;
1218
+ const filesToWrite = files.filter((f) => typeof f.content === "string");
1219
+ const markers = {
1220
+ capabilities_path: config.paths.capabilities,
1221
+ adapters_path: config.paths.adapters,
1222
+ bridges_path: config.paths.bridges,
1223
+ skills_path: config.paths.skills
1224
+ };
1225
+ const bridgeRoot = pathe.normalize(pathe.join(cwd, config.paths.bridges, bridgeName));
1226
+ const incomingFiles = filesToWrite.map((f) => ({
1227
+ relativePath: f.path,
1228
+ content: f.content
1229
+ }));
1230
+ try {
1231
+ const report = await detectConflicts(bridgeRoot, incomingFiles);
1232
+ if (report.files.every((f) => f.status === "identical")) {
1233
+ log.info("All bridge files are identical. No changes needed.");
1234
+ outro("Nothing to update.");
1235
+ return;
1236
+ }
1237
+ if (report.hasConflicts) {
1238
+ renderConflictSummary(report);
1239
+ const action = await promptConflictResolution();
1240
+ if (action === "abort") {
1241
+ outro("Installation cancelled. No files were written.");
1242
+ return;
1243
+ }
1244
+ if (action === "compare_and_continue") {
1245
+ renderDetailedDiffs(report);
1246
+ }
1247
+ if (action === "different_path") {
1248
+ outro("Bridge installation cancelled.");
1249
+ return;
1250
+ }
1251
+ }
1252
+ } catch (err) {
1253
+ if (err instanceof ConflictDetectionError) {
1254
+ fail(`Conflict detection failed for ${err.filePath}: ${err.message}
1255
+ ${err.suggestion}`);
1256
+ return;
1257
+ }
1258
+ throw err;
1259
+ }
1260
+ const confirmed = await promptInstallConfirm(bridgeName);
1261
+ if (!confirmed) {
1262
+ outro("Installation cancelled.");
1263
+ return;
1264
+ }
1265
+ await writeCapabilityFiles(filesToWrite, { capabilityRoot: bridgeRoot, markers });
1266
+ log.success(`Bridge files written to ${bridgeRoot}`);
1267
+ await updateConfigBridge(cwd, { name: bridgeName, version });
1268
+ const lines = [
1269
+ `Bridge ${bridgeName} v${version} installed successfully!`,
1270
+ "",
1271
+ ` Bridge: ${bridgeRoot}`,
1272
+ ` Connects: ${requiredDeps.join(" + ")}`,
1273
+ "",
1274
+ " Next steps:",
1275
+ ` 1. Review the bridge files in ${config.paths.bridges}/${bridgeName}/`,
1276
+ " 2. Wire the bridge in your application entry point",
1277
+ " 3. Run the test suite: npx vitest run"
1278
+ ];
1279
+ outro(lines.join("\n"));
1280
+ }
1281
+
1282
+ const DEFAULT_REGISTRY_URL = "https://backcap.dev";
1283
+ const bridges = citty.defineCommand({
1284
+ meta: {
1285
+ name: "bridges",
1286
+ description: "List available bridges between installed capabilities"
1287
+ },
1288
+ async run() {
1289
+ const cwd = process.cwd();
1290
+ clack__namespace.intro("backcap bridges");
1291
+ if (!await configExists(cwd)) {
1292
+ fail("No backcap.json found. Run `backcap init` first.");
1293
+ return;
1294
+ }
1295
+ const configResult = await loadConfig(cwd);
1296
+ if (configResult.isFail()) {
1297
+ fail(configResult.unwrapError().message);
1298
+ return;
1299
+ }
1300
+ const config = configResult.unwrap();
1301
+ const installedCaps = config.installed?.capabilities ?? [];
1302
+ const installedCapNames = new Set(installedCaps.map((c) => c.name));
1303
+ if (installedCapNames.size === 0) {
1304
+ clack__namespace.log.info("No capabilities installed yet. Run `backcap add <capability>` to get started.");
1305
+ clack__namespace.outro("No bridges available.");
1306
+ return;
1307
+ }
1308
+ let catalog;
1309
+ try {
1310
+ catalog = await ofetch.ofetch(`${DEFAULT_REGISTRY_URL}/dist/bridges/index.json`, {
1311
+ timeout: 5e3
1312
+ });
1313
+ } catch {
1314
+ fail("Could not fetch bridge catalog from registry.");
1315
+ return;
1316
+ }
1317
+ const compatible = catalog.bridges.filter(
1318
+ (b) => b.dependencies.every((dep) => installedCapNames.has(dep))
1319
+ );
1320
+ if (compatible.length === 0) {
1321
+ clack__namespace.log.info("No compatible bridges found for your installed capabilities.");
1322
+ clack__namespace.outro("Install more capabilities to unlock bridges.");
1323
+ return;
1324
+ }
1325
+ const installedBridges = new Set(
1326
+ (config.installed?.bridges ?? []).map((b) => b.name)
1327
+ );
1328
+ const lines = compatible.map((b) => {
1329
+ const status = installedBridges.has(b.name) ? "installed" : "available";
1330
+ return ` ${b.name} \u2014 ${b.description}
1331
+ Dependencies: ${b.dependencies.join(", ")} | Status: ${status}`;
1332
+ });
1333
+ clack__namespace.note(lines.join("\n\n"), "Available Bridges");
1334
+ const availableCount = compatible.filter((b) => !installedBridges.has(b.name)).length;
1335
+ if (availableCount > 0) {
1336
+ clack__namespace.log.info(`Run \`backcap add <bridge-name>\` to install a bridge.`);
1337
+ }
1338
+ clack__namespace.outro(`${compatible.length} bridge(s) found.`);
1339
+ }
1340
+ });
1341
+
1342
+ const main = citty.defineCommand({
1343
+ meta: {
1344
+ name: "backcap",
1345
+ version: "0.0.1",
1346
+ description: "Backcap \u2014 capability registry CLI"
1347
+ },
1348
+ subCommands: {
1349
+ init,
1350
+ list,
1351
+ add,
1352
+ bridges
1353
+ }
1354
+ });
1355
+
1356
+ citty.runMain(main);