loor-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1083 @@
1
+ import {
2
+ PackageRegistry,
3
+ addAnnotatedBlock,
4
+ detectProject,
5
+ ensureCache,
6
+ getAppsDir,
7
+ getPackagesDir,
8
+ getRegistryDir,
9
+ mergeDependencies,
10
+ readConfig,
11
+ removeBetweenAnnotations,
12
+ removeDependencies,
13
+ replaceInFile,
14
+ writeConfig
15
+ } from "./chunk-E6WOLYO3.js";
16
+ import {
17
+ enableDebug,
18
+ log
19
+ } from "./chunk-AUVNDYJL.js";
20
+
21
+ // src/cli.ts
22
+ import { Command as Command7 } from "commander";
23
+
24
+ // src/commands/init.ts
25
+ import path4 from "path";
26
+ import fs4 from "fs-extra";
27
+ import * as p2 from "@clack/prompts";
28
+ import { Command } from "commander";
29
+
30
+ // src/registry/dependencyResolver.ts
31
+ function resolveDependencies(packageNames, registry) {
32
+ const resolved = /* @__PURE__ */ new Set();
33
+ const visiting = /* @__PURE__ */ new Set();
34
+ const order = [];
35
+ function visit(name) {
36
+ if (resolved.has(name)) return;
37
+ if (visiting.has(name)) {
38
+ throw new Error(`Circular dependency detected: ${name}`);
39
+ }
40
+ const manifest = registry.get(name);
41
+ if (!manifest) {
42
+ throw new Error(`Package not found: ${name}`);
43
+ }
44
+ visiting.add(name);
45
+ for (const dep of manifest.requires) {
46
+ visit(dep);
47
+ }
48
+ visiting.delete(name);
49
+ resolved.add(name);
50
+ order.push(name);
51
+ }
52
+ for (const name of packageNames) {
53
+ visit(name);
54
+ }
55
+ return order;
56
+ }
57
+ function resolveWithTransitiveDeps(selected, registry) {
58
+ const allDeps = /* @__PURE__ */ new Set();
59
+ function collectDeps(name) {
60
+ if (allDeps.has(name)) return;
61
+ allDeps.add(name);
62
+ const manifest = registry.get(name);
63
+ if (!manifest) return;
64
+ for (const dep of manifest.requires) {
65
+ collectDeps(dep);
66
+ }
67
+ }
68
+ for (const name of selected) {
69
+ collectDeps(name);
70
+ }
71
+ return resolveDependencies(Array.from(allDeps), registry);
72
+ }
73
+
74
+ // src/generators/projectGenerator.ts
75
+ import path from "path";
76
+ import fs from "fs-extra";
77
+
78
+ // src/generators/mcpConfigGenerator.ts
79
+ import { execFile } from "child_process";
80
+ import { promisify } from "util";
81
+ var execFileAsync = promisify(execFile);
82
+ async function generateMcpConfig(projectDir) {
83
+ try {
84
+ await execFileAsync("claude", [
85
+ "mcp",
86
+ "add",
87
+ "--transport",
88
+ "stdio",
89
+ "--scope",
90
+ "project",
91
+ "loor",
92
+ "--",
93
+ "npx",
94
+ "loor",
95
+ "--mcp"
96
+ ], { cwd: projectDir });
97
+ } catch (err) {
98
+ const msg = err instanceof Error ? err.message : String(err);
99
+ if (msg.includes("ENOENT")) {
100
+ log.warn("Claude Code CLI not found. Install it to enable MCP integration.");
101
+ log.dim(" npm install -g @anthropic-ai/claude-code");
102
+ } else {
103
+ log.warn(`Could not register MCP server: ${msg}`);
104
+ }
105
+ }
106
+ }
107
+
108
+ // src/generators/projectGenerator.ts
109
+ async function generateProject(projectDir, projectName) {
110
+ const registryDir = await getRegistryDir();
111
+ log.step(`Creating project directory: ${projectName}`);
112
+ await fs.ensureDir(projectDir);
113
+ await fs.ensureDir(path.join(projectDir, "apps"));
114
+ await fs.ensureDir(path.join(projectDir, "packages"));
115
+ const filesToCopy = [
116
+ "turbo.json",
117
+ "pnpm-workspace.yaml",
118
+ "tsconfig.json",
119
+ "eslint.config.mjs",
120
+ ".env.example"
121
+ ];
122
+ for (const file of filesToCopy) {
123
+ const src = path.join(registryDir, file);
124
+ if (await fs.pathExists(src)) {
125
+ await fs.copy(src, path.join(projectDir, file));
126
+ }
127
+ }
128
+ const gitignoreSrc = path.join(registryDir, ".gitignore");
129
+ if (await fs.pathExists(gitignoreSrc)) {
130
+ await fs.copy(gitignoreSrc, path.join(projectDir, ".gitignore"));
131
+ }
132
+ const pkg = {
133
+ name: projectName,
134
+ version: "0.0.1",
135
+ private: true,
136
+ scripts: {
137
+ build: "turbo build",
138
+ dev: "turbo dev",
139
+ lint: "turbo lint",
140
+ format: 'prettier --write "**/*.{ts,tsx,md}"'
141
+ },
142
+ devDependencies: {
143
+ "@workspace/eslintConfig": "workspace:*",
144
+ "@workspace/typescriptConfig": "workspace:*",
145
+ prettier: "^3.6.2",
146
+ turbo: "^2.5.5",
147
+ typescript: "5.7.3"
148
+ },
149
+ packageManager: "pnpm@10.4.1",
150
+ engines: { node: ">=20" }
151
+ };
152
+ await fs.writeJson(path.join(projectDir, "package.json"), pkg, { spaces: 2 });
153
+ await generateMcpConfig(projectDir);
154
+ log.success("Project skeleton created");
155
+ }
156
+
157
+ // src/generators/appGenerator.ts
158
+ import path2 from "path";
159
+ import fs2 from "fs-extra";
160
+ async function generateApp(projectDir, appType, appName, projectName) {
161
+ const appsDir = await getAppsDir();
162
+ const sourceDir = path2.join(appsDir, appType);
163
+ const destDir = path2.join(projectDir, "apps", appName);
164
+ if (await fs2.pathExists(destDir)) {
165
+ throw new Error(`App directory already exists: apps/${appName}`);
166
+ }
167
+ log.step(`Generating ${appType} app: apps/${appName}`);
168
+ await fs2.copy(sourceDir, destDir, {
169
+ filter: (src) => {
170
+ const rel = path2.relative(sourceDir, src);
171
+ if (rel === "") return true;
172
+ if (rel.startsWith("node_modules") || rel.startsWith("dist") || rel.startsWith(".turbo")) return false;
173
+ return true;
174
+ }
175
+ });
176
+ await replaceInFile(
177
+ path2.join(destDir, "package.json"),
178
+ `@loor/${appType}`,
179
+ `@${projectName}/${appName}`
180
+ );
181
+ await replaceInFile(
182
+ path2.join(destDir, "package.json"),
183
+ "@loor/",
184
+ `@${projectName}/`
185
+ );
186
+ const authClientPath = path2.join(destDir, "src", "auth", "client.ts");
187
+ if (await fs2.pathExists(authClientPath)) {
188
+ await replaceInFile(authClientPath, "keyPrefix: 'loor.'", `keyPrefix: '${projectName}.'`);
189
+ }
190
+ log.success(`App created: apps/${appName}`);
191
+ }
192
+
193
+ // src/generators/packageGenerator.ts
194
+ import path3 from "path";
195
+ import fs3 from "fs-extra";
196
+ async function removePackageArtifacts(projectDir, manifest) {
197
+ const appsDir = path3.join(projectDir, "apps");
198
+ if (await fs3.pathExists(appsDir)) {
199
+ const apps = await fs3.readdir(appsDir, { withFileTypes: true });
200
+ for (const app of apps) {
201
+ if (!app.isDirectory()) continue;
202
+ if (manifest.apiInfra) {
203
+ for (const file of manifest.apiInfra) {
204
+ const filePath = path3.join(appsDir, app.name, file.path);
205
+ if (await fs3.pathExists(filePath)) {
206
+ await fs3.remove(filePath);
207
+ }
208
+ }
209
+ }
210
+ if (manifest.clientSetup) {
211
+ for (const file of manifest.clientSetup) {
212
+ const filePath = path3.join(appsDir, app.name, file.path);
213
+ if (await fs3.pathExists(filePath)) {
214
+ await fs3.remove(filePath);
215
+ }
216
+ }
217
+ }
218
+ const appPkgJsonPath = path3.join(appsDir, app.name, "package.json");
219
+ if (await fs3.pathExists(appPkgJsonPath)) {
220
+ const depsToRemove = [`@workspace/${manifest.name}`];
221
+ if (manifest.appDependencies?.api) {
222
+ depsToRemove.push(...Object.keys(manifest.appDependencies.api));
223
+ }
224
+ if (manifest.appDependencies?.client) {
225
+ depsToRemove.push(...Object.keys(manifest.appDependencies.client));
226
+ }
227
+ await removeDependencies(appPkgJsonPath, depsToRemove);
228
+ }
229
+ const configPath = path3.join(appsDir, app.name, "src", "config", "index.ts");
230
+ await removeBetweenAnnotations(configPath, manifest.name);
231
+ }
232
+ }
233
+ const envPath = path3.join(projectDir, ".env.example");
234
+ await removeBetweenAnnotations(envPath, manifest.name);
235
+ const pkgDir = path3.join(projectDir, "packages", manifest.name);
236
+ if (await fs3.pathExists(pkgDir)) {
237
+ await fs3.remove(pkgDir);
238
+ }
239
+ log.dim(` - Removed: ${manifest.name}`);
240
+ }
241
+ async function addPackage(projectDir, manifest) {
242
+ const srcPackagesDir = await getPackagesDir();
243
+ const srcPackageDir = path3.join(srcPackagesDir, manifest.name);
244
+ const destPackageDir = path3.join(projectDir, "packages", manifest.name);
245
+ if (!await fs3.pathExists(destPackageDir)) {
246
+ log.step(`Adding package: ${manifest.name}`);
247
+ await fs3.copy(srcPackageDir, destPackageDir, {
248
+ filter: (src) => {
249
+ const rel = path3.relative(srcPackageDir, src);
250
+ if (rel === "") return true;
251
+ if (rel.startsWith("node_modules") || rel.startsWith("dist")) return false;
252
+ if (rel === "loor.json") return false;
253
+ return true;
254
+ }
255
+ });
256
+ }
257
+ const referenceAppsDir = await getAppsDir();
258
+ const appsDir = path3.join(projectDir, "apps");
259
+ if (await fs3.pathExists(appsDir)) {
260
+ const apps = await fs3.readdir(appsDir, { withFileTypes: true });
261
+ for (const app of apps) {
262
+ if (!app.isDirectory()) continue;
263
+ const appPkgJsonPath = path3.join(appsDir, app.name, "package.json");
264
+ if (!await fs3.pathExists(appPkgJsonPath)) continue;
265
+ const appPkg = await fs3.readJson(appPkgJsonPath);
266
+ const isApi = appPkg.dependencies?.express || appPkg.dependencies?.mongoose;
267
+ const isClient = appPkg.dependencies?.react;
268
+ await mergeDependencies(appPkgJsonPath, {
269
+ [`@workspace/${manifest.name}`]: "workspace:*"
270
+ });
271
+ if (manifest.appDependencies) {
272
+ if (isApi && manifest.appDependencies.api) {
273
+ await mergeDependencies(appPkgJsonPath, manifest.appDependencies.api);
274
+ }
275
+ if (isClient && manifest.appDependencies.client) {
276
+ await mergeDependencies(appPkgJsonPath, manifest.appDependencies.client);
277
+ }
278
+ }
279
+ if (isApi && manifest.apiInfra) {
280
+ for (const file of manifest.apiInfra) {
281
+ const destPath = path3.join(appsDir, app.name, file.path);
282
+ if (await fs3.pathExists(destPath)) continue;
283
+ const srcPath = path3.join(referenceAppsDir, "api", file.path);
284
+ if (await fs3.pathExists(srcPath)) {
285
+ await fs3.ensureDir(path3.dirname(destPath));
286
+ await fs3.copy(srcPath, destPath);
287
+ log.dim(` + ${file.path} (${file.description})`);
288
+ }
289
+ }
290
+ }
291
+ if (isClient && manifest.clientSetup) {
292
+ for (const file of manifest.clientSetup) {
293
+ const destPath = path3.join(appsDir, app.name, file.path);
294
+ if (await fs3.pathExists(destPath)) continue;
295
+ const srcPath = path3.join(referenceAppsDir, "web-client", file.path);
296
+ if (await fs3.pathExists(srcPath)) {
297
+ await fs3.ensureDir(path3.dirname(destPath));
298
+ await fs3.copy(srcPath, destPath);
299
+ log.dim(` + ${file.path} (${file.description})`);
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+ if (manifest.configKeys) {
306
+ await addConfigBlock(projectDir, manifest);
307
+ }
308
+ if (manifest.envVars) {
309
+ await addEnvBlock(projectDir, manifest);
310
+ }
311
+ log.success(`Package ready: ${manifest.name}`);
312
+ }
313
+ async function addConfigBlock(projectDir, manifest) {
314
+ if (!manifest.configKeys) return;
315
+ const appsDir = path3.join(projectDir, "apps");
316
+ if (!await fs3.pathExists(appsDir)) return;
317
+ const apps = await fs3.readdir(appsDir, { withFileTypes: true });
318
+ for (const app of apps) {
319
+ if (!app.isDirectory()) continue;
320
+ const configPath = path3.join(appsDir, app.name, "src", "config", "index.ts");
321
+ if (!await fs3.pathExists(configPath)) continue;
322
+ const block = buildConfigBlock(manifest.configKeys);
323
+ await addAnnotatedBlock(configPath, manifest.name, block, "//");
324
+ }
325
+ }
326
+ async function addEnvBlock(projectDir, manifest) {
327
+ if (!manifest.envVars) return;
328
+ const envPath = path3.join(projectDir, ".env.example");
329
+ if (!await fs3.pathExists(envPath)) return;
330
+ const lines = [];
331
+ for (const envVar of manifest.envVars) {
332
+ lines.push(`${envVar.key}=${envVar.defaultValue}`);
333
+ }
334
+ await addAnnotatedBlock(envPath, manifest.name, lines.join("\n"), "#");
335
+ }
336
+ function buildConfigBlock(keys) {
337
+ const tree = {};
338
+ for (const key of keys) {
339
+ const parts = key.path.split(".");
340
+ let current = tree;
341
+ for (let i = 0; i < parts.length - 1; i++) {
342
+ if (!current[parts[i]] || typeof current[parts[i]] !== "object") {
343
+ current[parts[i]] = {};
344
+ }
345
+ current = current[parts[i]];
346
+ }
347
+ current[parts[parts.length - 1]] = key.expression;
348
+ }
349
+ return stringifyConfigTree(tree, 1);
350
+ }
351
+ function stringifyConfigTree(tree, depth) {
352
+ const indent = " ".repeat(depth);
353
+ const lines = [];
354
+ for (const [key, value] of Object.entries(tree)) {
355
+ if (typeof value === "string") {
356
+ lines.push(`${indent}${key}: ${value},`);
357
+ } else {
358
+ lines.push(`${indent}${key}: {`);
359
+ lines.push(stringifyConfigTree(value, depth + 1));
360
+ lines.push(`${indent}},`);
361
+ }
362
+ }
363
+ return lines.join("\n");
364
+ }
365
+
366
+ // src/utils/prompt.ts
367
+ import * as p from "@clack/prompts";
368
+ async function promptProjectName(defaultName) {
369
+ const name = await p.text({
370
+ message: "Project name:",
371
+ placeholder: defaultName || "my-app",
372
+ validate: (value) => {
373
+ if (!value.trim()) return "Project name is required";
374
+ if (!/^[a-z0-9-]+$/.test(value)) return "Use lowercase letters, numbers, and hyphens only";
375
+ return void 0;
376
+ }
377
+ });
378
+ if (p.isCancel(name)) {
379
+ p.cancel("Operation cancelled.");
380
+ process.exit(0);
381
+ }
382
+ return name;
383
+ }
384
+ async function promptAppTypes() {
385
+ const result = await p.multiselect({
386
+ message: "Which app types do you want to create?",
387
+ options: [
388
+ { value: "api", label: "API (Express + MongoDB)", hint: "REST API server" },
389
+ { value: "web-client", label: "Web Client (React + Vite)", hint: "Admin panel" }
390
+ ],
391
+ required: true
392
+ });
393
+ if (p.isCancel(result)) {
394
+ p.cancel("Operation cancelled.");
395
+ process.exit(0);
396
+ }
397
+ return result;
398
+ }
399
+ async function promptPackages(packages, appTypes) {
400
+ const compatTypes = appTypes.map((t) => t === "web-client" ? "client" : t);
401
+ const compatible = packages.filter(
402
+ (pkg) => pkg.compatibility.some((c) => compatTypes.includes(c))
403
+ );
404
+ if (compatible.length === 0) return [];
405
+ const grouped = {
406
+ core: compatible.filter((p4) => p4.category === "core"),
407
+ server: compatible.filter((p4) => p4.category === "server"),
408
+ client: compatible.filter((p4) => p4.category === "client"),
409
+ config: compatible.filter((p4) => p4.category === "config")
410
+ };
411
+ const options = [
412
+ ...grouped.core.map((p4) => ({ value: p4.name, label: `[core] ${p4.name}`, hint: p4.description })),
413
+ ...grouped.server.map((p4) => ({ value: p4.name, label: `[server] ${p4.name}`, hint: p4.description })),
414
+ ...grouped.client.map((p4) => ({ value: p4.name, label: `[client] ${p4.name}`, hint: p4.description })),
415
+ ...grouped.config.map((p4) => ({ value: p4.name, label: `[config] ${p4.name}`, hint: p4.description }))
416
+ ];
417
+ const result = await p.multiselect({
418
+ message: "Select packages to include:",
419
+ options,
420
+ required: false
421
+ });
422
+ if (p.isCancel(result)) {
423
+ p.cancel("Operation cancelled.");
424
+ process.exit(0);
425
+ }
426
+ return result;
427
+ }
428
+ async function promptAppName(appType) {
429
+ const defaultName = appType === "api" ? "api" : "admin";
430
+ const name = await p.text({
431
+ message: `Name for the ${appType} app:`,
432
+ placeholder: defaultName,
433
+ initialValue: defaultName,
434
+ validate: (value) => {
435
+ if (!value.trim()) return "App name is required";
436
+ if (!/^[a-z0-9-]+$/.test(value)) return "Use lowercase letters, numbers, and hyphens only";
437
+ return void 0;
438
+ }
439
+ });
440
+ if (p.isCancel(name)) {
441
+ p.cancel("Operation cancelled.");
442
+ process.exit(0);
443
+ }
444
+ return name;
445
+ }
446
+
447
+ // src/commands/init.ts
448
+ var REQUIRED_PACKAGES = {
449
+ api: ["auth", "base", "expressRouteKit", "logger", "eslintConfig", "typescriptConfig"],
450
+ "web-client": ["auth", "base", "storage", "ui", "i18n", "routerProvider", "commandMenu", "apiClient", "eslintConfig", "typescriptConfig"]
451
+ };
452
+ var VALID_APP_TYPES = ["api", "web-client"];
453
+ function parseAppsFlag(raw) {
454
+ const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
455
+ const apps = [];
456
+ for (const part of parts) {
457
+ const [typePart, namePart] = part.split(":");
458
+ const type = typePart.trim();
459
+ if (!VALID_APP_TYPES.includes(type)) {
460
+ log.error(`Invalid app type: "${type}". Must be one of: ${VALID_APP_TYPES.join(", ")}`);
461
+ process.exit(1);
462
+ }
463
+ const appType = type;
464
+ const defaultName = appType === "api" ? "api" : "admin";
465
+ const name = namePart?.trim() || defaultName;
466
+ if (!/^[a-z0-9-]+$/.test(name)) {
467
+ log.error(`Invalid app name: "${name}". Use lowercase letters, numbers, and hyphens only.`);
468
+ process.exit(1);
469
+ }
470
+ apps.push({ type: appType, name });
471
+ }
472
+ if (apps.length === 0) {
473
+ log.error("--apps requires at least one app type.");
474
+ process.exit(1);
475
+ }
476
+ return apps;
477
+ }
478
+ function parsePackagesFlag(raw) {
479
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
480
+ }
481
+ var initCommand = new Command("init").description("Create a new loor monorepo project").argument("[name]", "Project name (lowercase, hyphens allowed)").option("--apps <apps>", "App types with optional names (e.g., api:backend,web-client:dashboard)").option("--packages <packages>", "Optional packages to include (comma-separated)").option("--no-packages", "Skip optional package selection (install only required packages)").addHelpText("after", `
482
+ Examples:
483
+ $ loor init my-saas # Interactive mode
484
+ $ loor init my-saas --apps api,web-client # With default app names
485
+ $ loor init my-saas --apps api:backend,web-client:admin --packages mediaKit,mailer
486
+ $ loor init my-saas --apps api --no-packages # API only, no optional packages
487
+ `).action(async (name, opts) => {
488
+ await ensureCache();
489
+ p2.intro("Create a new loor project");
490
+ const projectName = name || await promptProjectName();
491
+ const projectDir = path4.resolve(process.cwd(), projectName);
492
+ if (await fs4.pathExists(projectDir)) {
493
+ log.error(`Directory already exists: ${projectName}`);
494
+ process.exit(1);
495
+ }
496
+ let apps;
497
+ if (opts.apps) {
498
+ apps = parseAppsFlag(opts.apps);
499
+ log.info(`Apps: ${apps.map((a) => `${a.type}:${a.name}`).join(", ")}`);
500
+ } else {
501
+ const appTypes2 = await promptAppTypes();
502
+ apps = [];
503
+ for (const appType of appTypes2) {
504
+ const appName = await promptAppName(appType);
505
+ apps.push({ type: appType, name: appName });
506
+ }
507
+ }
508
+ const appTypes = apps.map((a) => a.type);
509
+ const requiredSet = /* @__PURE__ */ new Set();
510
+ for (const appType of appTypes) {
511
+ for (const pkg of REQUIRED_PACKAGES[appType]) {
512
+ requiredSet.add(pkg);
513
+ }
514
+ }
515
+ const registry = new PackageRegistry(await getPackagesDir());
516
+ await registry.load();
517
+ const allPackages = registry.getAll();
518
+ let selectedPackageNames;
519
+ if (opts.packages === false) {
520
+ selectedPackageNames = [];
521
+ log.info("Skipping optional packages (--no-packages)");
522
+ } else if (typeof opts.packages === "string") {
523
+ selectedPackageNames = parsePackagesFlag(opts.packages);
524
+ for (const pkgName of selectedPackageNames) {
525
+ if (!registry.get(pkgName)) {
526
+ log.error(`Package not found: "${pkgName}". Run "loor list" to see available packages.`);
527
+ process.exit(1);
528
+ }
529
+ }
530
+ log.info(`Selected packages: ${selectedPackageNames.join(", ")}`);
531
+ } else {
532
+ const optionalPackages = allPackages.filter((pkg) => !requiredSet.has(pkg.name));
533
+ selectedPackageNames = await promptPackages(optionalPackages, appTypes);
534
+ }
535
+ const allSelected = [...requiredSet, ...selectedPackageNames];
536
+ const resolvedPackages = resolveWithTransitiveDeps(allSelected, registry);
537
+ const userPicked = new Set(selectedPackageNames);
538
+ const autoAdded = resolvedPackages.filter((n) => !userPicked.has(n) && !requiredSet.has(n));
539
+ if (autoAdded.length > 0) {
540
+ log.info(`Auto-adding transitive dependencies: ${autoAdded.join(", ")}`);
541
+ }
542
+ log.info(`Required by apps: ${[...requiredSet].join(", ")}`);
543
+ const resolvedSet = new Set(resolvedPackages);
544
+ const spinner3 = p2.spinner();
545
+ spinner3.start("Creating project skeleton");
546
+ await generateProject(projectDir, projectName);
547
+ spinner3.stop("Project skeleton created");
548
+ for (const app of apps) {
549
+ spinner3.start(`Generating ${app.type} app: ${app.name}`);
550
+ await generateApp(projectDir, app.type, app.name, projectName);
551
+ spinner3.stop(`App created: ${app.name}`);
552
+ }
553
+ spinner3.start("Copying packages");
554
+ const srcPackagesDir = await getPackagesDir();
555
+ const destPackagesDir = path4.join(projectDir, "packages");
556
+ const allPkgNames = allPackages.map((p4) => p4.name);
557
+ for (const pkgName of allPkgNames) {
558
+ const srcDir = path4.join(srcPackagesDir, pkgName);
559
+ const destDir = path4.join(destPackagesDir, pkgName);
560
+ if (await fs4.pathExists(srcDir)) {
561
+ await fs4.copy(srcDir, destDir, {
562
+ filter: (src) => {
563
+ const rel = path4.relative(srcDir, src);
564
+ if (rel === "") return true;
565
+ if (rel.startsWith("node_modules") || rel.startsWith("dist")) return false;
566
+ if (rel === "loor.json") return false;
567
+ return true;
568
+ }
569
+ });
570
+ }
571
+ }
572
+ spinner3.stop("Packages copied");
573
+ const unselected = allPkgNames.filter((n) => !resolvedSet.has(n));
574
+ if (unselected.length > 0) {
575
+ spinner3.start("Removing unselected packages");
576
+ for (const pkgName of unselected) {
577
+ const manifest = registry.get(pkgName);
578
+ if (!manifest) continue;
579
+ await removePackageArtifacts(projectDir, manifest);
580
+ }
581
+ spinner3.stop("Cleaned up unselected packages");
582
+ }
583
+ spinner3.start("Applying project name");
584
+ await applyProjectName(projectDir, projectName);
585
+ spinner3.stop("Project name applied");
586
+ p2.outro("Project created successfully!");
587
+ console.log("");
588
+ log.info("Next steps:");
589
+ log.dim(` cd ${projectName}`);
590
+ log.dim(" pnpm install");
591
+ log.dim(" pnpm dev");
592
+ });
593
+ async function applyProjectName(projectDir, projectName) {
594
+ const appsDir = path4.join(projectDir, "apps");
595
+ if (await fs4.pathExists(appsDir)) {
596
+ const apps = await fs4.readdir(appsDir, { withFileTypes: true });
597
+ for (const app of apps) {
598
+ if (!app.isDirectory()) continue;
599
+ const pkgJsonPath = path4.join(appsDir, app.name, "package.json");
600
+ await replaceInFile(pkgJsonPath, "@loor/", `@${projectName}/`);
601
+ const authClientPath = path4.join(appsDir, app.name, "src", "auth", "client.ts");
602
+ await replaceInFile(authClientPath, "keyPrefix: 'loor.'", `keyPrefix: '${projectName}.'`);
603
+ }
604
+ }
605
+ }
606
+
607
+ // src/commands/add.ts
608
+ import path5 from "path";
609
+ import fs5 from "fs-extra";
610
+ import * as p3 from "@clack/prompts";
611
+ import { Command as Command2 } from "commander";
612
+ var REQUIRED_PACKAGES2 = {
613
+ api: ["auth", "base", "expressRouteKit", "logger", "eslintConfig", "typescriptConfig"],
614
+ "web-client": ["auth", "base", "storage", "ui", "i18n", "routerProvider", "commandMenu", "apiClient", "eslintConfig", "typescriptConfig"]
615
+ };
616
+ var addApp = new Command2("app").description("Add a new app to the project").argument("<type>", "App type: api or web-client").argument("[name]", "Custom app name (e.g. backend, dashboard)").addHelpText("after", `
617
+ Examples:
618
+ $ loor add app api # Prompts for name if "api" already exists
619
+ $ loor add app api backend # Creates apps/backend (api type)
620
+ $ loor add app web-client # Prompts for name if "admin" already exists
621
+ $ loor add app web-client dash # Creates apps/dash (web-client type)
622
+ `).action(async (type, name) => {
623
+ await ensureCache();
624
+ if (type !== "api" && type !== "web-client") {
625
+ log.error(`Invalid app type: "${type}". Must be "api" or "web-client".`);
626
+ process.exit(1);
627
+ }
628
+ const projectDir = process.cwd();
629
+ if (!await detectProject(projectDir)) {
630
+ log.error("Not a loor project. Run this command from the project root.");
631
+ process.exit(1);
632
+ }
633
+ if (name && !/^[a-z0-9-]+$/.test(name)) {
634
+ log.error("App name must be lowercase letters, numbers, and hyphens only.");
635
+ process.exit(1);
636
+ }
637
+ const appType = type;
638
+ let appName = name || (appType === "api" ? "api" : "admin");
639
+ const destDir = path5.join(projectDir, "apps", appName);
640
+ if (!name && await fs5.pathExists(destDir)) {
641
+ log.warn(`apps/${appName} already exists.`);
642
+ const customName = await p3.text({
643
+ message: `Enter a name for the new ${appType} app:`,
644
+ placeholder: appType === "api" ? "backend" : "dashboard",
645
+ validate: (value) => {
646
+ if (!value.trim()) return "App name is required";
647
+ if (!/^[a-z0-9-]+$/.test(value)) return "Use lowercase letters, numbers, and hyphens only";
648
+ return void 0;
649
+ }
650
+ });
651
+ if (p3.isCancel(customName)) {
652
+ p3.cancel("Operation cancelled.");
653
+ process.exit(0);
654
+ }
655
+ appName = customName;
656
+ if (await fs5.pathExists(path5.join(projectDir, "apps", appName))) {
657
+ log.error(`apps/${appName} already exists. Choose a different name.`);
658
+ process.exit(1);
659
+ }
660
+ }
661
+ if (name && await fs5.pathExists(path5.join(projectDir, "apps", appName))) {
662
+ log.error(`apps/${appName} already exists. Choose a different name.`);
663
+ process.exit(1);
664
+ }
665
+ const rootPkg = await fs5.readJson(path5.join(projectDir, "package.json"));
666
+ const projectName = rootPkg.name || "app";
667
+ p3.intro(`Adding ${appType} app: ${appName}`);
668
+ const registry = new PackageRegistry(await getPackagesDir());
669
+ await registry.load();
670
+ const spinner3 = p3.spinner();
671
+ spinner3.start(`Generating ${appType} app`);
672
+ await generateApp(projectDir, appType, appName, projectName);
673
+ spinner3.stop(`App created: ${appName}`);
674
+ const required = REQUIRED_PACKAGES2[appType];
675
+ const resolved = resolveWithTransitiveDeps(required, registry);
676
+ const packagesDir = path5.join(projectDir, "packages");
677
+ const missing = [];
678
+ for (const pkgName of resolved) {
679
+ if (!await fs5.pathExists(path5.join(packagesDir, pkgName))) {
680
+ missing.push(pkgName);
681
+ }
682
+ }
683
+ if (missing.length > 0) {
684
+ log.info(`Installing required packages: ${missing.join(", ")}`);
685
+ for (const pkgName of missing) {
686
+ const manifest = registry.get(pkgName);
687
+ if (!manifest) continue;
688
+ spinner3.start(`Adding required package: ${pkgName}`);
689
+ await addPackage(projectDir, manifest);
690
+ spinner3.stop(`Package ready: ${pkgName}`);
691
+ }
692
+ }
693
+ const existingPkgs = /* @__PURE__ */ new Set();
694
+ if (await fs5.pathExists(packagesDir)) {
695
+ const entries = await fs5.readdir(packagesDir, { withFileTypes: true });
696
+ for (const entry of entries) {
697
+ if (entry.isDirectory()) existingPkgs.add(entry.name);
698
+ }
699
+ }
700
+ const allPackages = registry.getAll();
701
+ for (const pkg of allPackages) {
702
+ if (!existingPkgs.has(pkg.name)) {
703
+ await removePackageArtifacts(projectDir, pkg);
704
+ }
705
+ }
706
+ p3.outro("App added successfully!");
707
+ log.dim(" Run pnpm install to update dependencies");
708
+ });
709
+ var addPackageCmd = new Command2("package").description("Add a package to the project").argument("<name>", 'Package name (run "loor list" to see available)').addHelpText("after", `
710
+ Examples:
711
+ $ loor add package mediaKit
712
+ $ loor add package mailer
713
+ `).action(async (name) => {
714
+ await ensureCache();
715
+ const projectDir = process.cwd();
716
+ if (!await detectProject(projectDir)) {
717
+ log.error("Not a loor project. Run this command from the project root.");
718
+ process.exit(1);
719
+ }
720
+ const registry = new PackageRegistry(await getPackagesDir());
721
+ await registry.load();
722
+ const manifest = registry.get(name);
723
+ if (!manifest) {
724
+ log.error(`Package not found: ${name}`);
725
+ log.dim('Run "loor list" to see available packages.');
726
+ process.exit(1);
727
+ }
728
+ const resolved = resolveWithTransitiveDeps([name], registry);
729
+ const toInstall = [];
730
+ for (const pkgName of resolved) {
731
+ const pkgDir = path5.join(projectDir, "packages", pkgName);
732
+ if (!await fs5.pathExists(pkgDir)) {
733
+ toInstall.push(pkgName);
734
+ }
735
+ }
736
+ if (toInstall.length === 0) {
737
+ log.info(`Package "${name}" is already installed.`);
738
+ return;
739
+ }
740
+ p3.intro(`Adding package: ${name}`);
741
+ if (toInstall.length > 1) {
742
+ const deps = toInstall.filter((n) => n !== name);
743
+ if (deps.length > 0) {
744
+ log.info(`Also adding dependencies: ${deps.join(", ")}`);
745
+ }
746
+ }
747
+ const spinner3 = p3.spinner();
748
+ for (const pkgName of toInstall) {
749
+ const pkgManifest = registry.get(pkgName);
750
+ spinner3.start(`Adding ${pkgName}`);
751
+ await addPackage(projectDir, pkgManifest);
752
+ spinner3.stop(`Package ready: ${pkgName}`);
753
+ }
754
+ p3.outro("Package added successfully!");
755
+ log.dim(" Run pnpm install to update dependencies");
756
+ });
757
+ var addCommand = new Command2("add").description("Add an app or package to the project").addHelpText("after", `
758
+ Examples:
759
+ $ loor add app api backend
760
+ $ loor add app web-client dashboard
761
+ $ loor add package mediaKit
762
+ `).addCommand(addApp).addCommand(addPackageCmd);
763
+
764
+ // src/commands/list.ts
765
+ import { Command as Command3 } from "commander";
766
+ import chalk from "chalk";
767
+ var listCommand = new Command3("list").description("List all available packages").action(async () => {
768
+ await ensureCache();
769
+ const registry = new PackageRegistry(await getPackagesDir());
770
+ await registry.load();
771
+ const categories = ["core", "server", "client", "config"];
772
+ console.log("");
773
+ console.log(chalk.bold("Available packages:"));
774
+ console.log("");
775
+ for (const category of categories) {
776
+ const packages = registry.getByCategory(category);
777
+ if (packages.length === 0) continue;
778
+ console.log(chalk.cyan.bold(` ${category.toUpperCase()}`));
779
+ for (const pkg of packages) {
780
+ const compat = pkg.compatibility.join(", ");
781
+ const deps = pkg.requires.length > 0 ? chalk.dim(` (requires: ${pkg.requires.join(", ")})`) : "";
782
+ console.log(` ${chalk.white(pkg.name.padEnd(20))} ${chalk.dim(pkg.description)}${deps}`);
783
+ console.log(` ${"".padEnd(20)} ${chalk.dim(`compat: ${compat}`)}`);
784
+ }
785
+ console.log("");
786
+ }
787
+ });
788
+
789
+ // src/commands/update.ts
790
+ import { Command as Command4 } from "commander";
791
+ var updateCommand = new Command4("update").description("Update package registry from remote").action(async () => {
792
+ await ensureCache({ force: true });
793
+ log.success("Registry is up to date.");
794
+ });
795
+
796
+ // src/commands/ai.ts
797
+ import { Command as Command5 } from "commander";
798
+
799
+ // src/generators/claudeMdGenerator.ts
800
+ import path6 from "path";
801
+ import fs6 from "fs-extra";
802
+ async function generateClaudeMd(projectDir, registry) {
803
+ const sections = [];
804
+ const projectName = await getProjectName(projectDir);
805
+ sections.push(`# ${projectName}`);
806
+ sections.push("");
807
+ sections.push("> Auto-generated by `loor ai init`. MCP server is auto-configured in `.mcp.json`.");
808
+ sections.push("");
809
+ const apps = await detectApps(projectDir);
810
+ if (apps.length > 0) {
811
+ sections.push("## Apps");
812
+ sections.push("");
813
+ for (const app of apps) {
814
+ sections.push(`- **${app.name}** (${app.type}) \u2014 \`${app.path}\``);
815
+ }
816
+ sections.push("");
817
+ }
818
+ const archSummary = await getArchitectureSummary(registry.getPackagesDir());
819
+ if (archSummary) {
820
+ sections.push("## Architecture");
821
+ sections.push("");
822
+ sections.push(archSummary);
823
+ sections.push("");
824
+ }
825
+ const installedPackages = await getInstalledPackages(projectDir, registry, apps);
826
+ if (installedPackages.length > 0) {
827
+ sections.push("## Installed Packages");
828
+ sections.push("");
829
+ for (const pkg of installedPackages) {
830
+ sections.push(`### ${pkg.name}`);
831
+ sections.push(`${pkg.description} | **${pkg.category}** | compat: ${pkg.compatibility.join(", ")}`);
832
+ if (pkg.ai?.patterns?.length) {
833
+ sections.push("");
834
+ sections.push("**DO:**");
835
+ for (const p4 of pkg.ai.patterns) {
836
+ sections.push(`- ${p4}`);
837
+ }
838
+ }
839
+ if (pkg.ai?.donts?.length) {
840
+ sections.push("");
841
+ sections.push("**DON'T:**");
842
+ for (const d of pkg.ai.donts) {
843
+ sections.push(`- ${d}`);
844
+ }
845
+ }
846
+ if (pkg.apiInfra?.length) {
847
+ sections.push("");
848
+ sections.push("**Infra files:**");
849
+ for (const infra of pkg.apiInfra) {
850
+ sections.push(`- \`${infra.path}\` \u2014 ${infra.description}`);
851
+ }
852
+ }
853
+ if (pkg.envVars?.length) {
854
+ const required = pkg.envVars.filter((e) => e.required);
855
+ if (required.length > 0) {
856
+ sections.push("");
857
+ sections.push("**Required env vars:** " + required.map((e) => `\`${e.key}\``).join(", "));
858
+ }
859
+ }
860
+ sections.push("");
861
+ }
862
+ }
863
+ sections.push("## Conventions");
864
+ sections.push("");
865
+ sections.push("- **Hexagonal Architecture:** Packages define ports (interfaces), apps implement adapters in `src/infra/`");
866
+ sections.push("- **Package exports:** `index.ts` = pure/universal, `server/index.ts` = Node.js only, `client/index.ts` = browser only");
867
+ sections.push("- **Routes:** Extend `RouteProvider`, place in `src/routes/<domain>/<action>/index.ts` for auto-discovery");
868
+ sections.push("- **Features:** Self-contained in `src/features/<name>/` with components, hooks, types, locales");
869
+ sections.push("- **No Buffer in packages:** Use `Uint8Array` for universal compatibility");
870
+ sections.push("");
871
+ for (const app of apps) {
872
+ const appFullPath = path6.join(projectDir, app.path);
873
+ const appContext = await getAppContext(app, appFullPath);
874
+ if (appContext) {
875
+ sections.push(appContext);
876
+ }
877
+ }
878
+ const content = sections.join("\n");
879
+ await fs6.writeFile(path6.join(projectDir, "CLAUDE.md"), content, "utf-8");
880
+ }
881
+ async function getProjectName(projectDir) {
882
+ const pkgJsonPath = path6.join(projectDir, "package.json");
883
+ if (await fs6.pathExists(pkgJsonPath)) {
884
+ const pkg = await fs6.readJson(pkgJsonPath);
885
+ if (pkg.name) return pkg.name;
886
+ }
887
+ return path6.basename(projectDir);
888
+ }
889
+ async function detectApps(projectDir) {
890
+ const appsDir = path6.join(projectDir, "apps");
891
+ if (!await fs6.pathExists(appsDir)) return [];
892
+ const entries = await fs6.readdir(appsDir, { withFileTypes: true });
893
+ const apps = [];
894
+ for (const entry of entries) {
895
+ if (!entry.isDirectory()) continue;
896
+ const appDir = path6.join(appsDir, entry.name);
897
+ const pkgJsonPath = path6.join(appDir, "package.json");
898
+ if (!await fs6.pathExists(pkgJsonPath)) continue;
899
+ const pkg = await fs6.readJson(pkgJsonPath);
900
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
901
+ const type = inferAppType(deps);
902
+ apps.push({
903
+ name: entry.name,
904
+ type,
905
+ path: `apps/${entry.name}`
906
+ });
907
+ }
908
+ return apps;
909
+ }
910
+ function inferAppType(deps) {
911
+ if (deps.express || deps.mongoose || deps.fastify) return "api";
912
+ if (deps.react || deps.vue || deps.svelte || deps["react-dom"]) return "client";
913
+ return "api";
914
+ }
915
+ async function getArchitectureSummary(packagesDir) {
916
+ const docsDir = path6.join(packagesDir, "..", "docs");
917
+ const standardPath = path6.join(docsDir, "PACKAGE_STANDARD.md");
918
+ if (await fs6.pathExists(standardPath)) {
919
+ const content = await fs6.readFile(standardPath, "utf-8");
920
+ const lines = content.split("\n");
921
+ const summary = lines.slice(0, 15).join("\n").trim();
922
+ if (summary) return summary;
923
+ }
924
+ return [
925
+ "Hexagonal Architecture (Ports & Adapters):",
926
+ "- Packages define abstract ports (interfaces) in `ports/` folder",
927
+ "- Apps implement adapters in `infra/` folder",
928
+ "- Factory functions accept ports as dependencies for DI"
929
+ ].join("\n");
930
+ }
931
+ async function getInstalledPackages(projectDir, registry, apps = []) {
932
+ const found = /* @__PURE__ */ new Set();
933
+ const pkgsDir = path6.join(projectDir, "packages");
934
+ if (await fs6.pathExists(pkgsDir)) {
935
+ const entries = await fs6.readdir(pkgsDir, { withFileTypes: true });
936
+ for (const entry of entries) {
937
+ if (!entry.isDirectory()) continue;
938
+ if (entry.name === "cli") continue;
939
+ if (registry.get(entry.name)) found.add(entry.name);
940
+ }
941
+ }
942
+ for (const app of apps) {
943
+ const pkgJsonPath = path6.join(projectDir, app.path, "package.json");
944
+ if (!await fs6.pathExists(pkgJsonPath)) continue;
945
+ const pkg = await fs6.readJson(pkgJsonPath);
946
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
947
+ for (const dep of Object.keys(allDeps)) {
948
+ if (!dep.startsWith("@workspace/")) continue;
949
+ const pkgName = dep.replace("@workspace/", "");
950
+ if (registry.get(pkgName)) found.add(pkgName);
951
+ }
952
+ }
953
+ return Array.from(found).sort().map((name) => registry.get(name)).filter(Boolean);
954
+ }
955
+ async function getAppContext(app, appFullPath) {
956
+ const lines = [];
957
+ if (app.type === "api") {
958
+ const routesDir = path6.join(appFullPath, "src", "routes");
959
+ if (await fs6.pathExists(routesDir)) {
960
+ const routes = await scanDirs(routesDir, 2);
961
+ if (routes.length > 0) {
962
+ lines.push(`## ${app.name} \u2014 Routes`);
963
+ lines.push("");
964
+ for (const route of routes) {
965
+ lines.push(`- \`src/routes/${route}\``);
966
+ }
967
+ lines.push("");
968
+ }
969
+ }
970
+ const infraDir = path6.join(appFullPath, "src", "infra");
971
+ if (await fs6.pathExists(infraDir)) {
972
+ const infraFiles = await fs6.readdir(infraDir);
973
+ const tsFiles = infraFiles.filter((f) => f.endsWith(".ts"));
974
+ if (tsFiles.length > 0) {
975
+ lines.push(`## ${app.name} \u2014 Infrastructure`);
976
+ lines.push("");
977
+ for (const f of tsFiles) {
978
+ lines.push(`- \`src/infra/${f}\``);
979
+ }
980
+ lines.push("");
981
+ }
982
+ }
983
+ } else {
984
+ const featuresDir = path6.join(appFullPath, "src", "features");
985
+ if (await fs6.pathExists(featuresDir)) {
986
+ const features = await fs6.readdir(featuresDir, { withFileTypes: true });
987
+ const featureNames = features.filter((f) => f.isDirectory()).map((f) => f.name);
988
+ if (featureNames.length > 0) {
989
+ lines.push(`## ${app.name} \u2014 Features`);
990
+ lines.push("");
991
+ for (const f of featureNames) {
992
+ lines.push(`- \`src/features/${f}\``);
993
+ }
994
+ lines.push("");
995
+ }
996
+ }
997
+ }
998
+ return lines.length > 0 ? lines.join("\n") : null;
999
+ }
1000
+ async function scanDirs(dir, depth, prefix = "") {
1001
+ if (depth <= 0) return [prefix].filter(Boolean);
1002
+ const entries = await fs6.readdir(dir, { withFileTypes: true });
1003
+ const results = [];
1004
+ for (const entry of entries) {
1005
+ if (!entry.isDirectory()) continue;
1006
+ const childPath = prefix ? `${prefix}/${entry.name}` : entry.name;
1007
+ const children = await scanDirs(path6.join(dir, entry.name), depth - 1, childPath);
1008
+ results.push(...children);
1009
+ }
1010
+ return results;
1011
+ }
1012
+
1013
+ // src/commands/ai.ts
1014
+ var aiCommand = new Command5("ai").description("AI context tools");
1015
+ aiCommand.addCommand(
1016
+ new Command5("init").description("Generate CLAUDE.md with project-specific AI context").action(async () => {
1017
+ await ensureCache();
1018
+ const registry = new PackageRegistry(await getPackagesDir());
1019
+ await registry.load();
1020
+ const projectDir = process.cwd();
1021
+ await generateClaudeMd(projectDir, registry);
1022
+ await generateMcpConfig(projectDir);
1023
+ log.success("CLAUDE.md generated successfully.");
1024
+ log.success(".mcp.json generated successfully.");
1025
+ })
1026
+ );
1027
+
1028
+ // src/commands/config.ts
1029
+ import path7 from "path";
1030
+ import fs7 from "fs-extra";
1031
+ import { Command as Command6 } from "commander";
1032
+ var configCommand = new Command6("config").description("Configure loor CLI source (local or remote)").option("--local [path]", "Use local repo as scaffold source (defaults to cwd)").option("--remote", "Use remote registry (production)").action(async (opts) => {
1033
+ if (!opts.local && !opts.remote) {
1034
+ const config = await readConfig();
1035
+ if (process.env.LOOR_LOCAL) {
1036
+ log.warn(`LOOR_LOCAL env var is set \u2014 overrides config to: ${process.env.LOOR_LOCAL}`);
1037
+ }
1038
+ log.info(`source: ${config.source}`);
1039
+ if (config.source === "local" && config.localPath) {
1040
+ log.info(`path: ${config.localPath}`);
1041
+ }
1042
+ return;
1043
+ }
1044
+ if (opts.remote) {
1045
+ await writeConfig({ source: "remote" });
1046
+ log.success("Switched to remote mode.");
1047
+ return;
1048
+ }
1049
+ if (opts.local !== void 0) {
1050
+ const rawPath = typeof opts.local === "string" ? opts.local : ".";
1051
+ const absPath = path7.resolve(process.cwd(), rawPath);
1052
+ const hasApps = await fs7.pathExists(path7.join(absPath, "apps"));
1053
+ const hasPackages = await fs7.pathExists(path7.join(absPath, "packages"));
1054
+ if (!hasApps || !hasPackages) {
1055
+ log.error(`Invalid loor repo: ${absPath}`);
1056
+ log.dim(" Expected apps/ and packages/ directories.");
1057
+ process.exit(1);
1058
+ }
1059
+ await writeConfig({ source: "local", localPath: absPath });
1060
+ log.success(`Switched to local mode: ${absPath}`);
1061
+ }
1062
+ });
1063
+
1064
+ // src/cli.ts
1065
+ var cli = new Command7().name("loor").description("Scaffold and manage loor monorepo projects").version("0.1.0").option("--debug", "Enable debug mode with detailed error output").addHelpText("after", `
1066
+ Getting started:
1067
+ $ loor init my-project Create a new project
1068
+ $ loor add app api Add an app to existing project
1069
+ $ loor add package mailer Add a package to existing project
1070
+ $ loor list List available packages
1071
+ `).hook("preAction", (thisCommand) => {
1072
+ const opts = thisCommand.optsWithGlobals();
1073
+ if (opts.debug) enableDebug();
1074
+ });
1075
+ cli.addCommand(initCommand);
1076
+ cli.addCommand(addCommand);
1077
+ cli.addCommand(listCommand);
1078
+ cli.addCommand(updateCommand);
1079
+ cli.addCommand(aiCommand);
1080
+ cli.addCommand(configCommand);
1081
+ export {
1082
+ cli
1083
+ };