@locusai/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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/bin/locus.js +1147 -0
  4. package/bin/mcp.js +19819 -0
  5. package/bin/server.js +36449 -0
  6. package/index.ts +220 -0
  7. package/package.json +27 -0
  8. package/public/dashboard/404.html +1 -0
  9. package/public/dashboard/_next/static/FKNiy9gxnBJm8JQK-p25K/_buildManifest.js +1 -0
  10. package/public/dashboard/_next/static/FKNiy9gxnBJm8JQK-p25K/_ssgManifest.js +1 -0
  11. package/public/dashboard/_next/static/WfUedKoDM4nZdcatRoQdk/_buildManifest.js +1 -0
  12. package/public/dashboard/_next/static/WfUedKoDM4nZdcatRoQdk/_ssgManifest.js +1 -0
  13. package/public/dashboard/_next/static/XW3_p_di5drusE-VlxzXK/_buildManifest.js +1 -0
  14. package/public/dashboard/_next/static/XW3_p_di5drusE-VlxzXK/_ssgManifest.js +1 -0
  15. package/public/dashboard/_next/static/bf2GOBXb9EaNnZkv7RVlG/_buildManifest.js +1 -0
  16. package/public/dashboard/_next/static/bf2GOBXb9EaNnZkv7RVlG/_ssgManifest.js +1 -0
  17. package/public/dashboard/_next/static/chunks/140.29d6e9ea1df01666.js +1 -0
  18. package/public/dashboard/_next/static/chunks/142.e0cbd3087fd025fc.js +1 -0
  19. package/public/dashboard/_next/static/chunks/18.1e768729d24d723d.js +1 -0
  20. package/public/dashboard/_next/static/chunks/188.1512869303a9ef11.js +1 -0
  21. package/public/dashboard/_next/static/chunks/273-a1771233d25f2119.js +1 -0
  22. package/public/dashboard/_next/static/chunks/84-575620eb3bbfced4.js +1 -0
  23. package/public/dashboard/_next/static/chunks/855-df0aee06463ea596.js +2 -0
  24. package/public/dashboard/_next/static/chunks/87c73c54-18883d7ce69be0ce.js +1 -0
  25. package/public/dashboard/_next/static/chunks/886-5056f491a3d531b3.js +1 -0
  26. package/public/dashboard/_next/static/chunks/954-73b5906ca7819e42.js +1 -0
  27. package/public/dashboard/_next/static/chunks/972-efb0c31aeb3e1619.js +1 -0
  28. package/public/dashboard/_next/static/chunks/app/_not-found/page-3884acf5e0397003.js +1 -0
  29. package/public/dashboard/_next/static/chunks/app/backlog/page-f30275eedcf12ad8.js +1 -0
  30. package/public/dashboard/_next/static/chunks/app/docs/page-377e5ca3267a2eb5.js +1 -0
  31. package/public/dashboard/_next/static/chunks/app/docs/page-89769946bd53dc5b.js +1 -0
  32. package/public/dashboard/_next/static/chunks/app/docs/page-e90e86777b3c946f.js +1 -0
  33. package/public/dashboard/_next/static/chunks/app/layout-152c7fef4bd8f727.js +1 -0
  34. package/public/dashboard/_next/static/chunks/app/page-e1e7887da301e162.js +1 -0
  35. package/public/dashboard/_next/static/chunks/app/settings/page-67b58b5201cc6766.js +1 -0
  36. package/public/dashboard/_next/static/chunks/framework-fdd4ff226e9057cd.js +1 -0
  37. package/public/dashboard/_next/static/chunks/main-8ea28c2ff0c09b83.js +1 -0
  38. package/public/dashboard/_next/static/chunks/main-app-18102516cfd3e949.js +1 -0
  39. package/public/dashboard/_next/static/chunks/pages/_app-3e3e3e64529ea027.js +1 -0
  40. package/public/dashboard/_next/static/chunks/pages/_error-8cfbe37f68950a2b.js +1 -0
  41. package/public/dashboard/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  42. package/public/dashboard/_next/static/chunks/webpack-3acb64ba00ce78c9.js +1 -0
  43. package/public/dashboard/_next/static/chunks/webpack-a4df5dd62e0babef.js +1 -0
  44. package/public/dashboard/_next/static/chunks/webpack-ce3693c6fe6b715d.js +1 -0
  45. package/public/dashboard/_next/static/css/8aea088cdc4338f0.css +1 -0
  46. package/public/dashboard/_next/static/css/a979e4e0673642a5.css +1 -0
  47. package/public/dashboard/_next/static/css/c2c06e0e7e056d3e.css +1 -0
  48. package/public/dashboard/_next/static/media/24c15609eaa28576-s.woff2 +0 -0
  49. package/public/dashboard/_next/static/media/2c07349e02a7b712-s.woff2 +0 -0
  50. package/public/dashboard/_next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
  51. package/public/dashboard/_next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
  52. package/public/dashboard/_next/static/media/4f77bef990aad698-s.woff2 +0 -0
  53. package/public/dashboard/_next/static/media/627d916fd739a539-s.woff2 +0 -0
  54. package/public/dashboard/_next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
  55. package/public/dashboard/_next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
  56. package/public/dashboard/_next/static/media/84602850c8fd81c3-s.woff2 +0 -0
  57. package/public/dashboard/_next/static/omFaTNN93MZypoe_iVckS/_buildManifest.js +1 -0
  58. package/public/dashboard/_next/static/omFaTNN93MZypoe_iVckS/_ssgManifest.js +1 -0
  59. package/public/dashboard/backlog.html +1 -0
  60. package/public/dashboard/backlog.txt +21 -0
  61. package/public/dashboard/docs.html +1 -0
  62. package/public/dashboard/docs.txt +22 -0
  63. package/public/dashboard/index.html +1 -0
  64. package/public/dashboard/index.txt +21 -0
  65. package/public/dashboard/settings.html +1 -0
  66. package/public/dashboard/settings.txt +21 -0
  67. package/src/constants.ts +28 -0
  68. package/src/generators/locus.ts +131 -0
  69. package/src/generators/root.ts +244 -0
  70. package/src/generators/server.ts +135 -0
  71. package/src/generators/shared.ts +35 -0
  72. package/src/generators/web.ts +513 -0
  73. package/src/types.ts +6 -0
  74. package/src/utils.ts +13 -0
