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