@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.
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/bin/locus.js +1147 -0
- package/bin/mcp.js +19819 -0
- package/bin/server.js +36449 -0
- package/index.ts +220 -0
- package/package.json +27 -0
- package/public/dashboard/404.html +1 -0
- package/public/dashboard/_next/static/FKNiy9gxnBJm8JQK-p25K/_buildManifest.js +1 -0
- package/public/dashboard/_next/static/FKNiy9gxnBJm8JQK-p25K/_ssgManifest.js +1 -0
- package/public/dashboard/_next/static/WfUedKoDM4nZdcatRoQdk/_buildManifest.js +1 -0
- package/public/dashboard/_next/static/WfUedKoDM4nZdcatRoQdk/_ssgManifest.js +1 -0
- package/public/dashboard/_next/static/XW3_p_di5drusE-VlxzXK/_buildManifest.js +1 -0
- package/public/dashboard/_next/static/XW3_p_di5drusE-VlxzXK/_ssgManifest.js +1 -0
- package/public/dashboard/_next/static/bf2GOBXb9EaNnZkv7RVlG/_buildManifest.js +1 -0
- package/public/dashboard/_next/static/bf2GOBXb9EaNnZkv7RVlG/_ssgManifest.js +1 -0
- package/public/dashboard/_next/static/chunks/140.29d6e9ea1df01666.js +1 -0
- package/public/dashboard/_next/static/chunks/142.e0cbd3087fd025fc.js +1 -0
- package/public/dashboard/_next/static/chunks/18.1e768729d24d723d.js +1 -0
- package/public/dashboard/_next/static/chunks/188.1512869303a9ef11.js +1 -0
- package/public/dashboard/_next/static/chunks/273-a1771233d25f2119.js +1 -0
- package/public/dashboard/_next/static/chunks/84-575620eb3bbfced4.js +1 -0
- package/public/dashboard/_next/static/chunks/855-df0aee06463ea596.js +2 -0
- package/public/dashboard/_next/static/chunks/87c73c54-18883d7ce69be0ce.js +1 -0
- package/public/dashboard/_next/static/chunks/886-5056f491a3d531b3.js +1 -0
- package/public/dashboard/_next/static/chunks/954-73b5906ca7819e42.js +1 -0
- package/public/dashboard/_next/static/chunks/972-efb0c31aeb3e1619.js +1 -0
- package/public/dashboard/_next/static/chunks/app/_not-found/page-3884acf5e0397003.js +1 -0
- package/public/dashboard/_next/static/chunks/app/backlog/page-f30275eedcf12ad8.js +1 -0
- package/public/dashboard/_next/static/chunks/app/docs/page-377e5ca3267a2eb5.js +1 -0
- package/public/dashboard/_next/static/chunks/app/docs/page-89769946bd53dc5b.js +1 -0
- package/public/dashboard/_next/static/chunks/app/docs/page-e90e86777b3c946f.js +1 -0
- package/public/dashboard/_next/static/chunks/app/layout-152c7fef4bd8f727.js +1 -0
- package/public/dashboard/_next/static/chunks/app/page-e1e7887da301e162.js +1 -0
- package/public/dashboard/_next/static/chunks/app/settings/page-67b58b5201cc6766.js +1 -0
- package/public/dashboard/_next/static/chunks/framework-fdd4ff226e9057cd.js +1 -0
- package/public/dashboard/_next/static/chunks/main-8ea28c2ff0c09b83.js +1 -0
- package/public/dashboard/_next/static/chunks/main-app-18102516cfd3e949.js +1 -0
- package/public/dashboard/_next/static/chunks/pages/_app-3e3e3e64529ea027.js +1 -0
- package/public/dashboard/_next/static/chunks/pages/_error-8cfbe37f68950a2b.js +1 -0
- package/public/dashboard/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/public/dashboard/_next/static/chunks/webpack-3acb64ba00ce78c9.js +1 -0
- package/public/dashboard/_next/static/chunks/webpack-a4df5dd62e0babef.js +1 -0
- package/public/dashboard/_next/static/chunks/webpack-ce3693c6fe6b715d.js +1 -0
- package/public/dashboard/_next/static/css/8aea088cdc4338f0.css +1 -0
- package/public/dashboard/_next/static/css/a979e4e0673642a5.css +1 -0
- package/public/dashboard/_next/static/css/c2c06e0e7e056d3e.css +1 -0
- package/public/dashboard/_next/static/media/24c15609eaa28576-s.woff2 +0 -0
- package/public/dashboard/_next/static/media/2c07349e02a7b712-s.woff2 +0 -0
- package/public/dashboard/_next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
- package/public/dashboard/_next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
- package/public/dashboard/_next/static/media/4f77bef990aad698-s.woff2 +0 -0
- package/public/dashboard/_next/static/media/627d916fd739a539-s.woff2 +0 -0
- package/public/dashboard/_next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
- package/public/dashboard/_next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
- package/public/dashboard/_next/static/media/84602850c8fd81c3-s.woff2 +0 -0
- package/public/dashboard/_next/static/omFaTNN93MZypoe_iVckS/_buildManifest.js +1 -0
- package/public/dashboard/_next/static/omFaTNN93MZypoe_iVckS/_ssgManifest.js +1 -0
- package/public/dashboard/backlog.html +1 -0
- package/public/dashboard/backlog.txt +21 -0
- package/public/dashboard/docs.html +1 -0
- package/public/dashboard/docs.txt +22 -0
- package/public/dashboard/index.html +1 -0
- package/public/dashboard/index.txt +21 -0
- package/public/dashboard/settings.html +1 -0
- package/public/dashboard/settings.txt +21 -0
- package/src/constants.ts +28 -0
- package/src/generators/locus.ts +131 -0
- package/src/generators/root.ts +244 -0
- package/src/generators/server.ts +135 -0
- package/src/generators/shared.ts +35 -0
- package/src/generators/web.ts +513 -0
- package/src/types.ts +6 -0
- 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();
|