package/bin/locus.js ADDED
@@ -0,0 +1,1147 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // packages/cli/index.ts
5
+ import { existsSync as existsSync2 } from "fs";
6
+ import { homedir } from "os";
7
+ import { isAbsolute, join as join6, resolve as resolve2 } from "path";
8
+ import { parseArgs } from "util";
9
+
10
+ // packages/cli/src/generators/locus.ts
11
+ import { Database } from "bun:sqlite";
12
+ import { existsSync } from "fs";
13
+ import { writeFile as writeFile2 } from "fs/promises";
14
+ import { join, resolve } from "path";
15
+
16
+ // packages/cli/src/utils.ts
17
+ import { mkdir, writeFile } from "fs/promises";
18
+ async function writeJson(path, content) {
19
+ const jsonContent = `${JSON.stringify(content, null, 2)}
20
+ `;
21
+ await writeFile(path, jsonContent);
22
+ }
23
+ async function ensureDir(path) {
24
+ await mkdir(path, { recursive: true });
25
+ }
26
+
27
+ // packages/cli/src/generators/locus.ts
28
+ async function initializeLocus(config) {
29
+ console.log("Initializing Locus workspace...");
30
+ const { projectPath, locusDir, projectName } = config;
31
+ await ensureDir(locusDir);
32
+ const workspaceConfig = {
33
+ repoPath: projectPath,
34
+ docsPath: join(locusDir, "docs"),
35
+ ciPresetsPath: join(locusDir, "ci-presets.json"),
36
+ projectName
37
+ };
38
+ await writeJson(join(locusDir, "workspace.config.json"), workspaceConfig);
39
+ const ciPresets = {
40
+ quick: ["bun run lint", "bun run typecheck"],
41
+ full: ["bun run lint", "bun run typecheck", "bun run build"]
42
+ };
43
+ await writeJson(join(locusDir, "ci-presets.json"), ciPresets);
44
+ const dbPath = join(locusDir, "db.sqlite");
45
+ const db = new Database(dbPath);
46
+ db.run(`
47
+ CREATE TABLE IF NOT EXISTS tasks (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ title TEXT NOT NULL,
50
+ description TEXT,
51
+ status TEXT NOT NULL,
52
+ priority TEXT NOT NULL DEFAULT 'MEDIUM',
53
+ labels TEXT,
54
+ assigneeRole TEXT,
55
+ parentId INTEGER,
56
+ lockedBy TEXT,
57
+ lockExpiresAt INTEGER,
58
+ acceptanceChecklist TEXT,
59
+ createdAt INTEGER NOT NULL,
60
+ updatedAt INTEGER NOT NULL,
61
+ FOREIGN KEY(parentId) REFERENCES tasks(id)
62
+ );`);
63
+ db.run(`
64
+ CREATE TABLE IF NOT EXISTS comments (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ taskId INTEGER NOT NULL,
67
+ author TEXT NOT NULL,
68
+ text TEXT NOT NULL,
69
+ createdAt INTEGER NOT NULL,
70
+ FOREIGN KEY(taskId) REFERENCES tasks(id)
71
+ );`);
72
+ db.run(`
73
+ CREATE TABLE IF NOT EXISTS artifacts (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
75
+ taskId INTEGER NOT NULL,
76
+ type TEXT NOT NULL,
77
+ title TEXT NOT NULL,
78
+ contentText TEXT,
79
+ filePath TEXT,
80
+ createdBy TEXT NOT NULL,
81
+ createdAt INTEGER NOT NULL,
82
+ FOREIGN KEY(taskId) REFERENCES tasks(id)
83
+ );`);
84
+ db.run(`
85
+ CREATE TABLE IF NOT EXISTS events (
86
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
87
+ taskId INTEGER NOT NULL,
88
+ type TEXT NOT NULL,
89
+ payload TEXT,
90
+ createdAt INTEGER NOT NULL,
91
+ FOREIGN KEY(taskId) REFERENCES tasks(id)
92
+ );`);
93
+ if (!existsSync(join(projectPath, "README.md"))) {
94
+ await writeFile2(join(projectPath, "README.md"), `# ${projectName}
95
+
96
+ Managed by Locus.
97
+ `);
98
+ }
99
+ }
100
+ async function logMcpConfig(config) {
101
+ const { projectPath, projectName } = config;
102
+ const scriptDir = import.meta.dir;
103
+ const isBundled = scriptDir.endsWith("/bin") || scriptDir.endsWith("\\bin");
104
+ const locusRoot = isBundled ? resolve(scriptDir, "../") : resolve(scriptDir, "../../../../");
105
+ const mcpSourcePath = join(locusRoot, "apps/mcp/src/index.ts");
106
+ const mcpBundledPath = isBundled ? join(scriptDir, "mcp.js") : join(locusRoot, "packages/cli/bin/mcp.js");
107
+ const mcpExecPath = existsSync(mcpSourcePath) ? mcpSourcePath : mcpBundledPath;
108
+ const mcpConfig = {
109
+ mcpServers: {
110
+ [projectName]: {
111
+ command: "bun",
112
+ args: ["run", mcpExecPath, "--project", join(projectPath, ".locus")],
113
+ env: {}
114
+ }
115
+ }
116
+ };
117
+ console.log(`
118
+ Project created successfully!`);
119
+ console.log(`
120
+ Next steps:`);
121
+ console.log(` cd ${projectName}`);
122
+ console.log(" bun install");
123
+ console.log(" bun run dev");
124
+ console.log(`
125
+ MCP Configuration (add to your IDE or Claude Desktop config):`);
126
+ console.log(JSON.stringify(mcpConfig, null, 2));
127
+ console.log(`
128
+ `);
129
+ }
130
+
131
+ // packages/cli/src/generators/root.ts
132
+ import { chmod, writeFile as writeFile3 } from "fs/promises";
133
+ import { join as join2 } from "path";
134
+
135
+ // packages/cli/src/constants.ts
136
+ var VERSIONS = {
137
+ node: "24.0.0",
138
+ bun: "1.3.6",
139
+ biome: "2.3.11",
140
+ typescript: "5.8.3",
141
+ react: "^19.0.0",
142
+ reactDom: "^19.0.0",
143
+ next: "15.1.11",
144
+ nestjs: "^11.0.0",
145
+ zod: "^3.23.8",
146
+ lucide: "^0.453.0",
147
+ tailwindcss: "^4.1.0",
148
+ tailwindPostcss: "^4.1.0",
149
+ postcss: "^8.5.0",
150
+ typesBun: "^1.3.0",
151
+ typesNode: "^22.10.0",
152
+ typesReact: "^19.0.0",
153
+ typesReactDom: "^19.0.0",
154
+ syncpack: "^13.0.0",
155
+ husky: "^9.1.0",
156
+ commitlint: "^19.6.0",
157
+ commitlintConfig: "^19.5.0",
158
+ radixUi: "^1.4.3",
159
+ classVarianceAuthority: "^0.7.0",
160
+ clsx: "^2.1.1",
161
+ tailwindMerge: "^3.4.0",
162
+ framerMotion: "^12.26.2"
163
+ };
164
+
165
+ // packages/cli/src/generators/root.ts
166
+ async function setupStructure(config) {
167
+ console.log("Setting up monorepo structure...");
168
+ const { projectPath, locusDir } = config;
169
+ await ensureDir(projectPath);
170
+ await ensureDir(join2(projectPath, "apps/web/src"));
171
+ await ensureDir(join2(projectPath, "apps/server/src"));
172
+ await ensureDir(join2(projectPath, "packages/shared/src"));
173
+ await ensureDir(join2(locusDir, "artifacts"));
174
+ await ensureDir(join2(locusDir, "logs"));
175
+ await ensureDir(join2(locusDir, "docs"));
176
+ await ensureDir(join2(projectPath, ".husky"));
177
+ await ensureDir(join2(projectPath, ".vscode"));
178
+ }
179
+ async function generateRootConfigs(config) {
180
+ console.log("Generating root configurations...");
181
+ const { projectPath, projectName, scopedName } = config;
182
+ await writeJson(join2(projectPath, "package.json"), {
183
+ name: projectName,
184
+ version: "0.1.0",
185
+ private: true,
186
+ type: "module",
187
+ workspaces: ["apps/*", "packages/*"],
188
+ engines: {
189
+ node: `>=${VERSIONS.node}`,
190
+ bun: `>=${VERSIONS.bun}`
191
+ },
192
+ scripts: {
193
+ dev: 'bun run --filter "*" dev',
194
+ build: 'bun run --filter "*" build',
195
+ lint: "biome lint .",
196
+ format: "biome check --write .",
197
+ typecheck: "tsc -b --noEmit",
198
+ syncpack: "syncpack list",
199
+ "syncpack:fix": "syncpack fix",
200
+ prepare: "husky"
201
+ },
202
+ devDependencies: {
203
+ "@biomejs/biome": VERSIONS.biome,
204
+ typescript: VERSIONS.typescript,
205
+ "@types/node": VERSIONS.typesNode,
206
+ syncpack: VERSIONS.syncpack,
207
+ husky: VERSIONS.husky,
208
+ "@commitlint/cli": VERSIONS.commitlint,
209
+ "@commitlint/config-conventional": VERSIONS.commitlintConfig,
210
+ "@types/bun": VERSIONS.typesBun
211
+ }
212
+ });
213
+ await writeJson(join2(projectPath, "tsconfig.base.json"), {
214
+ compilerOptions: {
215
+ target: "ESNext",
216
+ module: "ESNext",
217
+ moduleResolution: "bundler",
218
+ strict: true,
219
+ skipLibCheck: true,
220
+ esModuleInterop: true,
221
+ isolatedModules: true,
222
+ resolveJsonModule: true,
223
+ declaration: true,
224
+ declarationMap: true,
225
+ composite: true,
226
+ incremental: true,
227
+ lib: ["ESNext", "DOM", "DOM.Iterable"],
228
+ types: ["bun-types"]
229
+ }
230
+ });
231
+ await writeJson(join2(projectPath, "tsconfig.json"), {
232
+ files: [],
233
+ references: [
234
+ { path: "./packages/shared" },
235
+ { path: "./apps/web" },
236
+ { path: "./apps/server" }
237
+ ]
238
+ });
239
+ await writeJson(join2(projectPath, "biome.json"), {
240
+ $schema: `https://biomejs.dev/schemas/${VERSIONS.biome}/schema.json`,
241
+ vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
242
+ files: {
243
+ ignoreUnknown: false,
244
+ includes: [
245
+ "**",
246
+ "!**/node_modules",
247
+ "!**/dist",
248
+ "!**/build",
249
+ "!**/coverage"
250
+ ]
251
+ },
252
+ formatter: {
253
+ enabled: true,
254
+ formatWithErrors: false,
255
+ indentStyle: "space",
256
+ indentWidth: 2,
257
+ lineEnding: "lf",
258
+ lineWidth: 80,
259
+ attributePosition: "auto"
260
+ },
261
+ assist: { actions: { source: { organizeImports: "on" } } },
262
+ linter: {
263
+ enabled: true,
264
+ rules: {
265
+ recommended: true,
266
+ complexity: {
267
+ noExtraBooleanCast: "error",
268
+ noUselessCatch: "error",
269
+ noUselessTypeConstraint: "error"
270
+ },
271
+ correctness: {
272
+ noConstAssign: "error",
273
+ noEmptyPattern: "error",
274
+ noUnusedImports: "error",
275
+ noUnusedVariables: "error",
276
+ useValidTypeof: "error"
277
+ },
278
+ style: {
279
+ noNamespace: "error",
280
+ useAsConstAssertion: "error",
281
+ noParameterAssign: "error",
282
+ noNonNullAssertion: "error",
283
+ useImportType: "off"
284
+ },
285
+ suspicious: {
286
+ noAsyncPromiseExecutor: "error",
287
+ noCatchAssign: "error",
288
+ noDebugger: "error",
289
+ noDuplicateObjectKeys: "error",
290
+ noExplicitAny: "error"
291
+ }
292
+ }
293
+ },
294
+ javascript: {
295
+ globals: ["React", "JSX", "Bun"],
296
+ formatter: {
297
+ quoteStyle: "double",
298
+ jsxQuoteStyle: "double",
299
+ trailingCommas: "es5",
300
+ semicolons: "always",
301
+ arrowParentheses: "always",
302
+ bracketSpacing: true
303
+ }
304
+ },
305
+ css: {
306
+ parser: {
307
+ tailwindDirectives: true
308
+ }
309
+ }
310
+ });
311
+ await writeJson(join2(projectPath, ".syncpackrc"), {
312
+ dependencyTypes: ["dev", "prod"],
313
+ semverGroups: [{ range: "", dependencies: ["**"] }],
314
+ versionGroups: [
315
+ {
316
+ label: "Internal packages use workspace protocols",
317
+ dependencies: [`${scopedName}/*`],
318
+ dependencyTypes: ["prod", "dev"],
319
+ pinVersion: "workspace:*"
320
+ }
321
+ ]
322
+ });
323
+ await writeFile3(join2(projectPath, "commitlint.config.js"), `export default { extends: ['@commitlint/config-conventional'] };
324
+ `);
325
+ const preCommit = `#!/usr/bin/env bash
326
+ . "$(dirname -- "$0")/_/husky.sh"
327
+
328
+ bun run lint
329
+ `;
330
+ await writeFile3(join2(projectPath, ".husky/pre-commit"), preCommit);
331
+ await chmod(join2(projectPath, ".husky/pre-commit"), 493);
332
+ const gitignore = `node_modules
333
+ .next
334
+ dist
335
+ .locus/db.sqlite
336
+ .locus/logs
337
+ .locus/artifacts
338
+ .DS_Store
339
+ *.log
340
+ .env
341
+ .env.local
342
+ .turbo
343
+ `;
344
+ await writeFile3(join2(projectPath, ".gitignore"), gitignore);
345
+ await writeFile3(join2(projectPath, ".nvmrc"), `${VERSIONS.node}
346
+ `);
347
+ await writeJson(join2(projectPath, ".vscode/settings.json"), {
348
+ "editor.defaultFormatter": "biomejs.biome",
349
+ "editor.formatOnSave": true,
350
+ "editor.codeActionsOnSave": {
351
+ "source.organizeImports.biome": "explicit"
352
+ },
353
+ "[javascript]": {
354
+ "editor.defaultFormatter": "biomejs.biome"
355
+ },
356
+ "[javascriptreact]": {
357
+ "editor.defaultFormatter": "biomejs.biome"
358
+ },
359
+ "[typescript]": {
360
+ "editor.defaultFormatter": "biomejs.biome"
361
+ },
362
+ "[typescriptreact]": {
363
+ "editor.defaultFormatter": "biomejs.biome"
364
+ },
365
+ "[json]": {
366
+ "editor.defaultFormatter": "biomejs.biome"
367
+ },
368
+ "[jsonc]": {
369
+ "editor.defaultFormatter": "biomejs.biome"
370
+ },
371
+ "files.associations": {
372
+ "*.css": "tailwindcss",
373
+ "*.scss": "tailwindcss"
374
+ }
375
+ });
376
+ await writeJson(join2(projectPath, ".vscode/extensions.json"), {
377
+ recommendations: ["biomejs.biome"]
378
+ });
379
+ }
380
+
381
+ // packages/cli/src/generators/server.ts
382
+ import { writeFile as writeFile4 } from "fs/promises";
383
+ import { join as join3 } from "path";
384
+ async function generateAppServer(config) {
385
+ const { projectPath, scopedName } = config;
386
+ const appDir = join3(projectPath, "apps/server");
387
+ const srcDir = join3(appDir, "src");
388
+ await ensureDir(srcDir);
389
+ await writeJson(join3(appDir, "package.json"), {
390
+ name: `${scopedName}/server`,
391
+ version: "0.1.0",
392
+ private: true,
393
+ type: "module",
394
+ scripts: {
395
+ dev: "nest start --watch",
396
+ build: "nest build",
397
+ start: "nest start"
398
+ },
399
+ dependencies: {
400
+ "@nestjs/common": VERSIONS.nestjs,
401
+ "@nestjs/core": VERSIONS.nestjs,
402
+ "@nestjs/platform-express": VERSIONS.nestjs,
403
+ "reflect-metadata": "^0.2.0",
404
+ rxjs: "^7.8.0",
405
+ [`${scopedName}/shared`]: "workspace:*"
406
+ },
407
+ devDependencies: {
408
+ "@nestjs/cli": VERSIONS.nestjs,
409
+ "@nestjs/schematics": VERSIONS.nestjs,
410
+ "@types/node": VERSIONS.typesNode,
411
+ typescript: VERSIONS.typescript
412
+ }
413
+ });
414
+ await writeJson(join3(appDir, "tsconfig.json"), {
415
+ extends: "../../tsconfig.base.json",
416
+ compilerOptions: {
417
+ removeComments: true,
418
+ emitDecoratorMetadata: true,
419
+ experimentalDecorators: true,
420
+ allowSyntheticDefaultImports: true,
421
+ target: "ESNext",
422
+ sourceMap: true,
423
+ outDir: "./dist",
424
+ baseUrl: "./",
425
+ incremental: true,
426
+ skipLibCheck: true,
427
+ strictNullChecks: false,
428
+ noImplicitAny: false,
429
+ strictBindCallApply: false,
430
+ forceConsistentCasingInFileNames: false,
431
+ noFallthroughCasesInSwitch: false
432
+ },
433
+ include: ["src"]
434
+ });
435
+ await writeFile4(join3(appDir, ".env.example"), `PORT=8000
436
+ `);
437
+ await writeFile4(join3(appDir, ".env"), `PORT=8000
438
+ `);
439
+ await writeJson(join3(appDir, "nest-cli.json"), {
440
+ $schema: "https://json.schemastore.org/nest-cli",
441
+ collection: "@nestjs/schematics",
442
+ sourceRoot: "src",
443
+ compilerOptions: {
444
+ deleteOutDir: true
445
+ }
446
+ });
447
+ await writeFile4(join3(srcDir, "main.ts"), `import { NestFactory } from '@nestjs/core';
448
+ import { AppModule } from './app.module.js';
449
+
450
+ async function bootstrap() {
451
+ const app = await NestFactory.create(AppModule);
452
+ app.enableCors();
453
+ const port = process.env.PORT || 8000;
454
+ await app.listen(port);
455
+ console.log(\`Application is running on: http://localhost:\${port}\`);
456
+ }
457
+ bootstrap();
458
+ `);
459
+ await writeFile4(join3(srcDir, "app.module.ts"), `import { Module } from '@nestjs/common';
460
+ import { AppController } from './app.controller.js';
461
+ import { AppService } from './app.service.js';
462
+
463
+ @Module({
464
+ imports: [],
465
+ controllers: [AppController],
466
+ providers: [AppService],
467
+ })
468
+ export class AppModule {}
469
+ `);
470
+ await writeFile4(join3(srcDir, "app.controller.ts"), `import { Controller, Get } from '@nestjs/common';
471
+ import { AppService } from './app.service.js';
472
+
473
+ @Controller()
474
+ export class AppController {
475
+ constructor(private readonly appService: AppService) {}
476
+
477
+ @Get()
478
+ getHello(): string {
479
+ return this.appService.getHello();
480
+ }
481
+ }
482
+ `);
483
+ await writeFile4(join3(srcDir, "app.service.ts"), `import { Injectable } from '@nestjs/common';
484
+
485
+ @Injectable()
486
+ export class AppService {
487
+ getHello(): string {
488
+ return 'Hello World!';
489
+ }
490
+ }
491
+ `);
492
+ }
493
+
494
+ // packages/cli/src/generators/shared.ts
495
+ import { writeFile as writeFile5 } from "fs/promises";
496
+ import { join as join4 } from "path";
497
+ async function generatePackageShared(config) {
498
+ const { projectPath, scopedName } = config;
499
+ const pkgDir = join4(projectPath, "packages/shared");
500
+ await writeJson(join4(pkgDir, "package.json"), {
501
+ name: `${scopedName}/shared`,
502
+ version: "0.1.0",
503
+ private: true,
504
+ type: "module",
505
+ main: "./src/index.ts",
506
+ types: "./src/index.ts",
507
+ scripts: {
508
+ build: "tsc"
509
+ },
510
+ devDependencies: {
511
+ typescript: VERSIONS.typescript
512
+ }
513
+ });
514
+ await writeJson(join4(pkgDir, "tsconfig.json"), {
515
+ extends: "../../tsconfig.base.json",
516
+ include: ["src"]
517
+ });
518
+ await writeFile5(join4(pkgDir, "src/index.ts"), `export const VERSION = '0.1.0';
519
+ `);
520
+ }
521
+
522
+ // packages/cli/src/generators/web.ts
523
+ import { writeFile as writeFile6 } from "fs/promises";
524
+ import { join as join5 } from "path";
525
+ async function generateAppWeb(config) {
526
+ const { projectPath, projectName, scopedName } = config;
527
+ const appDir = join5(projectPath, "apps/web");
528
+ const srcDir = join5(appDir, "src/app");
529
+ await ensureDir(srcDir);
530
+ await ensureDir(join5(appDir, "src/components"));
531
+ await ensureDir(join5(appDir, "src/lib"));
532
+ await writeJson(join5(appDir, "package.json"), {
533
+ name: `${scopedName}/web`,
534
+ version: "0.1.0",
535
+ private: true,
536
+ type: "module",
537
+ scripts: {
538
+ dev: "next dev -p 3000",
539
+ build: "next build",
540
+ start: "next start",
541
+ lint: "biome lint ."
542
+ },
543
+ dependencies: {
544
+ next: VERSIONS.next,
545
+ react: VERSIONS.react,
546
+ "react-dom": VERSIONS.reactDom,
547
+ "lucide-react": VERSIONS.lucide,
548
+ "radix-ui": VERSIONS.radixUi,
549
+ "class-variance-authority": VERSIONS.classVarianceAuthority,
550
+ clsx: VERSIONS.clsx,
551
+ "tailwind-merge": VERSIONS.tailwindMerge,
552
+ "framer-motion": VERSIONS.framerMotion,
553
+ [`${scopedName}/shared`]: "workspace:*"
554
+ },
555
+ devDependencies: {
556
+ "@types/node": VERSIONS.typesNode,
557
+ "@types/react": VERSIONS.typesReact,
558
+ "@types/react-dom": VERSIONS.typesReactDom,
559
+ typescript: VERSIONS.typescript,
560
+ tailwindcss: VERSIONS.tailwindcss,
561
+ "@tailwindcss/postcss": VERSIONS.tailwindPostcss,
562
+ postcss: VERSIONS.postcss
563
+ }
564
+ });
565
+ await writeJson(join5(appDir, "tsconfig.json"), {
566
+ extends: "../../tsconfig.base.json",
567
+ compilerOptions: {
568
+ plugins: [{ name: "next" }],
569
+ jsx: "preserve",
570
+ lib: ["dom", "dom.iterable", "esnext"],
571
+ module: "esnext",
572
+ noEmit: true,
573
+ allowJs: true,
574
+ paths: {
575
+ "@/*": ["./src/*"]
576
+ }
577
+ },
578
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
579
+ exclude: ["node_modules"]
580
+ });
581
+ await writeFile6(join5(appDir, "next.config.ts"), `import type { NextConfig } from "next";
582
+
583
+ const nextConfig: NextConfig = {
584
+ reactStrictMode: true,
585
+ };
586
+
587
+ export default nextConfig;
588
+ `);
589
+ await writeFile6(join5(appDir, "postcss.config.mjs"), `export default {
590
+ plugins: {
591
+ "@tailwindcss/postcss": {},
592
+ },
593
+ };
594
+ `);
595
+ await writeFile6(join5(srcDir, "layout.tsx"), `import type { Metadata } from "next";
596
+ import { Roboto } from "next/font/google";
597
+ import "./globals.css";
598
+
599
+ const roboto = Roboto({
600
+ subsets: ["latin"],
601
+ weight: ["300", "400", "500", "700"],
602
+ variable: "--font-roboto",
603
+ });
604
+
605
+ export const metadata: Metadata = {
606
+ title: "${projectName}",
607
+ description: "Managed by Locus - AI-powered engineering workspace",
608
+ };
609
+
610
+ export default function RootLayout({
611
+ children,
612
+ }: {
613
+ children: React.ReactNode;
614
+ }) {
615
+ return (
616
+ <html lang="en" className={roboto.variable}>
617
+ <body className="min-h-screen bg-background text-foreground antialiased font-sans">
618
+ {children}
619
+ </body>
620
+ </html>
621
+ );
622
+ }
623
+ `);
624
+ await writeFile6(join5(srcDir, "page.tsx"), `export default function Home() {
625
+ return (
626
+ <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
627
+ <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
628
+ <div className="flex items-center gap-3">
629
+ <h1 className="text-2xl font-bold tracking-tight">${projectName}</h1>
630
+ </div>
631
+
632
+ <p className="text-muted-foreground text-center sm:text-left max-w-md">
633
+ Welcome to your new project. This workspace is managed by{" "}
634
+ <span className="font-semibold text-foreground">Locus</span> \u2014 an AI-powered
635
+ engineering platform for agentic development.
636
+ </p>
637
+
638
+ <ol className="list-inside list-decimal text-sm text-center sm:text-left space-y-2">
639
+ <li>
640
+ Get started by editing{" "}
641
+ <code className="bg-secondary/80 px-1.5 py-0.5 rounded font-mono text-sm">
642
+ src/app/page.tsx
643
+ </code>
644
+ </li>
645
+ <li>Save and see your changes instantly.</li>
646
+ <li>Create tasks in Locus to let AI agents help you build.</li>
647
+ </ol>
648
+
649
+ <div className="flex gap-4 items-center flex-col sm:flex-row">
650
+ <a
651
+ className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-foreground/90 text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
652
+ href="https://nextjs.org/docs"
653
+ target="_blank"
654
+ rel="noopener noreferrer"
655
+ >
656
+ Next.js Docs
657
+ </a>
658
+ <a
659
+ className="rounded-full border border-solid border-border transition-colors flex items-center justify-center hover:bg-secondary hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
660
+ href="http://localhost:3081"
661
+ target="_blank"
662
+ rel="noopener noreferrer"
663
+ >
664
+ Open Locus Dashboard
665
+ </a>
666
+ </div>
667
+ </main>
668
+
669
+ <footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center text-sm text-muted-foreground">
670
+ <a
671
+ className="flex items-center gap-2 hover:underline hover:underline-offset-4"
672
+ href="https://nextjs.org/learn"
673
+ target="_blank"
674
+ rel="noopener noreferrer"
675
+ >
676
+ Learn
677
+ </a>
678
+ <a
679
+ className="flex items-center gap-2 hover:underline hover:underline-offset-4"
680
+ href="https://vercel.com/templates"
681
+ target="_blank"
682
+ rel="noopener noreferrer"
683
+ >
684
+ Examples
685
+ </a>
686
+ <span className="text-muted-foreground/50">\u2022</span>
687
+ <span>Powered by Locus</span>
688
+ </footer>
689
+ </div>
690
+ );
691
+ }
692
+ `);
693
+ const globalCss = `@import "tailwindcss";
694
+
695
+ @theme {
696
+ --font-roboto: "Roboto", sans-serif;
697
+ --color-background: hsl(var(--background));
698
+ --color-foreground: hsl(var(--foreground));
699
+ --color-card: hsl(var(--card));
700
+ --color-card-foreground: hsl(var(--card-foreground));
701
+ --color-primary: hsl(var(--primary));
702
+ --color-primary-foreground: hsl(var(--primary-foreground));
703
+ --color-secondary: hsl(var(--secondary));
704
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
705
+ --color-muted: hsl(var(--muted));
706
+ --color-muted-foreground: hsl(var(--muted-foreground));
707
+ --color-border: hsl(var(--border));
708
+ --radius-lg: var(--radius);
709
+ --radius-md: calc(var(--radius) - 2px);
710
+ --radius-sm: calc(var(--radius) - 4px);
711
+ }
712
+
713
+ @layer base {
714
+ :root {
715
+ --background: 0 0% 100%;
716
+ --foreground: 222.2 84% 4.9%;
717
+ --card: 0 0% 100%;
718
+ --card-foreground: 222.2 84% 4.9%;
719
+ --primary: 240 100% 50%;
720
+ --primary-foreground: 210 40% 98%;
721
+ --secondary: 210 40% 96.1%;
722
+ --secondary-foreground: 222.2 47.4% 11.2%;
723
+ --muted: 210 40% 96.1%;
724
+ --muted-foreground: 215.4 16.3% 46.9%;
725
+ --border: 214.3 31.8% 91.4%;
726
+ --radius: 0.5rem;
727
+ }
728
+
729
+ @media (prefers-color-scheme: dark) {
730
+ :root {
731
+ --background: 0 0% 0%;
732
+ --foreground: 0 0% 98%;
733
+ --card: 0 0% 3%;
734
+ --card-foreground: 0 0% 98%;
735
+ --primary: 240 100% 50%;
736
+ --primary-foreground: 0 0% 100%;
737
+ --secondary: 0 0% 9%;
738
+ --secondary-foreground: 0 0% 98%;
739
+ --muted: 0 0% 9%;
740
+ --muted-foreground: 0 0% 63%;
741
+ --border: 0 0% 12%;
742
+ }
743
+ }
744
+
745
+ * {
746
+ box-sizing: border-box;
747
+ border-color: var(--color-border);
748
+ }
749
+
750
+ body {
751
+ background-color: var(--color-background);
752
+ color: var(--color-foreground);
753
+ font-family: var(--font-roboto), system-ui, sans-serif;
754
+ -webkit-font-smoothing: antialiased;
755
+ -moz-osx-font-smoothing: grayscale;
756
+ }
757
+ }
758
+ `;
759
+ await writeFile6(join5(srcDir, "globals.css"), globalCss);
760
+ await writeFile6(join5(appDir, "src/lib/utils.ts"), `import { clsx, type ClassValue } from "clsx";
761
+ import { twMerge } from "tailwind-merge";
762
+
763
+ export function cn(...inputs: ClassValue[]) {
764
+ return twMerge(clsx(inputs));
765
+ }
766
+ `);
767
+ await writeFile6(join5(appDir, "src/components/Button.tsx"), `import { cva, type VariantProps } from "class-variance-authority";
768
+ import type { ButtonHTMLAttributes, ReactNode } from "react";
769
+ import { cn } from "@/lib/utils";
770
+
771
+ const buttonVariants = cva(
772
+ "inline-flex items-center justify-center font-medium rounded-lg transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 active:scale-95",
773
+ {
774
+ variants: {
775
+ variant: {
776
+ primary:
777
+ "bg-primary text-primary-foreground hover:bg-primary/90 shadow-lg shadow-primary/20",
778
+ secondary: "bg-gray-500 text-white hover:bg-gray-600",
779
+ outline: "border border-border bg-background hover:bg-secondary",
780
+ ghost: "hover:bg-secondary",
781
+ destructive: "bg-red-500 text-white hover:bg-red-600",
782
+ },
783
+ size: {
784
+ sm: "h-8 px-3 text-xs",
785
+ md: "h-10 px-4 text-sm",
786
+ lg: "h-12 px-6 text-base",
787
+ },
788
+ },
789
+ defaultVariants: {
790
+ variant: "primary",
791
+ size: "md",
792
+ },
793
+ }
794
+ );
795
+
796
+ interface ButtonProps
797
+ extends ButtonHTMLAttributes<HTMLButtonElement>,
798
+ VariantProps<typeof buttonVariants> {
799
+ loading?: boolean;
800
+ leftIcon?: ReactNode;
801
+ rightIcon?: ReactNode;
802
+ }
803
+
804
+ export function Button({
805
+ className,
806
+ variant,
807
+ size,
808
+ loading = false,
809
+ leftIcon,
810
+ rightIcon,
811
+ children,
812
+ ...props
813
+ }: ButtonProps) {
814
+ return (
815
+ <button
816
+ className={cn(buttonVariants({ variant, size, className }))}
817
+ disabled={loading || props.disabled}
818
+ {...props}
819
+ >
820
+ {loading ? (
821
+ <div className="animate-spin rounded-full h-4 w-4 border-2 border-current border-t-transparent" />
822
+ ) : (
823
+ <>
824
+ {leftIcon && <span className="mr-2">{leftIcon}</span>}
825
+ {children}
826
+ {rightIcon && <span className="ml-2">{rightIcon}</span>}
827
+ </>
828
+ )}
829
+ </button>
830
+ );
831
+ }
832
+ `);
833
+ await writeFile6(join5(appDir, "src/components/Dialog.tsx"), `"use client";
834
+ import * as RadixDialog from "@radix-ui/react-dialog";
835
+ import { cva, type VariantProps } from "class-variance-authority";
836
+ import { motion } from "framer-motion";
837
+ import { X } from "lucide-react";
838
+ import React from "react";
839
+ import { cn } from "@/lib/utils";
840
+
841
+ const dialogContentVariants = cva(
842
+ "fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] bg-card rounded-xl border border-border p-6 shadow-2xl w-full z-50",
843
+ {
844
+ variants: {
845
+ size: {
846
+ sm: "max-w-sm",
847
+ md: "max-w-md",
848
+ lg: "max-w-lg",
849
+ xl: "max-w-xl",
850
+ "2xl": "max-w-2xl",
851
+ "3xl": "max-w-3xl",
852
+ "4xl": "max-w-4xl",
853
+ "5xl": "max-w-5xl",
854
+ },
855
+ },
856
+ defaultVariants: {
857
+ size: "md",
858
+ },
859
+ }
860
+ );
861
+
862
+ export function Dialog({ children, ...props }: RadixDialog.DialogProps) {
863
+ return <RadixDialog.Root {...props}>{children}</RadixDialog.Root>;
864
+ }
865
+
866
+ export function DialogTrigger({
867
+ children,
868
+ ...props
869
+ }: RadixDialog.DialogTriggerProps) {
870
+ return <RadixDialog.Trigger {...props}>{children}</RadixDialog.Trigger>;
871
+ }
872
+
873
+ interface DialogContentProps
874
+ extends RadixDialog.DialogContentProps,
875
+ VariantProps<typeof dialogContentVariants> {}
876
+
877
+ export function DialogContent({
878
+ children,
879
+ className,
880
+ size,
881
+ ...props
882
+ }: DialogContentProps) {
883
+ return (
884
+ <RadixDialog.Portal>
885
+ <RadixDialog.Overlay asChild>
886
+ <motion.div
887
+ initial={{ opacity: 0 }}
888
+ animate={{ opacity: 1 }}
889
+ exit={{ opacity: 0 }}
890
+ transition={{ duration: 0.2, ease: "easeOut" }}
891
+ className="fixed inset-0 bg-black/60 backdrop-blur-md z-50"
892
+ />
893
+ </RadixDialog.Overlay>
894
+ <RadixDialog.Content asChild {...props}>
895
+ <motion.div
896
+ initial={{ opacity: 0, scale: 0.9, y: 20 }}
897
+ animate={{ opacity: 1, scale: 1, y: 0 }}
898
+ exit={{ opacity: 0, scale: 0.9, y: 20 }}
899
+ transition={{
900
+ duration: 0.3,
901
+ ease: [0.16, 1, 0.3, 1], // Custom easing for smooth bounce
902
+ opacity: { duration: 0.2 },
903
+ }}
904
+ className={cn(dialogContentVariants({ size }), className)}
905
+ >
906
+ {children}
907
+ <RadixDialog.Close className="absolute right-4 top-4 rounded-full p-1.5 opacity-70 ring-offset-background transition-all hover:opacity-100 hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 hover:scale-105 active:scale-95">
908
+ <X className="h-4 w-4" />
909
+ <span className="sr-only">Close</span>
910
+ </RadixDialog.Close>
911
+ </motion.div>
912
+ </RadixDialog.Content>
913
+ </RadixDialog.Portal>
914
+ );
915
+ }
916
+
917
+ export function DialogHeader({
918
+ children,
919
+ className,
920
+ }: {
921
+ children: React.ReactNode;
922
+ className?: string;
923
+ }) {
924
+ return (
925
+ <div
926
+ className={cn(
927
+ "flex flex-col space-y-1.5 text-center sm:text-left",
928
+ className
929
+ )}
930
+ >
931
+ {children}
932
+ </div>
933
+ );
934
+ }
935
+
936
+ export function DialogTitle({
937
+ children,
938
+ className,
939
+ }: {
940
+ children: React.ReactNode;
941
+ className?: string;
942
+ }) {
943
+ return (
944
+ <RadixDialog.Title
945
+ className={cn(
946
+ "text-lg font-semibold leading-none tracking-tight",
947
+ className
948
+ )}
949
+ >
950
+ {children}
951
+ </RadixDialog.Title>
952
+ );
953
+ }
954
+
955
+ export function DialogDescription({
956
+ children,
957
+ className,
958
+ }: {
959
+ children: React.ReactNode;
960
+ className?: string;
961
+ }) {
962
+ return (
963
+ <RadixDialog.Description
964
+ className={cn("text-sm text-muted-foreground", className)}
965
+ >
966
+ {children}
967
+ </RadixDialog.Description>
968
+ );
969
+ }
970
+
971
+ export function DialogFooter({
972
+ children,
973
+ className,
974
+ }: {
975
+ children: React.ReactNode;
976
+ className?: string;
977
+ }) {
978
+ return (
979
+ <div
980
+ className={cn(
981
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 space-y-2 space-y-reverse sm:space-y-0 mt-6",
982
+ className
983
+ )}
984
+ >
985
+ {children}
986
+ </div>
987
+ );
988
+ }
989
+ `);
990
+ }
991
+
992
+ // packages/cli/index.ts
993
+ async function init(args) {
994
+ const { values, positionals } = parseArgs({
995
+ args,
996
+ options: {
997
+ name: { type: "string" },
998
+ path: { type: "string" }
999
+ },
1000
+ strict: true,
1001
+ allowPositionals: true
1002
+ });
1003
+ const projectNameInput = values.name;
1004
+ let projectPath;
1005
+ let projectName;
1006
+ const isNewProject = !!projectNameInput;
1007
+ if (isNewProject) {
1008
+ projectName = projectNameInput;
1009
+ const userPathInput = values.path || positionals[0];
1010
+ let basePath = process.cwd();
1011
+ if (userPathInput) {
1012
+ const userPath = userPathInput.startsWith("~") ? join6(homedir(), userPathInput.slice(1)) : userPathInput;
1013
+ basePath = isAbsolute(userPath) ? userPath : resolve2(process.cwd(), userPath);
1014
+ }
1015
+ projectPath = join6(basePath, projectName);
1016
+ } else {
1017
+ projectPath = process.cwd();
1018
+ projectName = projectPath.split("/").pop() || "locus-project";
1019
+ console.log(`Initializing Locus in current directory: ${projectName}`);
1020
+ }
1021
+ const scopedName = `@${projectName}`;
1022
+ const locusDir = join6(projectPath, ".locus");
1023
+ const config = {
1024
+ projectName,
1025
+ scopedName,
1026
+ projectPath,
1027
+ locusDir
1028
+ };
1029
+ try {
1030
+ if (isNewProject) {
1031
+ await setupStructure(config);
1032
+ await generateRootConfigs(config);
1033
+ await generatePackageShared(config);
1034
+ await generateAppWeb(config);
1035
+ await generateAppServer(config);
1036
+ }
1037
+ await initializeLocus(config);
1038
+ if (isNewProject) {
1039
+ if (!existsSync2(join6(projectPath, ".git"))) {
1040
+ console.log("Initializing git repository...");
1041
+ await Bun.spawn(["git", "init"], { cwd: projectPath, stdout: "ignore" }).exited;
1042
+ }
1043
+ console.log("Formatting project...");
1044
+ try {
1045
+ await Bun.spawn(["bun", "run", "format"], {
1046
+ cwd: projectPath,
1047
+ stdout: "ignore"
1048
+ }).exited;
1049
+ } catch {
1050
+ console.log("Note: Formatting skipped (biome not found). Run 'bun install' first.");
1051
+ }
1052
+ }
1053
+ await logMcpConfig(config);
1054
+ } catch (error) {
1055
+ console.error("Error creating project:", error);
1056
+ process.exit(1);
1057
+ }
1058
+ }
1059
+ async function dev(args) {
1060
+ const { values } = parseArgs({
1061
+ args,
1062
+ options: {
1063
+ project: { type: "string" }
1064
+ },
1065
+ strict: false
1066
+ });
1067
+ const projectPath = values.project || process.cwd();
1068
+ const locusDir = isAbsolute(projectPath) ? join6(projectPath, ".locus") : resolve2(process.cwd(), projectPath, ".locus");
1069
+ if (!existsSync2(locusDir)) {
1070
+ console.error(`Error: .locus directory not found at ${locusDir}`);
1071
+ console.log("Are you in a Locus project?");
1072
+ process.exit(1);
1073
+ }
1074
+ const cliDir = import.meta.dir;
1075
+ const isBundled = cliDir.endsWith("/bin") || cliDir.endsWith("\\bin");
1076
+ const locusRoot = isBundled ? resolve2(cliDir, "../") : resolve2(cliDir, "../../");
1077
+ const serverSourcePath = join6(locusRoot, "apps/server/src/index.ts");
1078
+ const serverBundledPath = isBundled ? join6(cliDir, "server.js") : join6(locusRoot, "packages/cli/bin/server.js");
1079
+ const serverExecPath = existsSync2(serverSourcePath) ? serverSourcePath : serverBundledPath;
1080
+ if (!existsSync2(serverExecPath)) {
1081
+ console.error("Error: Locus engine not found. Please reinstall the CLI.");
1082
+ process.exit(1);
1083
+ }
1084
+ console.log("\uD83D\uDE80 Starting Locus for project:", projectPath);
1085
+ const serverProcess = Bun.spawn(["bun", "run", serverExecPath, "--project", locusDir], {
1086
+ stdout: "inherit",
1087
+ stderr: "inherit"
1088
+ });
1089
+ let webProcess;
1090
+ const webSourceDir = join6(locusRoot, "apps/web");
1091
+ if (existsSync2(webSourceDir)) {
1092
+ webProcess = Bun.spawn(["bun", "run", "dev"], {
1093
+ cwd: webSourceDir,
1094
+ stdout: "inherit",
1095
+ stderr: "inherit"
1096
+ });
1097
+ } else {
1098
+ console.log("Dashboard UI: http://localhost:3081");
1099
+ }
1100
+ setTimeout(() => {
1101
+ try {
1102
+ if (process.platform === "darwin") {
1103
+ Bun.spawn(["open", "http://localhost:3080"], { stdout: "ignore" });
1104
+ }
1105
+ } catch {
1106
+ }
1107
+ }, 2000);
1108
+ process.on("SIGINT", () => {
1109
+ console.log(`
1110
+ \uD83D\uDED1 Shutting down Locus...`);
1111
+ serverProcess.kill();
1112
+ if (webProcess)
1113
+ webProcess.kill();
1114
+ process.exit();
1115
+ });
1116
+ await Promise.all([
1117
+ serverProcess.exited,
1118
+ webProcess ? webProcess.exited : Promise.resolve()
1119
+ ]);
1120
+ }
1121
+ async function main() {
1122
+ const command = process.argv[2];
1123
+ const args = process.argv.slice(3);
1124
+ switch (command) {
1125
+ case "init":
1126
+ await init(args);
1127
+ break;
1128
+ case "dev":
1129
+ await dev(args);
1130
+ break;
1131
+ case "help":
1132
+ case undefined:
1133
+ console.log(`
1134
+ Locus CLI - Agentic Engineering Workspace
1135
+
1136
+ Usage:
1137
+ locus init [--name <name>] Create a new project or initialize in current dir
1138
+ locus dev Start Locus for the current project
1139
+ locus help Show this help
1140
+ `);
1141
+ break;
1142
+ default:
1143
+ console.error(`Unknown command: ${command}`);
1144
+ process.exit(1);
1145
+ }
1146
+ }
1147
+ main();