@a3t/rapid 0.1.6 → 0.1.8
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/README.md +5 -1
- package/dist/bin.js +1 -1
- package/dist/chunk-VWXN2TG5.js +3618 -0
- package/dist/chunk-VWXN2TG5.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +12 -3
- package/dist/chunk-MNZPKPED.js +0 -1638
- package/dist/chunk-MNZPKPED.js.map +0 -1
|
@@ -0,0 +1,3618 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Command as Command13 } from "commander";
|
|
3
|
+
import { setLogLevel, logger as logger14 } from "@a3t/rapid-core";
|
|
4
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
7
|
+
|
|
8
|
+
// src/commands/init.ts
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import { writeFile, access, readFile, readdir, mkdir } from "fs/promises";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { existsSync } from "fs";
|
|
13
|
+
import {
|
|
14
|
+
getDefaultConfig,
|
|
15
|
+
logger,
|
|
16
|
+
MCP_SERVER_TEMPLATES,
|
|
17
|
+
addMcpServerFromTemplate,
|
|
18
|
+
getSecretReferences,
|
|
19
|
+
writeMcpConfig,
|
|
20
|
+
writeOpenCodeConfig,
|
|
21
|
+
RAPID_METHODOLOGY,
|
|
22
|
+
MCP_USAGE_GUIDELINES,
|
|
23
|
+
GIT_GUIDELINES
|
|
24
|
+
} from "@a3t/rapid-core";
|
|
25
|
+
import ora from "ora";
|
|
26
|
+
async function detectProjectType(dir) {
|
|
27
|
+
const files = await readdir(dir).catch(() => []);
|
|
28
|
+
const fileSet = new Set(files);
|
|
29
|
+
if (fileSet.has("Cargo.toml")) {
|
|
30
|
+
return { language: "rust", confidence: "high" };
|
|
31
|
+
}
|
|
32
|
+
if (fileSet.has("go.mod")) {
|
|
33
|
+
return { language: "go", confidence: "high" };
|
|
34
|
+
}
|
|
35
|
+
if (fileSet.has("pyproject.toml")) {
|
|
36
|
+
const content = await readFile(join(dir, "pyproject.toml"), "utf-8").catch(() => "");
|
|
37
|
+
const framework = content.includes("fastapi") ? "fastapi" : content.includes("django") ? "django" : content.includes("flask") ? "flask" : void 0;
|
|
38
|
+
if (framework) {
|
|
39
|
+
return { language: "python", framework, confidence: "high" };
|
|
40
|
+
}
|
|
41
|
+
return { language: "python", confidence: "high" };
|
|
42
|
+
}
|
|
43
|
+
if (fileSet.has("requirements.txt") || fileSet.has("setup.py") || fileSet.has("Pipfile")) {
|
|
44
|
+
return { language: "python", confidence: "medium" };
|
|
45
|
+
}
|
|
46
|
+
if (fileSet.has("Gemfile")) {
|
|
47
|
+
return { language: "ruby", confidence: "high" };
|
|
48
|
+
}
|
|
49
|
+
if (fileSet.has("pom.xml") || fileSet.has("build.gradle") || fileSet.has("build.gradle.kts")) {
|
|
50
|
+
return { language: "java", confidence: "high" };
|
|
51
|
+
}
|
|
52
|
+
if (fileSet.has("tsconfig.json")) {
|
|
53
|
+
const pkgManager = fileSet.has("pnpm-lock.yaml") ? "pnpm" : fileSet.has("yarn.lock") ? "yarn" : fileSet.has("bun.lockb") || fileSet.has("bun.lock") ? "bun" : "npm";
|
|
54
|
+
let framework;
|
|
55
|
+
if (fileSet.has("package.json")) {
|
|
56
|
+
const pkg = await readFile(join(dir, "package.json"), "utf-8").catch(() => "{}");
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(pkg);
|
|
59
|
+
const deps = { ...parsed.dependencies, ...parsed.devDependencies };
|
|
60
|
+
if (deps.next) framework = "nextjs";
|
|
61
|
+
else if (deps.nuxt) framework = "nuxt";
|
|
62
|
+
else if (deps.react) framework = "react";
|
|
63
|
+
else if (deps.vue) framework = "vue";
|
|
64
|
+
else if (deps.svelte) framework = "svelte";
|
|
65
|
+
else if (deps.express) framework = "express";
|
|
66
|
+
else if (deps.fastify) framework = "fastify";
|
|
67
|
+
else if (deps.hono) framework = "hono";
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const result = {
|
|
72
|
+
language: "typescript",
|
|
73
|
+
packageManager: pkgManager,
|
|
74
|
+
confidence: "high"
|
|
75
|
+
};
|
|
76
|
+
if (framework) {
|
|
77
|
+
result.framework = framework;
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
if (fileSet.has("package.json")) {
|
|
82
|
+
const pkgManager = fileSet.has("pnpm-lock.yaml") ? "pnpm" : fileSet.has("yarn.lock") ? "yarn" : "npm";
|
|
83
|
+
return { language: "javascript", packageManager: pkgManager, confidence: "medium" };
|
|
84
|
+
}
|
|
85
|
+
return { language: "unknown", confidence: "low" };
|
|
86
|
+
}
|
|
87
|
+
function getSuggestedTemplate(detected) {
|
|
88
|
+
switch (detected.language) {
|
|
89
|
+
case "typescript":
|
|
90
|
+
return "typescript";
|
|
91
|
+
case "javascript":
|
|
92
|
+
return "typescript";
|
|
93
|
+
// Use TS template for JS too
|
|
94
|
+
case "python":
|
|
95
|
+
return "python";
|
|
96
|
+
case "rust":
|
|
97
|
+
return "rust";
|
|
98
|
+
case "go":
|
|
99
|
+
return "go";
|
|
100
|
+
default:
|
|
101
|
+
return "universal";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function parseTemplateSource(input) {
|
|
105
|
+
const builtinTemplates = [
|
|
106
|
+
"typescript",
|
|
107
|
+
"python",
|
|
108
|
+
"rust",
|
|
109
|
+
"go",
|
|
110
|
+
"universal",
|
|
111
|
+
"default",
|
|
112
|
+
"infrastructure"
|
|
113
|
+
];
|
|
114
|
+
if (builtinTemplates.includes(input)) {
|
|
115
|
+
return { type: "builtin", source: input };
|
|
116
|
+
}
|
|
117
|
+
if (input.startsWith("github:") || input.startsWith("gh:")) {
|
|
118
|
+
const source = input.replace(/^(github|gh):/, "");
|
|
119
|
+
const parsed = parseRepoPath(source);
|
|
120
|
+
const result = { type: "github", source: parsed.repo };
|
|
121
|
+
if (parsed.subdir) result.subdir = parsed.subdir;
|
|
122
|
+
if (parsed.ref) result.ref = parsed.ref;
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
if (input.startsWith("gitlab:")) {
|
|
126
|
+
const source = input.replace(/^gitlab:/, "");
|
|
127
|
+
const parsed = parseRepoPath(source);
|
|
128
|
+
const result = { type: "gitlab", source: parsed.repo };
|
|
129
|
+
if (parsed.subdir) result.subdir = parsed.subdir;
|
|
130
|
+
if (parsed.ref) result.ref = parsed.ref;
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
if (input.startsWith("npm:")) {
|
|
134
|
+
return { type: "npm", source: input.replace(/^npm:/, "") };
|
|
135
|
+
}
|
|
136
|
+
if (input.startsWith("https://") || input.startsWith("http://")) {
|
|
137
|
+
return { type: "url", source: input };
|
|
138
|
+
}
|
|
139
|
+
if (/^[\w.-]+\/[\w.-]+/.test(input)) {
|
|
140
|
+
const parsed = parseRepoPath(input);
|
|
141
|
+
const result = { type: "github", source: parsed.repo };
|
|
142
|
+
if (parsed.subdir) result.subdir = parsed.subdir;
|
|
143
|
+
if (parsed.ref) result.ref = parsed.ref;
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
return { type: "builtin", source: "universal" };
|
|
147
|
+
}
|
|
148
|
+
function parseRepoPath(input) {
|
|
149
|
+
let ref;
|
|
150
|
+
let path = input;
|
|
151
|
+
if (path.includes("#")) {
|
|
152
|
+
const parts = path.split("#");
|
|
153
|
+
path = parts[0];
|
|
154
|
+
ref = parts[1];
|
|
155
|
+
}
|
|
156
|
+
const segments = path.split("/");
|
|
157
|
+
if (segments.length >= 2) {
|
|
158
|
+
const repo = `${segments[0]}/${segments[1]}`;
|
|
159
|
+
const subdir = segments.length > 2 ? segments.slice(2).join("/") : void 0;
|
|
160
|
+
const result2 = { repo };
|
|
161
|
+
if (subdir) result2.subdir = subdir;
|
|
162
|
+
if (ref) result2.ref = ref;
|
|
163
|
+
return result2;
|
|
164
|
+
}
|
|
165
|
+
const result = { repo: path };
|
|
166
|
+
if (ref) result.ref = ref;
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
async function downloadRemoteTemplate(parsed, destDir, spinner) {
|
|
170
|
+
try {
|
|
171
|
+
const { downloadTemplate } = await import("giget");
|
|
172
|
+
let source;
|
|
173
|
+
switch (parsed.type) {
|
|
174
|
+
case "github":
|
|
175
|
+
source = `github:${parsed.source}${parsed.subdir ? "/" + parsed.subdir : ""}${parsed.ref ? "#" + parsed.ref : ""}`;
|
|
176
|
+
break;
|
|
177
|
+
case "gitlab":
|
|
178
|
+
source = `gitlab:${parsed.source}${parsed.subdir ? "/" + parsed.subdir : ""}${parsed.ref ? "#" + parsed.ref : ""}`;
|
|
179
|
+
break;
|
|
180
|
+
case "url":
|
|
181
|
+
source = parsed.source;
|
|
182
|
+
break;
|
|
183
|
+
default:
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
spinner.text = `Downloading template from ${source}...`;
|
|
187
|
+
await downloadTemplate(source, {
|
|
188
|
+
dir: destDir,
|
|
189
|
+
force: true
|
|
190
|
+
});
|
|
191
|
+
return true;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger.debug(
|
|
194
|
+
`Failed to download template: ${error instanceof Error ? error.message : String(error)}`
|
|
195
|
+
);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
var PREBUILT_IMAGE_REGISTRY = "ghcr.io/a3tai/rapid-devcontainer";
|
|
200
|
+
var PREBUILT_IMAGES = {
|
|
201
|
+
typescript: `${PREBUILT_IMAGE_REGISTRY}-typescript:latest`,
|
|
202
|
+
javascript: `${PREBUILT_IMAGE_REGISTRY}-typescript:latest`,
|
|
203
|
+
python: `${PREBUILT_IMAGE_REGISTRY}-python:latest`,
|
|
204
|
+
rust: `${PREBUILT_IMAGE_REGISTRY}-rust:latest`,
|
|
205
|
+
go: `${PREBUILT_IMAGE_REGISTRY}-go:latest`,
|
|
206
|
+
universal: `${PREBUILT_IMAGE_REGISTRY}-universal:latest`,
|
|
207
|
+
infrastructure: `${PREBUILT_IMAGE_REGISTRY}-infrastructure:latest`
|
|
208
|
+
};
|
|
209
|
+
var TEMPLATE_CUSTOMIZATIONS = {
|
|
210
|
+
typescript: {
|
|
211
|
+
vscode: {
|
|
212
|
+
extensions: [
|
|
213
|
+
"dbaeumer.vscode-eslint",
|
|
214
|
+
"esbenp.prettier-vscode",
|
|
215
|
+
"bradlc.vscode-tailwindcss",
|
|
216
|
+
"prisma.prisma",
|
|
217
|
+
"mikestead.dotenv"
|
|
218
|
+
],
|
|
219
|
+
settings: {
|
|
220
|
+
"editor.formatOnSave": true,
|
|
221
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
python: {
|
|
226
|
+
vscode: {
|
|
227
|
+
extensions: [
|
|
228
|
+
"ms-python.python",
|
|
229
|
+
"ms-python.vscode-pylance",
|
|
230
|
+
"charliermarsh.ruff",
|
|
231
|
+
"ms-toolsai.jupyter"
|
|
232
|
+
],
|
|
233
|
+
settings: {
|
|
234
|
+
"[python]": {
|
|
235
|
+
"editor.formatOnSave": true,
|
|
236
|
+
"editor.defaultFormatter": "charliermarsh.ruff"
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
rust: {
|
|
242
|
+
vscode: {
|
|
243
|
+
extensions: ["rust-lang.rust-analyzer", "tamasfe.even-better-toml", "vadimcn.vscode-lldb"],
|
|
244
|
+
settings: { "[rust]": { "editor.formatOnSave": true } }
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
go: {
|
|
248
|
+
vscode: {
|
|
249
|
+
extensions: ["golang.go", "zxh404.vscode-proto3"],
|
|
250
|
+
settings: { "[go]": { "editor.formatOnSave": true } }
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
universal: {
|
|
254
|
+
vscode: {
|
|
255
|
+
extensions: [
|
|
256
|
+
"dbaeumer.vscode-eslint",
|
|
257
|
+
"esbenp.prettier-vscode",
|
|
258
|
+
"ms-python.python",
|
|
259
|
+
"golang.go"
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
infrastructure: {
|
|
264
|
+
vscode: {
|
|
265
|
+
extensions: [
|
|
266
|
+
"hashicorp.terraform",
|
|
267
|
+
"ms-kubernetes-tools.vscode-kubernetes-tools",
|
|
268
|
+
"redhat.vscode-yaml"
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
function getPrebuiltConfig(templateName, containerEnv, postStartCommand) {
|
|
274
|
+
const image = PREBUILT_IMAGES[templateName] ?? PREBUILT_IMAGES.universal;
|
|
275
|
+
const customizations = TEMPLATE_CUSTOMIZATIONS[templateName] ?? TEMPLATE_CUSTOMIZATIONS.universal;
|
|
276
|
+
const remoteUser = templateName === "typescript" ? "node" : "vscode";
|
|
277
|
+
return {
|
|
278
|
+
name: `RAPID ${templateName.charAt(0).toUpperCase() + templateName.slice(1)} (Pre-built)`,
|
|
279
|
+
image,
|
|
280
|
+
customizations,
|
|
281
|
+
containerEnv,
|
|
282
|
+
postStartCommand,
|
|
283
|
+
remoteUser
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function getDevContainerConfig(detected, usePrebuilt = false) {
|
|
287
|
+
const baseFeatures = {
|
|
288
|
+
"ghcr.io/devcontainers/features/git:1": {},
|
|
289
|
+
"ghcr.io/devcontainers-contrib/features/direnv:1": {},
|
|
290
|
+
"ghcr.io/devcontainers-contrib/features/starship:1": {},
|
|
291
|
+
"ghcr.io/devcontainers-contrib/features/1password-cli:1": {}
|
|
292
|
+
};
|
|
293
|
+
const containerEnv = {
|
|
294
|
+
OP_SERVICE_ACCOUNT_TOKEN: "${localEnv:OP_SERVICE_ACCOUNT_TOKEN}"
|
|
295
|
+
};
|
|
296
|
+
const postCreateBase = "npm install -g @anthropic-ai/claude-code && curl -fsSL https://opencode.ai/install | bash";
|
|
297
|
+
const postStartCommand = "direnv allow 2>/dev/null || true";
|
|
298
|
+
const language = detected?.language || "unknown";
|
|
299
|
+
const templateName = language === "javascript" ? "typescript" : language === "unknown" ? "universal" : language;
|
|
300
|
+
if (usePrebuilt && PREBUILT_IMAGES[templateName]) {
|
|
301
|
+
return getPrebuiltConfig(templateName, containerEnv, postStartCommand);
|
|
302
|
+
}
|
|
303
|
+
switch (language) {
|
|
304
|
+
case "typescript":
|
|
305
|
+
case "javascript":
|
|
306
|
+
return {
|
|
307
|
+
name: "RAPID TypeScript",
|
|
308
|
+
image: "mcr.microsoft.com/devcontainers/typescript-node:22",
|
|
309
|
+
features: {
|
|
310
|
+
...baseFeatures,
|
|
311
|
+
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
|
|
312
|
+
},
|
|
313
|
+
customizations: {
|
|
314
|
+
vscode: {
|
|
315
|
+
extensions: [
|
|
316
|
+
"dbaeumer.vscode-eslint",
|
|
317
|
+
"esbenp.prettier-vscode",
|
|
318
|
+
"bradlc.vscode-tailwindcss",
|
|
319
|
+
"prisma.prisma",
|
|
320
|
+
"mikestead.dotenv"
|
|
321
|
+
],
|
|
322
|
+
settings: {
|
|
323
|
+
"editor.formatOnSave": true,
|
|
324
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
325
|
+
"editor.codeActionsOnSave": {
|
|
326
|
+
"source.fixAll.eslint": "explicit"
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
containerEnv,
|
|
332
|
+
postCreateCommand: postCreateBase,
|
|
333
|
+
postStartCommand,
|
|
334
|
+
remoteUser: "node"
|
|
335
|
+
};
|
|
336
|
+
case "python":
|
|
337
|
+
return {
|
|
338
|
+
name: "RAPID Python",
|
|
339
|
+
image: "mcr.microsoft.com/devcontainers/python:3.12",
|
|
340
|
+
features: {
|
|
341
|
+
...baseFeatures,
|
|
342
|
+
"ghcr.io/devcontainers/features/node:1": { version: "22" },
|
|
343
|
+
"ghcr.io/devcontainers-contrib/features/poetry:2": {},
|
|
344
|
+
"ghcr.io/devcontainers-contrib/features/uv:1": {}
|
|
345
|
+
},
|
|
346
|
+
customizations: {
|
|
347
|
+
vscode: {
|
|
348
|
+
extensions: [
|
|
349
|
+
"ms-python.python",
|
|
350
|
+
"ms-python.vscode-pylance",
|
|
351
|
+
"ms-python.debugpy",
|
|
352
|
+
"charliermarsh.ruff",
|
|
353
|
+
"ms-toolsai.jupyter",
|
|
354
|
+
"tamasfe.even-better-toml"
|
|
355
|
+
],
|
|
356
|
+
settings: {
|
|
357
|
+
"python.defaultInterpreterPath": "/usr/local/bin/python",
|
|
358
|
+
"[python]": {
|
|
359
|
+
"editor.formatOnSave": true,
|
|
360
|
+
"editor.defaultFormatter": "charliermarsh.ruff",
|
|
361
|
+
"editor.codeActionsOnSave": {
|
|
362
|
+
"source.fixAll": "explicit",
|
|
363
|
+
"source.organizeImports": "explicit"
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
containerEnv,
|
|
370
|
+
postCreateCommand: `${postCreateBase} && pip install aider-chat`,
|
|
371
|
+
postStartCommand,
|
|
372
|
+
remoteUser: "vscode"
|
|
373
|
+
};
|
|
374
|
+
case "rust":
|
|
375
|
+
return {
|
|
376
|
+
name: "RAPID Rust",
|
|
377
|
+
image: "mcr.microsoft.com/devcontainers/rust:latest",
|
|
378
|
+
features: {
|
|
379
|
+
...baseFeatures,
|
|
380
|
+
"ghcr.io/devcontainers/features/node:1": { version: "22" }
|
|
381
|
+
},
|
|
382
|
+
customizations: {
|
|
383
|
+
vscode: {
|
|
384
|
+
extensions: [
|
|
385
|
+
"rust-lang.rust-analyzer",
|
|
386
|
+
"tamasfe.even-better-toml",
|
|
387
|
+
"serayuzgur.crates",
|
|
388
|
+
"vadimcn.vscode-lldb"
|
|
389
|
+
],
|
|
390
|
+
settings: {
|
|
391
|
+
"rust-analyzer.checkOnSave.command": "clippy",
|
|
392
|
+
"[rust]": {
|
|
393
|
+
"editor.formatOnSave": true,
|
|
394
|
+
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
containerEnv,
|
|
400
|
+
postCreateCommand: `${postCreateBase} && rustup component add clippy rustfmt`,
|
|
401
|
+
postStartCommand,
|
|
402
|
+
remoteUser: "vscode"
|
|
403
|
+
};
|
|
404
|
+
case "go":
|
|
405
|
+
return {
|
|
406
|
+
name: "RAPID Go",
|
|
407
|
+
image: "mcr.microsoft.com/devcontainers/go:1.23",
|
|
408
|
+
features: {
|
|
409
|
+
...baseFeatures,
|
|
410
|
+
"ghcr.io/devcontainers/features/node:1": { version: "22" }
|
|
411
|
+
},
|
|
412
|
+
customizations: {
|
|
413
|
+
vscode: {
|
|
414
|
+
extensions: ["golang.go", "zxh404.vscode-proto3", "tamasfe.even-better-toml"],
|
|
415
|
+
settings: {
|
|
416
|
+
"go.useLanguageServer": true,
|
|
417
|
+
"go.lintTool": "golangci-lint",
|
|
418
|
+
"go.lintFlags": ["--fast"],
|
|
419
|
+
"[go]": {
|
|
420
|
+
"editor.formatOnSave": true,
|
|
421
|
+
"editor.codeActionsOnSave": {
|
|
422
|
+
"source.organizeImports": "explicit"
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
containerEnv,
|
|
429
|
+
postCreateCommand: `${postCreateBase} && go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && go install github.com/air-verse/air@latest`,
|
|
430
|
+
postStartCommand,
|
|
431
|
+
remoteUser: "vscode"
|
|
432
|
+
};
|
|
433
|
+
default:
|
|
434
|
+
return {
|
|
435
|
+
name: "RAPID Universal",
|
|
436
|
+
image: "mcr.microsoft.com/devcontainers/base:ubuntu",
|
|
437
|
+
features: {
|
|
438
|
+
...baseFeatures,
|
|
439
|
+
"ghcr.io/devcontainers/features/node:1": { version: "22" },
|
|
440
|
+
"ghcr.io/devcontainers/features/python:1": { version: "3.12" },
|
|
441
|
+
"ghcr.io/devcontainers/features/go:1": { version: "1.23" },
|
|
442
|
+
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
|
|
443
|
+
},
|
|
444
|
+
customizations: {
|
|
445
|
+
vscode: {
|
|
446
|
+
extensions: [
|
|
447
|
+
"dbaeumer.vscode-eslint",
|
|
448
|
+
"esbenp.prettier-vscode",
|
|
449
|
+
"ms-python.python",
|
|
450
|
+
"ms-python.vscode-pylance",
|
|
451
|
+
"golang.go",
|
|
452
|
+
"tamasfe.even-better-toml",
|
|
453
|
+
"redhat.vscode-yaml"
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
containerEnv,
|
|
458
|
+
postCreateCommand: `${postCreateBase} && pip install aider-chat`,
|
|
459
|
+
postStartCommand,
|
|
460
|
+
remoteUser: "vscode"
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function createDevContainer(dir, detected, force = false, usePrebuilt = false) {
|
|
465
|
+
const devcontainerDir = join(dir, ".devcontainer");
|
|
466
|
+
const devcontainerJsonPath = join(devcontainerDir, "devcontainer.json");
|
|
467
|
+
if (!force && existsSync(devcontainerJsonPath)) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
await mkdir(devcontainerDir, { recursive: true });
|
|
471
|
+
const config = getDevContainerConfig(detected, usePrebuilt);
|
|
472
|
+
await writeFile(devcontainerJsonPath, JSON.stringify(config, null, 2) + "\n");
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
var initCommand = new Command("init").description("Initialize RAPID in a project").argument("[template]", "Template: builtin name, github:user/repo, npm:package, or URL").option("--force", "Overwrite existing files", false).option("--agent <name>", "Default agent to configure", "claude").option("--no-devcontainer", "Skip devcontainer creation").option("--prebuilt", "Use pre-built devcontainer images from ghcr.io (faster startup)", false).option("--mcp <servers>", "MCP servers to enable (comma-separated)", "context7,tavily").option("--no-mcp", "Skip MCP server configuration").option("--no-detect", "Skip auto-detection of project type").action(async (templateArg, options) => {
|
|
476
|
+
const spinner = ora("Initializing RAPID...").start();
|
|
477
|
+
try {
|
|
478
|
+
const cwd = process.cwd();
|
|
479
|
+
const configPath = join(cwd, "rapid.json");
|
|
480
|
+
if (!options.force) {
|
|
481
|
+
try {
|
|
482
|
+
await access(configPath);
|
|
483
|
+
spinner.fail("rapid.json already exists. Use --force to overwrite.");
|
|
484
|
+
process.exit(1);
|
|
485
|
+
} catch {
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
let detectedProject;
|
|
489
|
+
let templateSource = templateArg;
|
|
490
|
+
if (!templateArg && options.detect !== false) {
|
|
491
|
+
spinner.text = "Detecting project type...";
|
|
492
|
+
detectedProject = await detectProjectType(cwd);
|
|
493
|
+
if (detectedProject.language !== "unknown") {
|
|
494
|
+
const suggested = getSuggestedTemplate(detectedProject);
|
|
495
|
+
spinner.succeed(
|
|
496
|
+
`Detected ${detectedProject.language}${detectedProject.framework ? ` (${detectedProject.framework})` : ""} project`
|
|
497
|
+
);
|
|
498
|
+
templateSource = suggested;
|
|
499
|
+
logger.info(`Using ${logger.brand(suggested)} template`);
|
|
500
|
+
} else {
|
|
501
|
+
spinner.info("Could not detect project type, using universal template");
|
|
502
|
+
templateSource = "universal";
|
|
503
|
+
}
|
|
504
|
+
spinner.start("Initializing RAPID...");
|
|
505
|
+
}
|
|
506
|
+
const parsed = parseTemplateSource(templateSource || "universal");
|
|
507
|
+
if (parsed.type !== "builtin") {
|
|
508
|
+
spinner.text = `Fetching template from ${parsed.source}...`;
|
|
509
|
+
if (parsed.type === "npm") {
|
|
510
|
+
spinner.fail("npm template support coming soon. Use github:user/repo instead.");
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
const downloaded = await downloadRemoteTemplate(parsed, cwd, spinner);
|
|
514
|
+
if (!downloaded) {
|
|
515
|
+
spinner.fail(`Failed to download template from ${parsed.source}`);
|
|
516
|
+
logger.info("Make sure the repository exists and is accessible.");
|
|
517
|
+
logger.info("For private repos, set GIGET_AUTH environment variable.");
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
spinner.succeed(`Downloaded template from ${parsed.source}`);
|
|
521
|
+
try {
|
|
522
|
+
await access(join(cwd, "rapid.json"));
|
|
523
|
+
logger.blank();
|
|
524
|
+
logger.info("Template includes rapid.json configuration.");
|
|
525
|
+
logger.info("Run `rapid dev` to start coding!");
|
|
526
|
+
return;
|
|
527
|
+
} catch {
|
|
528
|
+
spinner.start("Creating RAPID configuration...");
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const mcpServers = options.mcp === false ? [] : options.mcp.split(",").map((s) => s.trim());
|
|
532
|
+
let config = createConfig(options, detectedProject);
|
|
533
|
+
if (mcpServers.length > 0) {
|
|
534
|
+
spinner.text = "Configuring MCP servers...";
|
|
535
|
+
for (const serverName of mcpServers) {
|
|
536
|
+
if (MCP_SERVER_TEMPLATES[serverName]) {
|
|
537
|
+
config = addMcpServerFromTemplate(config, serverName);
|
|
538
|
+
} else {
|
|
539
|
+
logger.warn(`Unknown MCP server template: ${serverName}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const secretRefs = getSecretReferences(mcpServers);
|
|
543
|
+
if (Object.keys(secretRefs).length > 0) {
|
|
544
|
+
config.secrets = {
|
|
545
|
+
...config.secrets,
|
|
546
|
+
provider: "1password",
|
|
547
|
+
vault: "Development",
|
|
548
|
+
items: {
|
|
549
|
+
...config.secrets?.items,
|
|
550
|
+
...secretRefs
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
spinner.text = "Writing rapid.json...";
|
|
556
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
557
|
+
if (mcpServers.length > 0) {
|
|
558
|
+
spinner.text = "Generating MCP configuration files...";
|
|
559
|
+
await writeMcpConfig(cwd, config);
|
|
560
|
+
await writeOpenCodeConfig(cwd, config);
|
|
561
|
+
}
|
|
562
|
+
if (config.agents.available.claude) {
|
|
563
|
+
spinner.text = "Creating CLAUDE.md...";
|
|
564
|
+
const claudeMdPath = join(cwd, "CLAUDE.md");
|
|
565
|
+
await writeFile(claudeMdPath, getClaudeMdTemplate(cwd, detectedProject));
|
|
566
|
+
}
|
|
567
|
+
spinner.text = "Creating AGENTS.md...";
|
|
568
|
+
const agentsMdPath = join(cwd, "AGENTS.md");
|
|
569
|
+
await writeFile(agentsMdPath, getAgentsMdTemplate(cwd, detectedProject));
|
|
570
|
+
let devcontainerCreated = false;
|
|
571
|
+
const usePrebuilt = options.prebuilt === true;
|
|
572
|
+
if (options.devcontainer !== false) {
|
|
573
|
+
spinner.text = usePrebuilt ? "Creating devcontainer configuration (using pre-built image)..." : "Creating devcontainer configuration...";
|
|
574
|
+
devcontainerCreated = await createDevContainer(
|
|
575
|
+
cwd,
|
|
576
|
+
detectedProject,
|
|
577
|
+
options.force,
|
|
578
|
+
usePrebuilt
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
spinner.succeed("RAPID initialized successfully!");
|
|
582
|
+
if (detectedProject && detectedProject.language !== "unknown") {
|
|
583
|
+
logger.blank();
|
|
584
|
+
logger.info("Project detected:");
|
|
585
|
+
console.log(` ${logger.dim("Language:")} ${detectedProject.language}`);
|
|
586
|
+
if (detectedProject.framework) {
|
|
587
|
+
console.log(` ${logger.dim("Framework:")} ${detectedProject.framework}`);
|
|
588
|
+
}
|
|
589
|
+
if (detectedProject.packageManager) {
|
|
590
|
+
console.log(` ${logger.dim("Package Mgr:")} ${detectedProject.packageManager}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
logger.blank();
|
|
594
|
+
logger.info("Created files:");
|
|
595
|
+
console.log(` ${logger.dim("\u2022")} rapid.json`);
|
|
596
|
+
if (mcpServers.length > 0) {
|
|
597
|
+
console.log(` ${logger.dim("\u2022")} .mcp.json`);
|
|
598
|
+
console.log(` ${logger.dim("\u2022")} opencode.json`);
|
|
599
|
+
}
|
|
600
|
+
if (devcontainerCreated) {
|
|
601
|
+
console.log(` ${logger.dim("\u2022")} .devcontainer/devcontainer.json`);
|
|
602
|
+
}
|
|
603
|
+
console.log(` ${logger.dim("\u2022")} CLAUDE.md`);
|
|
604
|
+
console.log(` ${logger.dim("\u2022")} AGENTS.md`);
|
|
605
|
+
if (mcpServers.length > 0) {
|
|
606
|
+
logger.blank();
|
|
607
|
+
logger.info("MCP servers configured:");
|
|
608
|
+
for (const serverName of mcpServers) {
|
|
609
|
+
const template = MCP_SERVER_TEMPLATES[serverName];
|
|
610
|
+
if (template) {
|
|
611
|
+
console.log(` ${logger.brand("\u2022")} ${serverName} - ${template.description}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
logger.blank();
|
|
616
|
+
logger.info("Next steps:");
|
|
617
|
+
let stepNum = 1;
|
|
618
|
+
console.log(
|
|
619
|
+
` ${logger.dim(`${stepNum++}.`)} Run ${logger.brand("rapid dev")} to start coding`
|
|
620
|
+
);
|
|
621
|
+
console.log(
|
|
622
|
+
` ${logger.dim(`${stepNum++}.`)} Edit ${logger.dim("rapid.json")} to customize your setup`
|
|
623
|
+
);
|
|
624
|
+
if (mcpServers.length > 0) {
|
|
625
|
+
console.log(
|
|
626
|
+
` ${logger.dim(`${stepNum++}.`)} Add API keys to ${logger.dim("secrets.items")} in rapid.json`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
if (devcontainerCreated) {
|
|
630
|
+
console.log(
|
|
631
|
+
` ${logger.dim(`${stepNum++}.`)} Set ${logger.dim("OP_SERVICE_ACCOUNT_TOKEN")} env var for 1Password secrets`
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
logger.blank();
|
|
635
|
+
} catch (error) {
|
|
636
|
+
spinner.fail("Failed to initialize RAPID");
|
|
637
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
function createConfig(options, detectedProject) {
|
|
642
|
+
const defaults = getDefaultConfig();
|
|
643
|
+
const config = {
|
|
644
|
+
$schema: "https://getrapid.dev/schema/v1/rapid.json",
|
|
645
|
+
version: "1.0",
|
|
646
|
+
agents: {
|
|
647
|
+
default: options.agent,
|
|
648
|
+
available: defaults.agents.available
|
|
649
|
+
},
|
|
650
|
+
secrets: {
|
|
651
|
+
provider: "env"
|
|
652
|
+
},
|
|
653
|
+
context: {
|
|
654
|
+
files: ["README.md", "CLAUDE.md", "AGENTS.md"],
|
|
655
|
+
generateAgentFiles: false
|
|
656
|
+
// We already created them
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
if (detectedProject && detectedProject.language !== "unknown") {
|
|
660
|
+
config.context = {
|
|
661
|
+
...config.context
|
|
662
|
+
// Store detected info for potential future use
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
return config;
|
|
666
|
+
}
|
|
667
|
+
function getClaudeMdTemplate(projectPath, detectedProject) {
|
|
668
|
+
const projectName = projectPath.split("/").pop() || "project";
|
|
669
|
+
let languageSection = "";
|
|
670
|
+
if (detectedProject && detectedProject.language !== "unknown") {
|
|
671
|
+
languageSection = `## Technology Stack
|
|
672
|
+
|
|
673
|
+
- **Language**: ${detectedProject.language}${detectedProject.framework ? `
|
|
674
|
+
- **Framework**: ${detectedProject.framework}` : ""}${detectedProject.packageManager ? `
|
|
675
|
+
- **Package Manager**: ${detectedProject.packageManager}` : ""}
|
|
676
|
+
|
|
677
|
+
`;
|
|
678
|
+
}
|
|
679
|
+
return `# Claude Instructions
|
|
680
|
+
|
|
681
|
+
## Project: ${projectName}
|
|
682
|
+
|
|
683
|
+
This file contains instructions for Claude Code when working on this project.
|
|
684
|
+
|
|
685
|
+
## Overview
|
|
686
|
+
|
|
687
|
+
<!-- Describe your project here -->
|
|
688
|
+
|
|
689
|
+
${languageSection}${RAPID_METHODOLOGY}
|
|
690
|
+
${MCP_USAGE_GUIDELINES}
|
|
691
|
+
${GIT_GUIDELINES}
|
|
692
|
+
## Key Files
|
|
693
|
+
|
|
694
|
+
- \`rapid.json\` - RAPID configuration
|
|
695
|
+
- \`README.md\` - Project documentation
|
|
696
|
+
|
|
697
|
+
## Commands
|
|
698
|
+
|
|
699
|
+
\`\`\`bash
|
|
700
|
+
# Start development
|
|
701
|
+
rapid dev
|
|
702
|
+
|
|
703
|
+
# Check status
|
|
704
|
+
rapid status
|
|
705
|
+
\`\`\`
|
|
706
|
+
`;
|
|
707
|
+
}
|
|
708
|
+
function getAgentsMdTemplate(projectPath, detectedProject) {
|
|
709
|
+
const projectName = projectPath.split("/").pop() || "project";
|
|
710
|
+
let languageSection = "";
|
|
711
|
+
if (detectedProject && detectedProject.language !== "unknown") {
|
|
712
|
+
languageSection = `## Technology Stack
|
|
713
|
+
|
|
714
|
+
- **Language**: ${detectedProject.language}${detectedProject.framework ? `
|
|
715
|
+
- **Framework**: ${detectedProject.framework}` : ""}${detectedProject.packageManager ? `
|
|
716
|
+
- **Package Manager**: ${detectedProject.packageManager}` : ""}
|
|
717
|
+
|
|
718
|
+
`;
|
|
719
|
+
}
|
|
720
|
+
return `# Agent Instructions
|
|
721
|
+
|
|
722
|
+
## Project: ${projectName}
|
|
723
|
+
|
|
724
|
+
This file contains instructions for AI coding agents working on this project.
|
|
725
|
+
|
|
726
|
+
## Overview
|
|
727
|
+
|
|
728
|
+
<!-- Describe your project here -->
|
|
729
|
+
|
|
730
|
+
${languageSection}${RAPID_METHODOLOGY}
|
|
731
|
+
${MCP_USAGE_GUIDELINES}
|
|
732
|
+
${GIT_GUIDELINES}
|
|
733
|
+
## Project Structure
|
|
734
|
+
|
|
735
|
+
\`\`\`
|
|
736
|
+
.
|
|
737
|
+
\u251C\u2500\u2500 rapid.json # RAPID configuration
|
|
738
|
+
\u251C\u2500\u2500 CLAUDE.md # Claude-specific instructions
|
|
739
|
+
\u251C\u2500\u2500 AGENTS.md # Generic agent instructions
|
|
740
|
+
\u2514\u2500\u2500 ...
|
|
741
|
+
\`\`\`
|
|
742
|
+
|
|
743
|
+
## Getting Started
|
|
744
|
+
|
|
745
|
+
1. Review the project structure
|
|
746
|
+
2. Check \`rapid.json\` for configuration
|
|
747
|
+
3. Follow the RAPID methodology above when making changes
|
|
748
|
+
`;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/commands/dev.ts
|
|
752
|
+
import { Command as Command2 } from "commander";
|
|
753
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
754
|
+
import { isAbsolute, join as join4 } from "path";
|
|
755
|
+
import {
|
|
756
|
+
loadConfig,
|
|
757
|
+
getAgent,
|
|
758
|
+
checkAgentAvailable,
|
|
759
|
+
logger as logger2,
|
|
760
|
+
getContainerStatus,
|
|
761
|
+
startContainer,
|
|
762
|
+
execInContainer,
|
|
763
|
+
hasDevcontainerCli,
|
|
764
|
+
loadSecrets,
|
|
765
|
+
hasOpCli,
|
|
766
|
+
isOpAuthenticated,
|
|
767
|
+
hasVaultCli,
|
|
768
|
+
isVaultAuthenticated,
|
|
769
|
+
buildAgentArgs,
|
|
770
|
+
agentSupportsRuntimeInjection
|
|
771
|
+
} from "@a3t/rapid-core";
|
|
772
|
+
import ora2 from "ora";
|
|
773
|
+
|
|
774
|
+
// src/utils/worktree.ts
|
|
775
|
+
import { execa } from "execa";
|
|
776
|
+
import { access as access2 } from "fs/promises";
|
|
777
|
+
import { basename, dirname, join as join2, resolve } from "path";
|
|
778
|
+
function getErrorMessage(err) {
|
|
779
|
+
if (typeof err.stderr === "string" && err.stderr) {
|
|
780
|
+
return err.stderr;
|
|
781
|
+
}
|
|
782
|
+
return err.message;
|
|
783
|
+
}
|
|
784
|
+
async function isGitRepo(dir) {
|
|
785
|
+
try {
|
|
786
|
+
await execa("git", ["rev-parse", "--git-dir"], { cwd: dir });
|
|
787
|
+
return true;
|
|
788
|
+
} catch {
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async function getGitRoot(dir) {
|
|
793
|
+
const { stdout } = await execa("git", ["rev-parse", "--show-toplevel"], { cwd: dir });
|
|
794
|
+
return stdout.trim();
|
|
795
|
+
}
|
|
796
|
+
async function getCurrentBranch(dir) {
|
|
797
|
+
try {
|
|
798
|
+
const { stdout: symbolicRef } = await execa("git", ["symbolic-ref", "-q", "HEAD"], {
|
|
799
|
+
cwd: dir,
|
|
800
|
+
reject: false
|
|
801
|
+
});
|
|
802
|
+
if (!symbolicRef) {
|
|
803
|
+
return { name: null, isDefault: false, detached: true };
|
|
804
|
+
}
|
|
805
|
+
const branchName = symbolicRef.trim().replace("refs/heads/", "");
|
|
806
|
+
const isDefault = branchName === "main" || branchName === "master";
|
|
807
|
+
return { name: branchName, isDefault, detached: false };
|
|
808
|
+
} catch {
|
|
809
|
+
return { name: null, isDefault: false, detached: true };
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
async function getDefaultBranch(dir) {
|
|
813
|
+
try {
|
|
814
|
+
const { stdout } = await execa("git", ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"], {
|
|
815
|
+
cwd: dir,
|
|
816
|
+
reject: false
|
|
817
|
+
});
|
|
818
|
+
if (stdout) {
|
|
819
|
+
return stdout.trim().replace("origin/", "");
|
|
820
|
+
}
|
|
821
|
+
const { stdout: branches } = await execa("git", ["branch", "--list", "main", "master"], {
|
|
822
|
+
cwd: dir
|
|
823
|
+
});
|
|
824
|
+
if (branches.includes("main")) return "main";
|
|
825
|
+
if (branches.includes("master")) return "master";
|
|
826
|
+
return "main";
|
|
827
|
+
} catch {
|
|
828
|
+
return "main";
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async function listWorktrees(dir) {
|
|
832
|
+
const { stdout } = await execa("git", ["worktree", "list", "--porcelain"], { cwd: dir });
|
|
833
|
+
const worktrees = [];
|
|
834
|
+
let current = {};
|
|
835
|
+
for (const line of stdout.split("\n")) {
|
|
836
|
+
if (line.startsWith("worktree ")) {
|
|
837
|
+
if (current.path) {
|
|
838
|
+
worktrees.push(current);
|
|
839
|
+
}
|
|
840
|
+
current = {
|
|
841
|
+
path: line.substring(9),
|
|
842
|
+
isMain: false,
|
|
843
|
+
locked: false,
|
|
844
|
+
exists: true,
|
|
845
|
+
prunable: false
|
|
846
|
+
};
|
|
847
|
+
} else if (line.startsWith("HEAD ")) {
|
|
848
|
+
current.head = line.substring(5);
|
|
849
|
+
} else if (line.startsWith("branch ")) {
|
|
850
|
+
current.branch = line.substring(7).replace("refs/heads/", "");
|
|
851
|
+
} else if (line === "bare") {
|
|
852
|
+
current.isMain = true;
|
|
853
|
+
} else if (line === "locked") {
|
|
854
|
+
current.locked = true;
|
|
855
|
+
} else if (line === "prunable") {
|
|
856
|
+
current.prunable = true;
|
|
857
|
+
current.exists = false;
|
|
858
|
+
} else if (line === "detached") {
|
|
859
|
+
current.branch = null;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (current.path) {
|
|
863
|
+
worktrees.push(current);
|
|
864
|
+
}
|
|
865
|
+
if (worktrees.length > 0 && worktrees[0]) {
|
|
866
|
+
worktrees[0].isMain = true;
|
|
867
|
+
}
|
|
868
|
+
for (const wt of worktrees) {
|
|
869
|
+
try {
|
|
870
|
+
await access2(wt.path);
|
|
871
|
+
wt.exists = true;
|
|
872
|
+
} catch {
|
|
873
|
+
wt.exists = false;
|
|
874
|
+
wt.prunable = true;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return worktrees;
|
|
878
|
+
}
|
|
879
|
+
async function findWorktreeByBranch(dir, branch) {
|
|
880
|
+
const worktrees = await listWorktrees(dir);
|
|
881
|
+
return worktrees.find((wt) => wt.branch === branch) ?? null;
|
|
882
|
+
}
|
|
883
|
+
function generateWorktreePath(repoRoot, branchName) {
|
|
884
|
+
const projectName = basename(repoRoot);
|
|
885
|
+
const parentDir = dirname(repoRoot);
|
|
886
|
+
const safeBranchName = branchName.replace(/\//g, "-").replace(/[^a-zA-Z0-9-_]/g, "").toLowerCase();
|
|
887
|
+
return join2(parentDir, `${projectName}-${safeBranchName}`);
|
|
888
|
+
}
|
|
889
|
+
async function createWorktree(repoRoot, worktreePath, branch, options = {}) {
|
|
890
|
+
try {
|
|
891
|
+
const existing = await listWorktrees(repoRoot);
|
|
892
|
+
const existingAtPath = existing.find((wt) => resolve(wt.path) === resolve(worktreePath));
|
|
893
|
+
if (existingAtPath && existingAtPath.exists && !options.force) {
|
|
894
|
+
return { success: true, path: worktreePath };
|
|
895
|
+
}
|
|
896
|
+
if (existingAtPath && !existingAtPath.exists) {
|
|
897
|
+
await execa("git", ["worktree", "remove", "--force", worktreePath], {
|
|
898
|
+
cwd: repoRoot,
|
|
899
|
+
reject: false
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
const args = ["worktree", "add"];
|
|
903
|
+
if (options.force) {
|
|
904
|
+
args.push("--force");
|
|
905
|
+
}
|
|
906
|
+
if (options.newBranch) {
|
|
907
|
+
args.push("-b", options.newBranch);
|
|
908
|
+
}
|
|
909
|
+
args.push(worktreePath);
|
|
910
|
+
if (!options.newBranch) {
|
|
911
|
+
args.push(branch);
|
|
912
|
+
} else if (options.startPoint) {
|
|
913
|
+
args.push(options.startPoint);
|
|
914
|
+
}
|
|
915
|
+
await execa("git", args, { cwd: repoRoot });
|
|
916
|
+
return { success: true, path: worktreePath };
|
|
917
|
+
} catch (err) {
|
|
918
|
+
const error = err;
|
|
919
|
+
return {
|
|
920
|
+
success: false,
|
|
921
|
+
path: worktreePath,
|
|
922
|
+
error: getErrorMessage(error)
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
async function removeWorktree(repoRoot, worktreePath, options = {}) {
|
|
927
|
+
try {
|
|
928
|
+
const args = ["worktree", "remove"];
|
|
929
|
+
if (options.force) {
|
|
930
|
+
args.push("--force");
|
|
931
|
+
}
|
|
932
|
+
args.push(worktreePath);
|
|
933
|
+
await execa("git", args, { cwd: repoRoot });
|
|
934
|
+
return { success: true };
|
|
935
|
+
} catch (err) {
|
|
936
|
+
const error = err;
|
|
937
|
+
return {
|
|
938
|
+
success: false,
|
|
939
|
+
error: getErrorMessage(error)
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async function pruneWorktrees(repoRoot) {
|
|
944
|
+
try {
|
|
945
|
+
const worktrees = await listWorktrees(repoRoot);
|
|
946
|
+
const prunable = worktrees.filter((wt) => wt.prunable).map((wt) => wt.path);
|
|
947
|
+
await execa("git", ["worktree", "prune"], { cwd: repoRoot });
|
|
948
|
+
return { success: true, pruned: prunable };
|
|
949
|
+
} catch (err) {
|
|
950
|
+
const error = err;
|
|
951
|
+
return {
|
|
952
|
+
success: false,
|
|
953
|
+
pruned: [],
|
|
954
|
+
error: getErrorMessage(error)
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
async function getOrCreateWorktreeForBranch(dir) {
|
|
959
|
+
const gitRoot = await getGitRoot(dir);
|
|
960
|
+
const branch = await getCurrentBranch(dir);
|
|
961
|
+
if (branch.isDefault || branch.detached || !branch.name) {
|
|
962
|
+
return { path: gitRoot, created: false, isMain: true };
|
|
963
|
+
}
|
|
964
|
+
const existing = await findWorktreeByBranch(gitRoot, branch.name);
|
|
965
|
+
if (existing && existing.exists) {
|
|
966
|
+
return { path: existing.path, created: false, isMain: existing.isMain };
|
|
967
|
+
}
|
|
968
|
+
const worktreePath = generateWorktreePath(gitRoot, branch.name);
|
|
969
|
+
const result = await createWorktree(gitRoot, worktreePath, branch.name);
|
|
970
|
+
if (!result.success) {
|
|
971
|
+
return { path: gitRoot, created: false, isMain: true };
|
|
972
|
+
}
|
|
973
|
+
return { path: worktreePath, created: true, isMain: false };
|
|
974
|
+
}
|
|
975
|
+
async function cleanupMergedWorktrees(repoRoot) {
|
|
976
|
+
const removed = [];
|
|
977
|
+
const errors = [];
|
|
978
|
+
const defaultBranch = await getDefaultBranch(repoRoot);
|
|
979
|
+
const { stdout } = await execa("git", ["branch", "--merged", defaultBranch], { cwd: repoRoot });
|
|
980
|
+
const mergedBranches = stdout.split("\n").map((b) => b.trim().replace(/^\*\s*/, "")).filter((b) => b && b !== defaultBranch);
|
|
981
|
+
const worktrees = await listWorktrees(repoRoot);
|
|
982
|
+
for (const wt of worktrees) {
|
|
983
|
+
if (wt.isMain || !wt.branch) continue;
|
|
984
|
+
if (mergedBranches.includes(wt.branch)) {
|
|
985
|
+
const result = await removeWorktree(repoRoot, wt.path, { force: true });
|
|
986
|
+
if (result.success) {
|
|
987
|
+
removed.push(wt.path);
|
|
988
|
+
} else if (result.error) {
|
|
989
|
+
errors.push(`${wt.path}: ${result.error}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return { removed, errors };
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/isolation/lima.ts
|
|
997
|
+
import { execa as execa2 } from "execa";
|
|
998
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
999
|
+
import { homedir, platform } from "os";
|
|
1000
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
1001
|
+
import { fileURLToPath } from "url";
|
|
1002
|
+
function getErrorMessage2(err) {
|
|
1003
|
+
if (typeof err.stderr === "string" && err.stderr) {
|
|
1004
|
+
return err.stderr;
|
|
1005
|
+
}
|
|
1006
|
+
return err.message;
|
|
1007
|
+
}
|
|
1008
|
+
function getOutputString(output) {
|
|
1009
|
+
if (typeof output === "string") {
|
|
1010
|
+
return output;
|
|
1011
|
+
}
|
|
1012
|
+
return void 0;
|
|
1013
|
+
}
|
|
1014
|
+
var RAPID_LIMA_INSTANCE = "rapid";
|
|
1015
|
+
var RAPID_LIMA_DIR = join3(homedir(), ".rapid", "lima");
|
|
1016
|
+
async function hasLima() {
|
|
1017
|
+
try {
|
|
1018
|
+
await execa2("limactl", ["--version"]);
|
|
1019
|
+
return true;
|
|
1020
|
+
} catch {
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
function isMacOS() {
|
|
1025
|
+
return platform() === "darwin";
|
|
1026
|
+
}
|
|
1027
|
+
function getLimaTemplatePath() {
|
|
1028
|
+
const __dirname3 = dirname2(fileURLToPath(import.meta.url));
|
|
1029
|
+
const possiblePaths = [
|
|
1030
|
+
join3(__dirname3, "../../../../templates/lima.yaml"),
|
|
1031
|
+
// From dist/isolation/
|
|
1032
|
+
join3(__dirname3, "../../../templates/lima.yaml"),
|
|
1033
|
+
// From src/isolation/
|
|
1034
|
+
join3(homedir(), ".rapid", "lima.yaml")
|
|
1035
|
+
// User config
|
|
1036
|
+
];
|
|
1037
|
+
return possiblePaths[0];
|
|
1038
|
+
}
|
|
1039
|
+
async function listInstances() {
|
|
1040
|
+
try {
|
|
1041
|
+
const { stdout } = await execa2("limactl", ["list", "--json"]);
|
|
1042
|
+
const instances = JSON.parse(stdout);
|
|
1043
|
+
return instances.map((inst) => {
|
|
1044
|
+
const result = {
|
|
1045
|
+
name: inst.name,
|
|
1046
|
+
status: inst.status,
|
|
1047
|
+
arch: inst.arch,
|
|
1048
|
+
cpus: inst.cpus,
|
|
1049
|
+
memory: `${Math.round(inst.memory / 1024 / 1024 / 1024)}GiB`,
|
|
1050
|
+
disk: `${Math.round(inst.disk / 1024 / 1024 / 1024)}GiB`,
|
|
1051
|
+
dir: inst.dir
|
|
1052
|
+
};
|
|
1053
|
+
if (inst.sshLocalPort !== void 0) {
|
|
1054
|
+
result.sshLocalPort = inst.sshLocalPort;
|
|
1055
|
+
}
|
|
1056
|
+
return result;
|
|
1057
|
+
});
|
|
1058
|
+
} catch {
|
|
1059
|
+
return [];
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
async function getInstance(name = RAPID_LIMA_INSTANCE) {
|
|
1063
|
+
const instances = await listInstances();
|
|
1064
|
+
return instances.find((i) => i.name === name) ?? null;
|
|
1065
|
+
}
|
|
1066
|
+
async function instanceExists(name = RAPID_LIMA_INSTANCE) {
|
|
1067
|
+
const instance = await getInstance(name);
|
|
1068
|
+
return instance !== null;
|
|
1069
|
+
}
|
|
1070
|
+
async function isRunning(name = RAPID_LIMA_INSTANCE) {
|
|
1071
|
+
const instance = await getInstance(name);
|
|
1072
|
+
return instance?.status === "Running";
|
|
1073
|
+
}
|
|
1074
|
+
async function createLimaConfig(projectDir, options = {}) {
|
|
1075
|
+
const templatePath = getLimaTemplatePath();
|
|
1076
|
+
const configDir = RAPID_LIMA_DIR;
|
|
1077
|
+
const configPath = join3(configDir, "lima.yaml");
|
|
1078
|
+
await mkdir2(configDir, { recursive: true });
|
|
1079
|
+
let template;
|
|
1080
|
+
try {
|
|
1081
|
+
template = await readFile2(templatePath, "utf-8");
|
|
1082
|
+
} catch {
|
|
1083
|
+
template = getMinimalLimaConfig();
|
|
1084
|
+
}
|
|
1085
|
+
let config = template;
|
|
1086
|
+
const projectMount = `
|
|
1087
|
+
- location: "${projectDir}"
|
|
1088
|
+
writable: true`;
|
|
1089
|
+
config = config.replace(
|
|
1090
|
+
/mounts:\s*\n\s*- location: "~"/,
|
|
1091
|
+
`mounts:
|
|
1092
|
+
- location: "~"${projectMount}`
|
|
1093
|
+
);
|
|
1094
|
+
if (options.cpus) {
|
|
1095
|
+
config = config.replace(/cpus: \d+/, `cpus: ${options.cpus}`);
|
|
1096
|
+
}
|
|
1097
|
+
if (options.memory) {
|
|
1098
|
+
config = config.replace(/memory: "[^"]*"/, `memory: "${options.memory}"`);
|
|
1099
|
+
}
|
|
1100
|
+
if (options.disk) {
|
|
1101
|
+
config = config.replace(/disk: "[^"]*"/, `disk: "${options.disk}"`);
|
|
1102
|
+
}
|
|
1103
|
+
if (options.env) {
|
|
1104
|
+
const envLines = Object.entries(options.env).map(([key, value]) => ` ${key}: "${value}"`).join("\n");
|
|
1105
|
+
config = config.replace(/env:[\s\S]*?(?=\n\w|\n#|$)/, `env:
|
|
1106
|
+
${envLines}
|
|
1107
|
+
`);
|
|
1108
|
+
}
|
|
1109
|
+
await writeFile2(configPath, config);
|
|
1110
|
+
return configPath;
|
|
1111
|
+
}
|
|
1112
|
+
function getMinimalLimaConfig() {
|
|
1113
|
+
return `
|
|
1114
|
+
vmType: "vz"
|
|
1115
|
+
rosetta:
|
|
1116
|
+
enabled: true
|
|
1117
|
+
binfmt: true
|
|
1118
|
+
cpus: 4
|
|
1119
|
+
memory: "8GiB"
|
|
1120
|
+
disk: "50GiB"
|
|
1121
|
+
images:
|
|
1122
|
+
- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
|
|
1123
|
+
arch: "aarch64"
|
|
1124
|
+
- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
|
|
1125
|
+
arch: "x86_64"
|
|
1126
|
+
mountType: "virtiofs"
|
|
1127
|
+
mounts:
|
|
1128
|
+
- location: "~"
|
|
1129
|
+
writable: true
|
|
1130
|
+
mountInotify: true
|
|
1131
|
+
ssh:
|
|
1132
|
+
forwardAgent: true
|
|
1133
|
+
localPort: 0
|
|
1134
|
+
networks:
|
|
1135
|
+
- vzNAT: true
|
|
1136
|
+
containerd:
|
|
1137
|
+
system: true
|
|
1138
|
+
user: false
|
|
1139
|
+
portForwards:
|
|
1140
|
+
- guestPort: 3000
|
|
1141
|
+
hostPort: 3000
|
|
1142
|
+
- guestPort: 8080
|
|
1143
|
+
hostPort: 8080
|
|
1144
|
+
provision:
|
|
1145
|
+
- mode: system
|
|
1146
|
+
script: |
|
|
1147
|
+
#!/bin/bash
|
|
1148
|
+
set -eux
|
|
1149
|
+
apt-get update
|
|
1150
|
+
apt-get install -y build-essential curl git jq
|
|
1151
|
+
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
|
1152
|
+
apt-get install -y nodejs
|
|
1153
|
+
npm install -g pnpm
|
|
1154
|
+
- mode: user
|
|
1155
|
+
script: |
|
|
1156
|
+
#!/bin/bash
|
|
1157
|
+
set -eux
|
|
1158
|
+
npm install -g @anthropic-ai/claude-code || true
|
|
1159
|
+
curl -fsSL https://opencode.ai/install | bash || true
|
|
1160
|
+
`;
|
|
1161
|
+
}
|
|
1162
|
+
async function startInstance(projectDir, options = {}) {
|
|
1163
|
+
const name = RAPID_LIMA_INSTANCE;
|
|
1164
|
+
if (!await hasLima()) {
|
|
1165
|
+
return {
|
|
1166
|
+
success: false,
|
|
1167
|
+
error: "Lima is not installed. Install with: brew install lima"
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
if (!isMacOS()) {
|
|
1171
|
+
return {
|
|
1172
|
+
success: false,
|
|
1173
|
+
error: "Lima is only available on macOS"
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
try {
|
|
1177
|
+
const exists = await instanceExists(name);
|
|
1178
|
+
if (exists) {
|
|
1179
|
+
if (await isRunning(name)) {
|
|
1180
|
+
return { success: true };
|
|
1181
|
+
}
|
|
1182
|
+
await execa2("limactl", ["start", name], {
|
|
1183
|
+
timeout: (options.timeout ?? 300) * 1e3
|
|
1184
|
+
});
|
|
1185
|
+
} else {
|
|
1186
|
+
const configPath = await createLimaConfig(projectDir, options);
|
|
1187
|
+
await execa2("limactl", ["start", "--name", name, configPath], {
|
|
1188
|
+
timeout: (options.timeout ?? 600) * 1e3,
|
|
1189
|
+
stdio: "inherit"
|
|
1190
|
+
// Show progress
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
return { success: true };
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
const error = err;
|
|
1196
|
+
return {
|
|
1197
|
+
success: false,
|
|
1198
|
+
error: getErrorMessage2(error)
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
async function stopInstance(name = RAPID_LIMA_INSTANCE, options = {}) {
|
|
1203
|
+
try {
|
|
1204
|
+
const args = ["stop"];
|
|
1205
|
+
if (options.force) {
|
|
1206
|
+
args.push("--force");
|
|
1207
|
+
}
|
|
1208
|
+
args.push(name);
|
|
1209
|
+
await execa2("limactl", args);
|
|
1210
|
+
return { success: true };
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
const error = err;
|
|
1213
|
+
return {
|
|
1214
|
+
success: false,
|
|
1215
|
+
error: getErrorMessage2(error)
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
async function deleteInstance(name = RAPID_LIMA_INSTANCE, options = {}) {
|
|
1220
|
+
try {
|
|
1221
|
+
const args = ["delete"];
|
|
1222
|
+
if (options.force) {
|
|
1223
|
+
args.push("--force");
|
|
1224
|
+
}
|
|
1225
|
+
args.push(name);
|
|
1226
|
+
await execa2("limactl", args);
|
|
1227
|
+
return { success: true };
|
|
1228
|
+
} catch (err) {
|
|
1229
|
+
const error = err;
|
|
1230
|
+
return {
|
|
1231
|
+
success: false,
|
|
1232
|
+
error: getErrorMessage2(error)
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
async function execInLima(command, options = {}) {
|
|
1237
|
+
const name = options.name ?? RAPID_LIMA_INSTANCE;
|
|
1238
|
+
try {
|
|
1239
|
+
let fullCommand = command.join(" ");
|
|
1240
|
+
if (options.cwd) {
|
|
1241
|
+
fullCommand = `cd "${options.cwd}" && ${fullCommand}`;
|
|
1242
|
+
}
|
|
1243
|
+
if (options.env) {
|
|
1244
|
+
const exports = Object.entries(options.env).map(([key, value]) => `export ${key}="${value}"`).join(" && ");
|
|
1245
|
+
fullCommand = `${exports} && ${fullCommand}`;
|
|
1246
|
+
}
|
|
1247
|
+
const useInheritStdio = options.interactive || options.tty;
|
|
1248
|
+
const result = await execa2(
|
|
1249
|
+
"limactl",
|
|
1250
|
+
["shell", name, "--", "bash", "-c", fullCommand],
|
|
1251
|
+
useInheritStdio ? { stdio: "inherit" } : {}
|
|
1252
|
+
);
|
|
1253
|
+
const successResult = {
|
|
1254
|
+
success: true
|
|
1255
|
+
};
|
|
1256
|
+
const stdout = getOutputString(result.stdout);
|
|
1257
|
+
const stderr = getOutputString(result.stderr);
|
|
1258
|
+
if (stdout) successResult.stdout = stdout;
|
|
1259
|
+
if (stderr) successResult.stderr = stderr;
|
|
1260
|
+
return successResult;
|
|
1261
|
+
} catch (err) {
|
|
1262
|
+
const error = err;
|
|
1263
|
+
const errorResult = {
|
|
1264
|
+
success: false,
|
|
1265
|
+
error: error.message
|
|
1266
|
+
};
|
|
1267
|
+
const stdout = getOutputString(error.stdout);
|
|
1268
|
+
const stderr = getOutputString(error.stderr);
|
|
1269
|
+
if (stdout) errorResult.stdout = stdout;
|
|
1270
|
+
if (stderr) errorResult.stderr = stderr;
|
|
1271
|
+
return errorResult;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
async function shellInLima(options = {}) {
|
|
1275
|
+
const name = options.name ?? RAPID_LIMA_INSTANCE;
|
|
1276
|
+
const args = ["shell", name];
|
|
1277
|
+
if (options.cwd) {
|
|
1278
|
+
args.push("--workdir", options.cwd);
|
|
1279
|
+
}
|
|
1280
|
+
if (options.command) {
|
|
1281
|
+
args.push("--", options.command);
|
|
1282
|
+
}
|
|
1283
|
+
await execa2("limactl", args, { stdio: "inherit" });
|
|
1284
|
+
}
|
|
1285
|
+
async function setupGitSsh(name = RAPID_LIMA_INSTANCE) {
|
|
1286
|
+
try {
|
|
1287
|
+
const result = await execInLima(["ssh-add", "-l"], { name });
|
|
1288
|
+
if (!result.success) {
|
|
1289
|
+
return {
|
|
1290
|
+
success: false,
|
|
1291
|
+
error: "SSH agent forwarding is not working. Make sure ssh-agent is running on the host."
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
return { success: true };
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
return {
|
|
1297
|
+
success: false,
|
|
1298
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// src/commands/dev.ts
|
|
1304
|
+
var devCommand = new Command2("dev").description("Launch AI coding session in the dev container").option("-a, --agent <name>", "Agent to use").option(
|
|
1305
|
+
"--multi [agents]",
|
|
1306
|
+
"Launch multiple agents (comma-separated, or interactive if no value)"
|
|
1307
|
+
).option("--list", "List available agents without launching").option("--local", "Run locally instead of in container (not recommended)").option("--no-start", "Do not auto-start container if stopped").option("--no-worktree", "Skip automatic worktree creation for feature branches").action(async (options) => {
|
|
1308
|
+
try {
|
|
1309
|
+
const spinner = ora2("Loading configuration...").start();
|
|
1310
|
+
const loaded = await loadConfig();
|
|
1311
|
+
if (!loaded) {
|
|
1312
|
+
spinner.fail("No rapid.json found. Run `rapid init` first.");
|
|
1313
|
+
process.exit(1);
|
|
1314
|
+
}
|
|
1315
|
+
const { config } = loaded;
|
|
1316
|
+
let { rootDir } = loaded;
|
|
1317
|
+
spinner.succeed("Configuration loaded");
|
|
1318
|
+
if (options.list) {
|
|
1319
|
+
listAgents(config);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
if (options.worktree !== false && await isGitRepo(rootDir)) {
|
|
1323
|
+
const branch = await getCurrentBranch(rootDir);
|
|
1324
|
+
if (!branch.isDefault && !branch.detached && branch.name) {
|
|
1325
|
+
spinner.start(`Checking worktree for branch: ${branch.name}...`);
|
|
1326
|
+
try {
|
|
1327
|
+
const worktree = await getOrCreateWorktreeForBranch(rootDir);
|
|
1328
|
+
if (worktree.created) {
|
|
1329
|
+
spinner.succeed(`Created worktree: ${worktree.path}`);
|
|
1330
|
+
rootDir = worktree.path;
|
|
1331
|
+
} else if (!worktree.isMain) {
|
|
1332
|
+
spinner.succeed(`Using worktree: ${worktree.path}`);
|
|
1333
|
+
rootDir = worktree.path;
|
|
1334
|
+
} else {
|
|
1335
|
+
spinner.info(`Using main directory (branch: ${branch.name})`);
|
|
1336
|
+
}
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
spinner.warn("Could not create worktree, using main directory");
|
|
1339
|
+
logger2.debug(err instanceof Error ? err.message : String(err));
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (options.multi !== void 0) {
|
|
1344
|
+
await runMultiAgent(config, rootDir, options);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
const agentName = options.agent || config.agents.default;
|
|
1348
|
+
const agent = getAgent(config, agentName);
|
|
1349
|
+
if (!agent) {
|
|
1350
|
+
logger2.error(`Agent "${agentName}" not found in configuration`);
|
|
1351
|
+
logger2.info("Available agents:");
|
|
1352
|
+
Object.keys(config.agents.available).forEach((name) => {
|
|
1353
|
+
const isDefault = name === config.agents.default;
|
|
1354
|
+
console.log(` ${isDefault ? "* " : " "}${name}${isDefault ? " (default)" : ""}`);
|
|
1355
|
+
});
|
|
1356
|
+
process.exit(1);
|
|
1357
|
+
}
|
|
1358
|
+
if (options.local) {
|
|
1359
|
+
logger2.warn("Running locally instead of in container");
|
|
1360
|
+
logger2.dim("This bypasses the isolated dev environment");
|
|
1361
|
+
logger2.blank();
|
|
1362
|
+
await runLocally(agent, agentName, rootDir, config);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const hasDevCli = await hasDevcontainerCli();
|
|
1366
|
+
if (!hasDevCli) {
|
|
1367
|
+
logger2.error("devcontainer CLI not found");
|
|
1368
|
+
logger2.info("Install with: npm install -g @devcontainers/cli");
|
|
1369
|
+
logger2.blank();
|
|
1370
|
+
logger2.info("Or use --local to run without container (not recommended)");
|
|
1371
|
+
process.exit(1);
|
|
1372
|
+
}
|
|
1373
|
+
spinner.start("Checking container status...");
|
|
1374
|
+
const status = await getContainerStatus(rootDir, config);
|
|
1375
|
+
if (!status.running) {
|
|
1376
|
+
if (options.start === false) {
|
|
1377
|
+
spinner.fail("Container not running. Use `rapid start` first.");
|
|
1378
|
+
process.exit(1);
|
|
1379
|
+
}
|
|
1380
|
+
spinner.text = "Starting container...";
|
|
1381
|
+
spinner.stopAndPersist({ symbol: "\u{1F433}", text: "Starting container..." });
|
|
1382
|
+
const result = await startContainer(rootDir, config, { quiet: false });
|
|
1383
|
+
if (!result.success) {
|
|
1384
|
+
logger2.blank();
|
|
1385
|
+
logger2.error("Failed to start container");
|
|
1386
|
+
logger2.error(result.error || "Unknown error");
|
|
1387
|
+
process.exit(1);
|
|
1388
|
+
}
|
|
1389
|
+
logger2.blank();
|
|
1390
|
+
} else {
|
|
1391
|
+
spinner.succeed(`Container running (${status.containerName})`);
|
|
1392
|
+
}
|
|
1393
|
+
let secrets = {};
|
|
1394
|
+
const secretsConfig = config.secrets;
|
|
1395
|
+
if (secretsConfig?.items && Object.keys(secretsConfig.items).length > 0) {
|
|
1396
|
+
const provider = secretsConfig.provider || "env";
|
|
1397
|
+
if (provider === "1password") {
|
|
1398
|
+
spinner.start("Loading secrets from 1Password...");
|
|
1399
|
+
const hasOp = await hasOpCli();
|
|
1400
|
+
if (!hasOp) {
|
|
1401
|
+
spinner.warn("1Password CLI not found - secrets will not be loaded");
|
|
1402
|
+
logger2.info("Install with: brew install 1password-cli");
|
|
1403
|
+
} else {
|
|
1404
|
+
const authenticated = await isOpAuthenticated();
|
|
1405
|
+
if (!authenticated) {
|
|
1406
|
+
spinner.warn("1Password not authenticated - secrets will not be loaded");
|
|
1407
|
+
logger2.info("Run: eval $(op signin)");
|
|
1408
|
+
} else {
|
|
1409
|
+
try {
|
|
1410
|
+
secrets = await loadSecrets(secretsConfig);
|
|
1411
|
+
const count = Object.keys(secrets).length;
|
|
1412
|
+
spinner.succeed(`Loaded ${count} secret${count !== 1 ? "s" : ""} from 1Password`);
|
|
1413
|
+
} catch (err) {
|
|
1414
|
+
spinner.warn("Failed to load secrets from 1Password");
|
|
1415
|
+
logger2.debug(err instanceof Error ? err.message : String(err));
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
} else if (provider === "vault") {
|
|
1420
|
+
spinner.start("Loading secrets from Vault...");
|
|
1421
|
+
const hasVault = await hasVaultCli();
|
|
1422
|
+
if (!hasVault) {
|
|
1423
|
+
spinner.warn("Vault CLI not found - secrets will not be loaded");
|
|
1424
|
+
logger2.info("Install from: https://developer.hashicorp.com/vault/docs/install");
|
|
1425
|
+
} else {
|
|
1426
|
+
const authenticated = await isVaultAuthenticated();
|
|
1427
|
+
if (!authenticated) {
|
|
1428
|
+
spinner.warn("Vault not authenticated - secrets will not be loaded");
|
|
1429
|
+
logger2.info("Run: vault login");
|
|
1430
|
+
} else {
|
|
1431
|
+
try {
|
|
1432
|
+
secrets = await loadSecrets(secretsConfig);
|
|
1433
|
+
const count = Object.keys(secrets).length;
|
|
1434
|
+
spinner.succeed(`Loaded ${count} secret${count !== 1 ? "s" : ""} from Vault`);
|
|
1435
|
+
} catch (err) {
|
|
1436
|
+
spinner.warn("Failed to load secrets from Vault");
|
|
1437
|
+
logger2.debug(err instanceof Error ? err.message : String(err));
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
} else if (provider === "env") {
|
|
1442
|
+
spinner.start("Loading secrets from environment...");
|
|
1443
|
+
try {
|
|
1444
|
+
secrets = await loadSecrets(secretsConfig);
|
|
1445
|
+
const count = Object.keys(secrets).length;
|
|
1446
|
+
if (count > 0) {
|
|
1447
|
+
spinner.succeed(`Loaded ${count} secret${count !== 1 ? "s" : ""} from environment`);
|
|
1448
|
+
} else {
|
|
1449
|
+
spinner.warn("No secrets found in environment");
|
|
1450
|
+
}
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
spinner.warn("Failed to load secrets from environment");
|
|
1453
|
+
logger2.debug(err instanceof Error ? err.message : String(err));
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
logger2.blank();
|
|
1458
|
+
logger2.info(`Launching ${logger2.brand(agentName)} in container...`);
|
|
1459
|
+
const builtArgs = buildAgentArgs(agent, { injectSystemPrompt: true });
|
|
1460
|
+
if (agentSupportsRuntimeInjection(agent)) {
|
|
1461
|
+
logger2.dim("Injecting RAPID methodology via CLI args");
|
|
1462
|
+
}
|
|
1463
|
+
logger2.blank();
|
|
1464
|
+
const agentArgs = [agent.cli, ...builtArgs];
|
|
1465
|
+
const mcpEnv = await prepareMcpEnv(rootDir, config.mcp);
|
|
1466
|
+
const mergedEnv = { ...secrets, ...mcpEnv ?? {} };
|
|
1467
|
+
await execInContainer(rootDir, agentArgs, config, {
|
|
1468
|
+
interactive: true,
|
|
1469
|
+
tty: true,
|
|
1470
|
+
env: mergedEnv
|
|
1471
|
+
});
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
logger2.error(error instanceof Error ? error.message : String(error));
|
|
1474
|
+
process.exit(1);
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
async function prepareMcpEnv(rootDir, mcp) {
|
|
1478
|
+
if (!mcp?.servers || Object.keys(mcp.servers).length === 0) {
|
|
1479
|
+
return void 0;
|
|
1480
|
+
}
|
|
1481
|
+
const configFile = mcp.configFile ?? ".mcp.json";
|
|
1482
|
+
const configPath = isAbsolute(configFile) ? configFile : join4(rootDir, configFile);
|
|
1483
|
+
const servers = {};
|
|
1484
|
+
for (const [name, serverConfig] of Object.entries(mcp.servers)) {
|
|
1485
|
+
if (!serverConfig || typeof serverConfig !== "object") {
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
const { enabled, ...rest } = serverConfig;
|
|
1489
|
+
if (enabled === false) {
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
servers[name] = rest;
|
|
1493
|
+
}
|
|
1494
|
+
if (Object.keys(servers).length === 0) {
|
|
1495
|
+
return void 0;
|
|
1496
|
+
}
|
|
1497
|
+
await writeFile3(configPath, `${JSON.stringify({ servers }, null, 2)}
|
|
1498
|
+
`, "utf-8");
|
|
1499
|
+
return {
|
|
1500
|
+
MCP_CONFIG_FILE: configFile
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
async function runLocally(agent, agentName, rootDir, config) {
|
|
1504
|
+
const { execa: execa4 } = await import("execa");
|
|
1505
|
+
const status = await checkAgentAvailable(agent);
|
|
1506
|
+
if (!status.available) {
|
|
1507
|
+
logger2.error(`${agentName} CLI not found locally`);
|
|
1508
|
+
process.exit(1);
|
|
1509
|
+
}
|
|
1510
|
+
let secrets = {};
|
|
1511
|
+
const secretsConfig = config.secrets;
|
|
1512
|
+
if (secretsConfig?.items && Object.keys(secretsConfig.items).length > 0) {
|
|
1513
|
+
const provider = secretsConfig.provider || "env";
|
|
1514
|
+
const spinner = ora2();
|
|
1515
|
+
if (provider === "1password") {
|
|
1516
|
+
spinner.start("Loading secrets from 1Password...");
|
|
1517
|
+
const hasOp = await hasOpCli();
|
|
1518
|
+
if (!hasOp) {
|
|
1519
|
+
spinner.warn("1Password CLI not found - secrets will not be loaded");
|
|
1520
|
+
logger2.info("Install with: brew install 1password-cli");
|
|
1521
|
+
} else {
|
|
1522
|
+
const authenticated = await isOpAuthenticated();
|
|
1523
|
+
if (!authenticated) {
|
|
1524
|
+
spinner.warn("1Password not authenticated - secrets will not be loaded");
|
|
1525
|
+
logger2.info("Run: eval $(op signin)");
|
|
1526
|
+
} else {
|
|
1527
|
+
try {
|
|
1528
|
+
secrets = await loadSecrets(secretsConfig);
|
|
1529
|
+
const count = Object.keys(secrets).length;
|
|
1530
|
+
spinner.succeed(`Loaded ${count} secret${count !== 1 ? "s" : ""} from 1Password`);
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
spinner.warn("Failed to load secrets from 1Password");
|
|
1533
|
+
logger2.debug(err instanceof Error ? err.message : String(err));
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
} else if (provider === "vault") {
|
|
1538
|
+
spinner.start("Loading secrets from Vault...");
|
|
1539
|
+
const hasVault = await hasVaultCli();
|
|
1540
|
+
if (!hasVault) {
|
|
1541
|
+
spinner.warn("Vault CLI not found - secrets will not be loaded");
|
|
1542
|
+
logger2.info("Install from: https://developer.hashicorp.com/vault/docs/install");
|
|
1543
|
+
} else {
|
|
1544
|
+
const authenticated = await isVaultAuthenticated();
|
|
1545
|
+
if (!authenticated) {
|
|
1546
|
+
spinner.warn("Vault not authenticated - secrets will not be loaded");
|
|
1547
|
+
logger2.info("Run: vault login");
|
|
1548
|
+
} else {
|
|
1549
|
+
try {
|
|
1550
|
+
secrets = await loadSecrets(secretsConfig);
|
|
1551
|
+
const count = Object.keys(secrets).length;
|
|
1552
|
+
spinner.succeed(`Loaded ${count} secret${count !== 1 ? "s" : ""} from Vault`);
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
spinner.warn("Failed to load secrets from Vault");
|
|
1555
|
+
logger2.debug(err instanceof Error ? err.message : String(err));
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
} else if (provider === "env") {
|
|
1560
|
+
spinner.start("Loading secrets from environment...");
|
|
1561
|
+
try {
|
|
1562
|
+
secrets = await loadSecrets(secretsConfig);
|
|
1563
|
+
const count = Object.keys(secrets).length;
|
|
1564
|
+
if (count > 0) {
|
|
1565
|
+
spinner.succeed(`Loaded ${count} secret${count !== 1 ? "s" : ""} from environment`);
|
|
1566
|
+
} else {
|
|
1567
|
+
spinner.warn("No secrets found in environment");
|
|
1568
|
+
}
|
|
1569
|
+
} catch (err) {
|
|
1570
|
+
spinner.warn("Failed to load secrets from environment");
|
|
1571
|
+
logger2.debug(err instanceof Error ? err.message : String(err));
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
const mcpEnv = await prepareMcpEnv(rootDir, config.mcp);
|
|
1576
|
+
const mergedEnv = { ...secrets, ...mcpEnv ?? {} };
|
|
1577
|
+
const builtArgs = buildAgentArgs(agent, { injectSystemPrompt: true });
|
|
1578
|
+
if (isMacOS() && await hasLima()) {
|
|
1579
|
+
await runInLimaVm(agent, agentName, rootDir, builtArgs, mergedEnv);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
logger2.info(`Launching ${logger2.brand(agentName)}...`);
|
|
1583
|
+
logger2.dim(`Working directory: ${rootDir}`);
|
|
1584
|
+
if (agentSupportsRuntimeInjection(agent)) {
|
|
1585
|
+
logger2.dim("Injecting RAPID methodology via CLI args");
|
|
1586
|
+
}
|
|
1587
|
+
logger2.blank();
|
|
1588
|
+
await execa4(agent.cli, builtArgs, {
|
|
1589
|
+
cwd: rootDir,
|
|
1590
|
+
stdio: "inherit",
|
|
1591
|
+
env: {
|
|
1592
|
+
...process.env,
|
|
1593
|
+
...mergedEnv
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
async function runInLimaVm(agent, agentName, rootDir, args, env) {
|
|
1598
|
+
const spinner = ora2();
|
|
1599
|
+
if (!await isRunning()) {
|
|
1600
|
+
spinner.start(`Starting Lima VM (${RAPID_LIMA_INSTANCE})...`);
|
|
1601
|
+
const result = await startInstance(rootDir, {
|
|
1602
|
+
env,
|
|
1603
|
+
timeout: 600
|
|
1604
|
+
// 10 minutes for first-time setup
|
|
1605
|
+
});
|
|
1606
|
+
if (!result.success) {
|
|
1607
|
+
spinner.fail("Failed to start Lima VM");
|
|
1608
|
+
logger2.error(result.error ?? "Unknown error");
|
|
1609
|
+
logger2.blank();
|
|
1610
|
+
logger2.info("Falling back to running directly on host...");
|
|
1611
|
+
logger2.blank();
|
|
1612
|
+
const { execa: execa4 } = await import("execa");
|
|
1613
|
+
await execa4(agent.cli, args, {
|
|
1614
|
+
cwd: rootDir,
|
|
1615
|
+
stdio: "inherit",
|
|
1616
|
+
env: {
|
|
1617
|
+
...process.env,
|
|
1618
|
+
...env
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
spinner.succeed("Lima VM started");
|
|
1624
|
+
} else {
|
|
1625
|
+
logger2.info(`Lima VM (${RAPID_LIMA_INSTANCE}) is running`);
|
|
1626
|
+
}
|
|
1627
|
+
logger2.info(`Launching ${logger2.brand(agentName)} in Lima VM...`);
|
|
1628
|
+
logger2.dim(`Working directory: ${rootDir}`);
|
|
1629
|
+
logger2.dim("SSH agent forwarded for commit signing");
|
|
1630
|
+
logger2.blank();
|
|
1631
|
+
await execInLima([agent.cli, ...args], {
|
|
1632
|
+
cwd: rootDir,
|
|
1633
|
+
env,
|
|
1634
|
+
interactive: true,
|
|
1635
|
+
tty: true
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
function listAgents(config) {
|
|
1639
|
+
logger2.header("Available Agents");
|
|
1640
|
+
Object.keys(config.agents.available).forEach((name) => {
|
|
1641
|
+
const isDefault = name === config.agents.default;
|
|
1642
|
+
console.log(
|
|
1643
|
+
` ${isDefault ? logger2.brand("*") : " "} ${name}${isDefault ? logger2.dim(" (default)") : ""}`
|
|
1644
|
+
);
|
|
1645
|
+
});
|
|
1646
|
+
logger2.blank();
|
|
1647
|
+
logger2.dim("Use --agent <name> to select a specific agent");
|
|
1648
|
+
}
|
|
1649
|
+
async function runMultiAgent(config, rootDir, options) {
|
|
1650
|
+
const availableAgents = Object.keys(config.agents.available);
|
|
1651
|
+
if (availableAgents.length === 0) {
|
|
1652
|
+
logger2.error("No agents configured");
|
|
1653
|
+
process.exit(1);
|
|
1654
|
+
}
|
|
1655
|
+
let selectedAgents;
|
|
1656
|
+
if (typeof options.multi === "string") {
|
|
1657
|
+
selectedAgents = options.multi.split(",").map((a) => a.trim()).filter(Boolean);
|
|
1658
|
+
for (const name of selectedAgents) {
|
|
1659
|
+
if (!config.agents.available[name]) {
|
|
1660
|
+
logger2.error(`Agent "${name}" not found in configuration`);
|
|
1661
|
+
logger2.info("Available agents: " + availableAgents.join(", "));
|
|
1662
|
+
process.exit(1);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
} else {
|
|
1666
|
+
logger2.header("Multi-Agent Mode");
|
|
1667
|
+
console.log();
|
|
1668
|
+
console.log(" Available agents:");
|
|
1669
|
+
for (const name of availableAgents) {
|
|
1670
|
+
const isDefault = name === config.agents.default;
|
|
1671
|
+
console.log(` ${logger2.brand("\u2022")} ${name}${isDefault ? logger2.dim(" (default)") : ""}`);
|
|
1672
|
+
}
|
|
1673
|
+
console.log();
|
|
1674
|
+
logger2.info("To run multiple agents, specify them with:");
|
|
1675
|
+
console.log(` ${logger2.brand("rapid dev --multi claude,aider")}`);
|
|
1676
|
+
console.log();
|
|
1677
|
+
logger2.info("Or run agents in separate terminals:");
|
|
1678
|
+
for (const name of availableAgents) {
|
|
1679
|
+
console.log(` ${logger2.dim("$")} rapid dev --agent ${name}`);
|
|
1680
|
+
}
|
|
1681
|
+
console.log();
|
|
1682
|
+
logger2.warn("Note: Running multiple agents simultaneously requires separate terminal windows.");
|
|
1683
|
+
logger2.info("Each agent maintains its own session and context.");
|
|
1684
|
+
console.log();
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
if (selectedAgents.length === 0) {
|
|
1688
|
+
logger2.error("No agents specified");
|
|
1689
|
+
process.exit(1);
|
|
1690
|
+
}
|
|
1691
|
+
if (selectedAgents.length === 1) {
|
|
1692
|
+
logger2.info(
|
|
1693
|
+
`Only one agent specified. Use ${logger2.brand("rapid dev --agent " + selectedAgents[0])} instead.`
|
|
1694
|
+
);
|
|
1695
|
+
process.exit(0);
|
|
1696
|
+
}
|
|
1697
|
+
logger2.header("Multi-Agent Session");
|
|
1698
|
+
console.log();
|
|
1699
|
+
console.log(" Selected agents:");
|
|
1700
|
+
for (const name of selectedAgents) {
|
|
1701
|
+
console.log(` ${logger2.brand("\u2022")} ${name}`);
|
|
1702
|
+
}
|
|
1703
|
+
console.log();
|
|
1704
|
+
const { execa: execa4 } = await import("execa");
|
|
1705
|
+
let hasTmux = false;
|
|
1706
|
+
try {
|
|
1707
|
+
await execa4("tmux", ["-V"]);
|
|
1708
|
+
hasTmux = true;
|
|
1709
|
+
} catch {
|
|
1710
|
+
hasTmux = false;
|
|
1711
|
+
}
|
|
1712
|
+
if (hasTmux) {
|
|
1713
|
+
logger2.info("Launching agents in tmux panes...");
|
|
1714
|
+
console.log();
|
|
1715
|
+
const sessionName = `rapid-${Date.now()}`;
|
|
1716
|
+
const firstAgent = selectedAgents[0];
|
|
1717
|
+
const firstCmd = options.local ? `rapid dev --agent ${firstAgent} --local` : `rapid dev --agent ${firstAgent}`;
|
|
1718
|
+
await execa4("tmux", ["new-session", "-d", "-s", sessionName, "-n", "rapid", firstCmd], {
|
|
1719
|
+
cwd: rootDir
|
|
1720
|
+
});
|
|
1721
|
+
for (let i = 1; i < selectedAgents.length; i++) {
|
|
1722
|
+
const agentName = selectedAgents[i];
|
|
1723
|
+
const cmd = options.local ? `rapid dev --agent ${agentName} --local` : `rapid dev --agent ${agentName}`;
|
|
1724
|
+
await execa4("tmux", ["split-window", "-t", sessionName, "-h", cmd], {
|
|
1725
|
+
cwd: rootDir
|
|
1726
|
+
});
|
|
1727
|
+
await execa4("tmux", ["select-layout", "-t", sessionName, "tiled"]);
|
|
1728
|
+
}
|
|
1729
|
+
logger2.success(`Started ${selectedAgents.length} agents in tmux session: ${sessionName}`);
|
|
1730
|
+
console.log();
|
|
1731
|
+
logger2.info("Attaching to tmux session...");
|
|
1732
|
+
logger2.dim("Press Ctrl+B then D to detach, or Ctrl+B then arrow keys to switch panes");
|
|
1733
|
+
console.log();
|
|
1734
|
+
await execa4("tmux", ["attach-session", "-t", sessionName], {
|
|
1735
|
+
stdio: "inherit"
|
|
1736
|
+
});
|
|
1737
|
+
} else {
|
|
1738
|
+
logger2.warn("tmux not found. Multi-agent mode works best with tmux installed.");
|
|
1739
|
+
console.log();
|
|
1740
|
+
logger2.info("To run multiple agents, open separate terminal windows and run:");
|
|
1741
|
+
console.log();
|
|
1742
|
+
for (const name of selectedAgents) {
|
|
1743
|
+
const cmd = options.local ? `--local` : "";
|
|
1744
|
+
console.log(
|
|
1745
|
+
` ${logger2.dim("Terminal " + (selectedAgents.indexOf(name) + 1) + ":")} rapid dev --agent ${name} ${cmd}`.trim()
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
console.log();
|
|
1749
|
+
logger2.info("Install tmux for integrated multi-pane support:");
|
|
1750
|
+
console.log(` ${logger2.dim("macOS:")} brew install tmux`);
|
|
1751
|
+
console.log(` ${logger2.dim("Ubuntu:")} sudo apt install tmux`);
|
|
1752
|
+
console.log();
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// src/commands/status.ts
|
|
1757
|
+
import { Command as Command3 } from "commander";
|
|
1758
|
+
import {
|
|
1759
|
+
loadConfig as loadConfig2,
|
|
1760
|
+
checkAllAgents,
|
|
1761
|
+
logger as logger3,
|
|
1762
|
+
getContainerStatus as getContainerStatus2,
|
|
1763
|
+
hasDevcontainerCli as hasDevcontainerCli2,
|
|
1764
|
+
hasDocker,
|
|
1765
|
+
loadDevcontainerConfig,
|
|
1766
|
+
verifySecrets,
|
|
1767
|
+
hasOpCli as hasOpCli2,
|
|
1768
|
+
hasVaultCli as hasVaultCli2,
|
|
1769
|
+
isOpAuthenticated as isOpAuthenticated2,
|
|
1770
|
+
isVaultAuthenticated as isVaultAuthenticated2,
|
|
1771
|
+
hasEnvrc,
|
|
1772
|
+
getProviderInfo,
|
|
1773
|
+
getAuthStatus
|
|
1774
|
+
} from "@a3t/rapid-core";
|
|
1775
|
+
import ora3 from "ora";
|
|
1776
|
+
var statusCommand = new Command3("status").description("Show environment status").option("--json", "Output as JSON").action(async (options) => {
|
|
1777
|
+
try {
|
|
1778
|
+
const spinner = ora3("Checking status...").start();
|
|
1779
|
+
const loaded = await loadConfig2();
|
|
1780
|
+
if (!loaded) {
|
|
1781
|
+
spinner.fail("No rapid.json found");
|
|
1782
|
+
if (options.json) {
|
|
1783
|
+
console.log(JSON.stringify({ configured: false }, null, 2));
|
|
1784
|
+
}
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
const { config, filepath, rootDir } = loaded;
|
|
1788
|
+
spinner.text = "Checking container...";
|
|
1789
|
+
const containerStatus = await getContainerStatus2(rootDir, config);
|
|
1790
|
+
const devcontainerConfig = await loadDevcontainerConfig(rootDir, config);
|
|
1791
|
+
const hasDevCli = await hasDevcontainerCli2();
|
|
1792
|
+
const dockerRunning = await hasDocker();
|
|
1793
|
+
spinner.text = "Checking agents...";
|
|
1794
|
+
const agentStatuses = await checkAllAgents(config);
|
|
1795
|
+
spinner.text = "Checking secrets...";
|
|
1796
|
+
const secretsConfig = config.secrets;
|
|
1797
|
+
const provider = secretsConfig?.provider || "env";
|
|
1798
|
+
let secretsStatus = null;
|
|
1799
|
+
if (secretsConfig) {
|
|
1800
|
+
let cliInstalled = true;
|
|
1801
|
+
let authenticated = true;
|
|
1802
|
+
if (provider === "1password") {
|
|
1803
|
+
cliInstalled = await hasOpCli2();
|
|
1804
|
+
authenticated = cliInstalled && await isOpAuthenticated2();
|
|
1805
|
+
} else if (provider === "vault") {
|
|
1806
|
+
cliInstalled = await hasVaultCli2();
|
|
1807
|
+
authenticated = cliInstalled && await isVaultAuthenticated2();
|
|
1808
|
+
}
|
|
1809
|
+
const envrcExists = await hasEnvrc(rootDir, secretsConfig);
|
|
1810
|
+
const verified = await verifySecrets(secretsConfig);
|
|
1811
|
+
secretsStatus = {
|
|
1812
|
+
provider,
|
|
1813
|
+
cliInstalled,
|
|
1814
|
+
authenticated,
|
|
1815
|
+
envrcExists,
|
|
1816
|
+
secretsCount: secretsConfig.items ? Object.keys(secretsConfig.items).length : 0,
|
|
1817
|
+
allAvailable: verified.allAvailable
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
spinner.text = "Checking authentication...";
|
|
1821
|
+
const authStatus = await getAuthStatus();
|
|
1822
|
+
spinner.stop();
|
|
1823
|
+
if (options.json) {
|
|
1824
|
+
console.log(
|
|
1825
|
+
JSON.stringify(
|
|
1826
|
+
{
|
|
1827
|
+
configured: true,
|
|
1828
|
+
configPath: filepath,
|
|
1829
|
+
rootDir,
|
|
1830
|
+
defaultAgent: config.agents.default,
|
|
1831
|
+
container: {
|
|
1832
|
+
configured: !!devcontainerConfig,
|
|
1833
|
+
running: containerStatus.running,
|
|
1834
|
+
name: containerStatus.containerName,
|
|
1835
|
+
devcontainerCli: hasDevCli,
|
|
1836
|
+
docker: dockerRunning
|
|
1837
|
+
},
|
|
1838
|
+
agents: agentStatuses,
|
|
1839
|
+
secrets: secretsStatus,
|
|
1840
|
+
auth: {
|
|
1841
|
+
authenticated: authStatus.authenticated,
|
|
1842
|
+
sources: authStatus.sources.map((s) => ({
|
|
1843
|
+
source: s.source,
|
|
1844
|
+
provider: s.provider,
|
|
1845
|
+
authType: s.authType,
|
|
1846
|
+
hasValue: !!s.value
|
|
1847
|
+
}))
|
|
1848
|
+
}
|
|
1849
|
+
},
|
|
1850
|
+
null,
|
|
1851
|
+
2
|
|
1852
|
+
)
|
|
1853
|
+
);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
console.log();
|
|
1857
|
+
console.log(` ${logger3.brand("RAPID")} Status`);
|
|
1858
|
+
console.log(` ${logger3.dim("\u2500".repeat(24))}`);
|
|
1859
|
+
console.log();
|
|
1860
|
+
console.log(` ${logger3.dim("Config:")} ${filepath}`);
|
|
1861
|
+
console.log(` ${logger3.dim("Root:")} ${rootDir}`);
|
|
1862
|
+
console.log();
|
|
1863
|
+
console.log(` ${logger3.dim("Container:")}`);
|
|
1864
|
+
if (!devcontainerConfig) {
|
|
1865
|
+
console.log(` ${logger3.dim("\u25CB")} ${logger3.dim("No devcontainer.json configured")}`);
|
|
1866
|
+
} else if (!dockerRunning) {
|
|
1867
|
+
console.log(` ${logger3.dim("\u25CB")} ${logger3.dim("Docker not running")}`);
|
|
1868
|
+
} else if (!hasDevCli) {
|
|
1869
|
+
console.log(` ${logger3.dim("\u25CB")} ${logger3.dim("devcontainer CLI not installed")}`);
|
|
1870
|
+
} else if (containerStatus.running) {
|
|
1871
|
+
console.log(
|
|
1872
|
+
` ${logger3.brand("\u25CF")} Running ${logger3.dim(`(${containerStatus.containerName})`)}`
|
|
1873
|
+
);
|
|
1874
|
+
} else if (containerStatus.exists) {
|
|
1875
|
+
console.log(
|
|
1876
|
+
` ${logger3.dim("\u25CB")} Stopped ${logger3.dim(`(${containerStatus.containerName})`)}`
|
|
1877
|
+
);
|
|
1878
|
+
} else {
|
|
1879
|
+
console.log(` ${logger3.dim("\u25CB")} Not started`);
|
|
1880
|
+
}
|
|
1881
|
+
console.log();
|
|
1882
|
+
console.log(
|
|
1883
|
+
` ${logger3.dim("Agents:")} ${logger3.dim(`(default: ${config.agents.default})`)}`
|
|
1884
|
+
);
|
|
1885
|
+
agentStatuses.forEach((status) => {
|
|
1886
|
+
const isDefault = status.name === config.agents.default;
|
|
1887
|
+
const icon = status.available ? logger3.brand("\u2713") : logger3.dim("\u25CB");
|
|
1888
|
+
const name = isDefault ? logger3.bold(status.name) : status.name;
|
|
1889
|
+
const version = status.version ? logger3.dim(` (${status.version})`) : "";
|
|
1890
|
+
console.log(` ${icon} ${name}${version}`);
|
|
1891
|
+
});
|
|
1892
|
+
console.log();
|
|
1893
|
+
if (secretsStatus) {
|
|
1894
|
+
const providerInfo = getProviderInfo(
|
|
1895
|
+
secretsStatus.provider
|
|
1896
|
+
);
|
|
1897
|
+
console.log(` ${logger3.dim("Secrets:")} ${logger3.dim(`(${providerInfo.name})`)}`);
|
|
1898
|
+
if (providerInfo.cliRequired) {
|
|
1899
|
+
const cliIcon = secretsStatus.cliInstalled ? logger3.brand("\u2713") : logger3.dim("\u25CB");
|
|
1900
|
+
console.log(
|
|
1901
|
+
` ${cliIcon} CLI ${secretsStatus.cliInstalled ? "installed" : "not installed"}`
|
|
1902
|
+
);
|
|
1903
|
+
if (secretsStatus.cliInstalled) {
|
|
1904
|
+
const authIcon = secretsStatus.authenticated ? logger3.brand("\u2713") : logger3.dim("\u25CB");
|
|
1905
|
+
console.log(
|
|
1906
|
+
` ${authIcon} ${secretsStatus.authenticated ? "Authenticated" : "Not authenticated"}`
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
if (secretsStatus.secretsCount > 0) {
|
|
1911
|
+
const allIcon = secretsStatus.allAvailable ? logger3.brand("\u2713") : logger3.dim("\u25CB");
|
|
1912
|
+
console.log(
|
|
1913
|
+
` ${allIcon} ${secretsStatus.secretsCount} secret${secretsStatus.secretsCount !== 1 ? "s" : ""} ${secretsStatus.allAvailable ? "available" : "configured"}`
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
const envrcIcon = secretsStatus.envrcExists ? logger3.brand("\u2713") : logger3.dim("\u25CB");
|
|
1917
|
+
console.log(
|
|
1918
|
+
` ${envrcIcon} .envrc ${secretsStatus.envrcExists ? "exists" : "not generated"}`
|
|
1919
|
+
);
|
|
1920
|
+
console.log();
|
|
1921
|
+
}
|
|
1922
|
+
console.log(` ${logger3.dim("Auth:")}`);
|
|
1923
|
+
if (!authStatus.authenticated) {
|
|
1924
|
+
console.log(` ${logger3.dim("\u25CB")} ${logger3.dim("No authentication detected")}`);
|
|
1925
|
+
console.log(` ${logger3.dim(" Run `rapid auth` for options")}`);
|
|
1926
|
+
} else {
|
|
1927
|
+
for (const cred of authStatus.sources) {
|
|
1928
|
+
const icon = cred.authType === "oauth" ? logger3.brand("\u25CF") : logger3.dim("\u25CB");
|
|
1929
|
+
const authType = cred.authType === "oauth" ? "OAuth" : "API Key";
|
|
1930
|
+
let info = `${cred.source} (${cred.provider}, ${authType})`;
|
|
1931
|
+
if (cred.accountInfo?.email) {
|
|
1932
|
+
info += ` - ${cred.accountInfo.email}`;
|
|
1933
|
+
}
|
|
1934
|
+
console.log(` ${icon} ${info}`);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
console.log();
|
|
1938
|
+
if (!containerStatus.running && devcontainerConfig && dockerRunning && hasDevCli) {
|
|
1939
|
+
logger3.info("Run `rapid start` to start the container");
|
|
1940
|
+
} else if (containerStatus.running) {
|
|
1941
|
+
logger3.info("Run `rapid dev` to start coding");
|
|
1942
|
+
}
|
|
1943
|
+
console.log();
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
logger3.error(error instanceof Error ? error.message : String(error));
|
|
1946
|
+
process.exit(1);
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1950
|
+
// src/commands/agent.ts
|
|
1951
|
+
import { Command as Command4 } from "commander";
|
|
1952
|
+
import { loadConfig as loadConfig3, checkAllAgents as checkAllAgents2, logger as logger4 } from "@a3t/rapid-core";
|
|
1953
|
+
var agentCommand = new Command4("agent").description("Manage AI agents");
|
|
1954
|
+
agentCommand.command("list").description("List available agents").action(async () => {
|
|
1955
|
+
try {
|
|
1956
|
+
const loaded = await loadConfig3();
|
|
1957
|
+
if (!loaded) {
|
|
1958
|
+
logger4.error("No rapid.json found. Run `rapid init` first.");
|
|
1959
|
+
process.exit(1);
|
|
1960
|
+
}
|
|
1961
|
+
const { config } = loaded;
|
|
1962
|
+
const statuses = await checkAllAgents2(config);
|
|
1963
|
+
logger4.header("Available Agents");
|
|
1964
|
+
statuses.forEach((status) => {
|
|
1965
|
+
const isDefault = status.name === config.agents.default;
|
|
1966
|
+
const icon = status.available ? "\u2713" : "\u25CB";
|
|
1967
|
+
const defaultTag = isDefault ? " (default)" : "";
|
|
1968
|
+
const versionTag = status.version ? ` - ${status.version}` : "";
|
|
1969
|
+
if (status.available) {
|
|
1970
|
+
console.log(
|
|
1971
|
+
` ${logger4.brand(icon)} ${status.name}${defaultTag}${logger4.dim(versionTag)}`
|
|
1972
|
+
);
|
|
1973
|
+
} else {
|
|
1974
|
+
console.log(
|
|
1975
|
+
` ${logger4.dim(icon)} ${logger4.dim(status.name)}${defaultTag} ${logger4.dim("[not installed]")}`
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
logger4.blank();
|
|
1980
|
+
} catch (error) {
|
|
1981
|
+
logger4.error(error instanceof Error ? error.message : String(error));
|
|
1982
|
+
process.exit(1);
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
agentCommand.command("default [name]").description("Get or set default agent").action(async (name) => {
|
|
1986
|
+
try {
|
|
1987
|
+
const loaded = await loadConfig3();
|
|
1988
|
+
if (!loaded) {
|
|
1989
|
+
logger4.error("No rapid.json found. Run `rapid init` first.");
|
|
1990
|
+
process.exit(1);
|
|
1991
|
+
}
|
|
1992
|
+
const { config } = loaded;
|
|
1993
|
+
if (!name) {
|
|
1994
|
+
console.log(config.agents.default);
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
if (!config.agents.available[name]) {
|
|
1998
|
+
logger4.error(`Agent "${name}" not found in configuration`);
|
|
1999
|
+
logger4.info("Available agents:");
|
|
2000
|
+
Object.keys(config.agents.available).forEach((n) => {
|
|
2001
|
+
console.log(` - ${n}`);
|
|
2002
|
+
});
|
|
2003
|
+
process.exit(1);
|
|
2004
|
+
}
|
|
2005
|
+
logger4.warn("Setting default agent requires editing rapid.json");
|
|
2006
|
+
logger4.info(`Set "agents.default" to "${name}" in your rapid.json`);
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
logger4.error(error instanceof Error ? error.message : String(error));
|
|
2009
|
+
process.exit(1);
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
// src/commands/start.ts
|
|
2014
|
+
import { Command as Command5 } from "commander";
|
|
2015
|
+
import {
|
|
2016
|
+
loadConfig as loadConfig4,
|
|
2017
|
+
logger as logger5,
|
|
2018
|
+
hasDevcontainerCli as hasDevcontainerCli3,
|
|
2019
|
+
hasDocker as hasDocker2,
|
|
2020
|
+
loadDevcontainerConfig as loadDevcontainerConfig2,
|
|
2021
|
+
getContainerStatus as getContainerStatus3,
|
|
2022
|
+
startContainer as startContainer2
|
|
2023
|
+
} from "@a3t/rapid-core";
|
|
2024
|
+
import ora4 from "ora";
|
|
2025
|
+
var startCommand = new Command5("start").description("Start the development container").option("--rebuild", "Force rebuild the container", false).option("--no-cache", "Build without Docker cache", false).action(async (options) => {
|
|
2026
|
+
const spinner = ora4("Starting development environment...").start();
|
|
2027
|
+
try {
|
|
2028
|
+
spinner.text = "Loading configuration...";
|
|
2029
|
+
const loaded = await loadConfig4();
|
|
2030
|
+
if (!loaded) {
|
|
2031
|
+
spinner.fail("No rapid.json found. Run `rapid init` first.");
|
|
2032
|
+
process.exit(1);
|
|
2033
|
+
}
|
|
2034
|
+
const { config, rootDir } = loaded;
|
|
2035
|
+
spinner.text = "Checking devcontainer CLI...";
|
|
2036
|
+
const hasDevCli = await hasDevcontainerCli3();
|
|
2037
|
+
if (!hasDevCli) {
|
|
2038
|
+
spinner.fail("devcontainer CLI not found");
|
|
2039
|
+
logger5.blank();
|
|
2040
|
+
logger5.info("Install with:");
|
|
2041
|
+
console.log(" npm install -g @devcontainers/cli");
|
|
2042
|
+
logger5.blank();
|
|
2043
|
+
process.exit(1);
|
|
2044
|
+
}
|
|
2045
|
+
spinner.text = "Checking Docker...";
|
|
2046
|
+
const dockerAvailable = await hasDocker2();
|
|
2047
|
+
if (!dockerAvailable) {
|
|
2048
|
+
spinner.fail("Docker is not running");
|
|
2049
|
+
logger5.info("Please start Docker Desktop and try again.");
|
|
2050
|
+
process.exit(1);
|
|
2051
|
+
}
|
|
2052
|
+
spinner.text = "Checking devcontainer configuration...";
|
|
2053
|
+
const devcontainerConfig = await loadDevcontainerConfig2(rootDir, config);
|
|
2054
|
+
if (!devcontainerConfig) {
|
|
2055
|
+
spinner.fail("No devcontainer.json found");
|
|
2056
|
+
logger5.blank();
|
|
2057
|
+
logger5.info("Create a .devcontainer/devcontainer.json or run:");
|
|
2058
|
+
console.log(" rapid init --template <template>");
|
|
2059
|
+
logger5.blank();
|
|
2060
|
+
process.exit(1);
|
|
2061
|
+
}
|
|
2062
|
+
spinner.text = "Checking container status...";
|
|
2063
|
+
const status = await getContainerStatus3(rootDir, config);
|
|
2064
|
+
if (status.running && !options.rebuild) {
|
|
2065
|
+
spinner.succeed("Container already running");
|
|
2066
|
+
logger5.info(`Container: ${status.containerName}`);
|
|
2067
|
+
logger5.blank();
|
|
2068
|
+
logger5.info("Run `rapid dev` to start coding");
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
spinner.text = options.rebuild ? "Rebuilding container..." : "Starting container...";
|
|
2072
|
+
spinner.stopAndPersist({ symbol: "\u{1F433}", text: spinner.text });
|
|
2073
|
+
const result = await startContainer2(rootDir, config, {
|
|
2074
|
+
rebuild: options.rebuild,
|
|
2075
|
+
quiet: false
|
|
2076
|
+
});
|
|
2077
|
+
if (!result.success) {
|
|
2078
|
+
logger5.blank();
|
|
2079
|
+
logger5.error("Failed to start container");
|
|
2080
|
+
logger5.error(result.error || "Unknown error");
|
|
2081
|
+
process.exit(1);
|
|
2082
|
+
}
|
|
2083
|
+
logger5.blank();
|
|
2084
|
+
logger5.success("Development environment ready!");
|
|
2085
|
+
logger5.blank();
|
|
2086
|
+
logger5.info("Next steps:");
|
|
2087
|
+
console.log(` ${logger5.dim("\u2022")} Run ${logger5.brand("rapid dev")} to start coding`);
|
|
2088
|
+
console.log(` ${logger5.dim("\u2022")} Run ${logger5.brand("rapid stop")} when done`);
|
|
2089
|
+
logger5.blank();
|
|
2090
|
+
} catch (error) {
|
|
2091
|
+
spinner.fail("Failed to start environment");
|
|
2092
|
+
logger5.error(error instanceof Error ? error.message : String(error));
|
|
2093
|
+
process.exit(1);
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
|
|
2097
|
+
// src/commands/stop.ts
|
|
2098
|
+
import { Command as Command6 } from "commander";
|
|
2099
|
+
import { loadConfig as loadConfig5, logger as logger6, getContainerStatus as getContainerStatus4, stopContainer } from "@a3t/rapid-core";
|
|
2100
|
+
import ora5 from "ora";
|
|
2101
|
+
var stopCommand = new Command6("stop").description("Stop the development container").option("--remove", "Remove container after stopping", false).action(async (options) => {
|
|
2102
|
+
const spinner = ora5("Stopping development environment...").start();
|
|
2103
|
+
try {
|
|
2104
|
+
const loaded = await loadConfig5();
|
|
2105
|
+
if (!loaded) {
|
|
2106
|
+
spinner.fail("No rapid.json found");
|
|
2107
|
+
process.exit(1);
|
|
2108
|
+
}
|
|
2109
|
+
const { config, rootDir } = loaded;
|
|
2110
|
+
spinner.text = "Checking container status...";
|
|
2111
|
+
const status = await getContainerStatus4(rootDir, config);
|
|
2112
|
+
if (!status.exists) {
|
|
2113
|
+
spinner.succeed("No container to stop");
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
if (!status.running) {
|
|
2117
|
+
if (options.remove) {
|
|
2118
|
+
spinner.text = "Removing container...";
|
|
2119
|
+
await stopContainer(rootDir, config, { remove: true });
|
|
2120
|
+
spinner.succeed("Container removed");
|
|
2121
|
+
} else {
|
|
2122
|
+
spinner.succeed("Container already stopped");
|
|
2123
|
+
}
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
spinner.text = "Stopping container...";
|
|
2127
|
+
const result = await stopContainer(rootDir, config, { remove: options.remove });
|
|
2128
|
+
if (!result.success) {
|
|
2129
|
+
spinner.fail("Failed to stop container");
|
|
2130
|
+
logger6.error(result.error || "Unknown error");
|
|
2131
|
+
process.exit(1);
|
|
2132
|
+
}
|
|
2133
|
+
spinner.succeed(options.remove ? "Container stopped and removed" : "Container stopped");
|
|
2134
|
+
} catch (error) {
|
|
2135
|
+
spinner.fail("Failed to stop environment");
|
|
2136
|
+
logger6.error(error instanceof Error ? error.message : String(error));
|
|
2137
|
+
process.exit(1);
|
|
2138
|
+
}
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
// src/commands/secrets.ts
|
|
2142
|
+
import { Command as Command7 } from "commander";
|
|
2143
|
+
import {
|
|
2144
|
+
loadConfig as loadConfig6,
|
|
2145
|
+
logger as logger7,
|
|
2146
|
+
verifySecrets as verifySecrets2,
|
|
2147
|
+
loadSecrets as loadSecrets2,
|
|
2148
|
+
hasOpCli as hasOpCli3,
|
|
2149
|
+
hasVaultCli as hasVaultCli3,
|
|
2150
|
+
isOpAuthenticated as isOpAuthenticated3,
|
|
2151
|
+
isVaultAuthenticated as isVaultAuthenticated3,
|
|
2152
|
+
getOpAuthStatus,
|
|
2153
|
+
hasOpServiceAccountToken,
|
|
2154
|
+
generateEnvrc,
|
|
2155
|
+
writeEnvrc,
|
|
2156
|
+
hasEnvrc as hasEnvrc2,
|
|
2157
|
+
getProviderInfo as getProviderInfo2
|
|
2158
|
+
} from "@a3t/rapid-core";
|
|
2159
|
+
import ora6 from "ora";
|
|
2160
|
+
var secretsCommand = new Command7("secrets").description("Manage project secrets");
|
|
2161
|
+
secretsCommand.command("verify").description("Verify all secrets are accessible").option("--json", "Output as JSON").action(async (options) => {
|
|
2162
|
+
const spinner = ora6("Verifying secrets...").start();
|
|
2163
|
+
try {
|
|
2164
|
+
const loaded = await loadConfig6();
|
|
2165
|
+
if (!loaded) {
|
|
2166
|
+
spinner.fail("No rapid.json found");
|
|
2167
|
+
process.exit(1);
|
|
2168
|
+
}
|
|
2169
|
+
const { config } = loaded;
|
|
2170
|
+
const secretsConfig = config.secrets;
|
|
2171
|
+
if (!secretsConfig) {
|
|
2172
|
+
spinner.info("No secrets configured");
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
const provider = secretsConfig.provider || "env";
|
|
2176
|
+
spinner.text = `Checking ${provider} availability...`;
|
|
2177
|
+
if (provider === "1password") {
|
|
2178
|
+
const hasOp = await hasOpCli3();
|
|
2179
|
+
if (!hasOp) {
|
|
2180
|
+
spinner.fail("1Password CLI (op) not found");
|
|
2181
|
+
console.log();
|
|
2182
|
+
logger7.info("Install with: brew install 1password-cli");
|
|
2183
|
+
logger7.info("More info: https://developer.1password.com/docs/cli/get-started/");
|
|
2184
|
+
process.exit(1);
|
|
2185
|
+
}
|
|
2186
|
+
const authenticated = await isOpAuthenticated3();
|
|
2187
|
+
if (!authenticated) {
|
|
2188
|
+
spinner.fail("1Password CLI not authenticated");
|
|
2189
|
+
console.log();
|
|
2190
|
+
logger7.info("Run: eval $(op signin)");
|
|
2191
|
+
process.exit(1);
|
|
2192
|
+
}
|
|
2193
|
+
} else if (provider === "vault") {
|
|
2194
|
+
const hasVault = await hasVaultCli3();
|
|
2195
|
+
if (!hasVault) {
|
|
2196
|
+
spinner.fail("Vault CLI not found");
|
|
2197
|
+
console.log();
|
|
2198
|
+
logger7.info("Install from: https://developer.hashicorp.com/vault/docs/install");
|
|
2199
|
+
process.exit(1);
|
|
2200
|
+
}
|
|
2201
|
+
const authenticated = await isVaultAuthenticated3();
|
|
2202
|
+
if (!authenticated) {
|
|
2203
|
+
spinner.fail("Vault CLI not authenticated");
|
|
2204
|
+
console.log();
|
|
2205
|
+
logger7.info("Run: vault login");
|
|
2206
|
+
process.exit(1);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
spinner.text = "Verifying secrets...";
|
|
2210
|
+
const status = await verifySecrets2(secretsConfig);
|
|
2211
|
+
spinner.stop();
|
|
2212
|
+
if (options.json) {
|
|
2213
|
+
console.log(JSON.stringify(status, null, 2));
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
console.log();
|
|
2217
|
+
console.log(` ${logger7.brand("Secrets")} Verification`);
|
|
2218
|
+
console.log(` ${logger7.dim("\u2500".repeat(24))}`);
|
|
2219
|
+
console.log();
|
|
2220
|
+
console.log(` ${logger7.dim("Provider:")} ${getProviderInfo2(provider).name}`);
|
|
2221
|
+
console.log(
|
|
2222
|
+
` ${logger7.dim("Auth:")} ${status.authenticated ? logger7.brand("\u2713") : logger7.dim("\u25CB")} ${status.authenticated ? "Authenticated" : "Not authenticated"}`
|
|
2223
|
+
);
|
|
2224
|
+
console.log();
|
|
2225
|
+
if (status.secrets.length === 0) {
|
|
2226
|
+
console.log(` ${logger7.dim("No secrets configured in rapid.json")}`);
|
|
2227
|
+
console.log();
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
console.log(` ${logger7.dim("Secrets:")}`);
|
|
2231
|
+
for (const secret of status.secrets) {
|
|
2232
|
+
const icon = secret.available ? logger7.brand("\u2713") : "\u2717";
|
|
2233
|
+
const ref = logger7.dim(`(${provider})`);
|
|
2234
|
+
const error = secret.error ? logger7.dim(` - ${secret.error}`) : "";
|
|
2235
|
+
console.log(` ${icon} ${secret.name} ${ref}${error}`);
|
|
2236
|
+
}
|
|
2237
|
+
console.log();
|
|
2238
|
+
if (status.allAvailable) {
|
|
2239
|
+
logger7.info("All secrets verified successfully!");
|
|
2240
|
+
} else {
|
|
2241
|
+
logger7.error("Some secrets are not available");
|
|
2242
|
+
process.exit(1);
|
|
2243
|
+
}
|
|
2244
|
+
console.log();
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
spinner.fail("Failed to verify secrets");
|
|
2247
|
+
logger7.error(error instanceof Error ? error.message : String(error));
|
|
2248
|
+
process.exit(1);
|
|
2249
|
+
}
|
|
2250
|
+
});
|
|
2251
|
+
secretsCommand.command("list").description("List configured secrets (names only, not values)").option("--json", "Output as JSON").action(async (options) => {
|
|
2252
|
+
try {
|
|
2253
|
+
const loaded = await loadConfig6();
|
|
2254
|
+
if (!loaded) {
|
|
2255
|
+
logger7.error("No rapid.json found");
|
|
2256
|
+
process.exit(1);
|
|
2257
|
+
}
|
|
2258
|
+
const { config } = loaded;
|
|
2259
|
+
const secretsConfig = config.secrets;
|
|
2260
|
+
if (!secretsConfig || !secretsConfig.items || Object.keys(secretsConfig.items).length === 0) {
|
|
2261
|
+
if (options.json) {
|
|
2262
|
+
console.log(JSON.stringify({ secrets: [] }, null, 2));
|
|
2263
|
+
} else {
|
|
2264
|
+
logger7.info("No secrets configured");
|
|
2265
|
+
}
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
const provider = secretsConfig.provider || "env";
|
|
2269
|
+
if (options.json) {
|
|
2270
|
+
const secrets = Object.entries(secretsConfig.items).map(([name, reference]) => ({
|
|
2271
|
+
name,
|
|
2272
|
+
reference,
|
|
2273
|
+
provider
|
|
2274
|
+
}));
|
|
2275
|
+
console.log(JSON.stringify({ provider, secrets }, null, 2));
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
console.log();
|
|
2279
|
+
console.log(` ${logger7.brand("Configured Secrets")}`);
|
|
2280
|
+
console.log(` ${logger7.dim("\u2500".repeat(24))}`);
|
|
2281
|
+
console.log();
|
|
2282
|
+
console.log(` ${logger7.dim("Provider:")} ${getProviderInfo2(provider).name}`);
|
|
2283
|
+
console.log();
|
|
2284
|
+
const maxNameLen = Math.max(...Object.keys(secretsConfig.items).map((n) => n.length));
|
|
2285
|
+
for (const [name, reference] of Object.entries(secretsConfig.items)) {
|
|
2286
|
+
const paddedName = name.padEnd(maxNameLen);
|
|
2287
|
+
console.log(` ${logger7.brand("\u2022")} ${paddedName} ${logger7.dim(reference)}`);
|
|
2288
|
+
}
|
|
2289
|
+
console.log();
|
|
2290
|
+
} catch (error) {
|
|
2291
|
+
logger7.error(error instanceof Error ? error.message : String(error));
|
|
2292
|
+
process.exit(1);
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
secretsCommand.command("generate").description("Generate .envrc file from rapid.json configuration").option("--force", "Overwrite existing .envrc", false).option("--stdout", "Print to stdout instead of writing file", false).action(async (options) => {
|
|
2296
|
+
try {
|
|
2297
|
+
const loaded = await loadConfig6();
|
|
2298
|
+
if (!loaded) {
|
|
2299
|
+
logger7.error("No rapid.json found");
|
|
2300
|
+
process.exit(1);
|
|
2301
|
+
}
|
|
2302
|
+
const { config, rootDir } = loaded;
|
|
2303
|
+
const secretsConfig = config.secrets;
|
|
2304
|
+
if (!secretsConfig) {
|
|
2305
|
+
logger7.error("No secrets configuration in rapid.json");
|
|
2306
|
+
process.exit(1);
|
|
2307
|
+
}
|
|
2308
|
+
const content = generateEnvrc(secretsConfig);
|
|
2309
|
+
if (options.stdout) {
|
|
2310
|
+
console.log(content);
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
const envrcExists = await hasEnvrc2(rootDir, secretsConfig);
|
|
2314
|
+
if (envrcExists && !options.force) {
|
|
2315
|
+
logger7.error(".envrc already exists. Use --force to overwrite.");
|
|
2316
|
+
process.exit(1);
|
|
2317
|
+
}
|
|
2318
|
+
const spinner = ora6("Generating .envrc...").start();
|
|
2319
|
+
const filepath = await writeEnvrc(rootDir, secretsConfig);
|
|
2320
|
+
spinner.succeed("Generated .envrc");
|
|
2321
|
+
console.log();
|
|
2322
|
+
console.log(` ${logger7.dim("File:")} ${filepath}`);
|
|
2323
|
+
console.log();
|
|
2324
|
+
const itemCount = secretsConfig.items ? Object.keys(secretsConfig.items).length : 0;
|
|
2325
|
+
logger7.info(`Generated .envrc with ${itemCount} secret${itemCount !== 1 ? "s" : ""}`);
|
|
2326
|
+
logger7.info(`Run ${logger7.brand("direnv allow")} to activate`);
|
|
2327
|
+
console.log();
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
logger7.error(error instanceof Error ? error.message : String(error));
|
|
2330
|
+
process.exit(1);
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2333
|
+
secretsCommand.command("info").description("Show secrets provider information and authentication status").option("--json", "Output as JSON").action(async (options) => {
|
|
2334
|
+
const spinner = ora6("Checking provider status...").start();
|
|
2335
|
+
try {
|
|
2336
|
+
const loaded = await loadConfig6();
|
|
2337
|
+
if (!loaded) {
|
|
2338
|
+
spinner.fail("No rapid.json found");
|
|
2339
|
+
process.exit(1);
|
|
2340
|
+
}
|
|
2341
|
+
const { config, rootDir } = loaded;
|
|
2342
|
+
const secretsConfig = config.secrets || { provider: "env" };
|
|
2343
|
+
const provider = secretsConfig.provider || "env";
|
|
2344
|
+
const info = getProviderInfo2(provider);
|
|
2345
|
+
let cliInstalled = true;
|
|
2346
|
+
let authenticated = true;
|
|
2347
|
+
let opAuthStatus = null;
|
|
2348
|
+
if (provider === "1password") {
|
|
2349
|
+
cliInstalled = await hasOpCli3();
|
|
2350
|
+
if (cliInstalled) {
|
|
2351
|
+
opAuthStatus = await getOpAuthStatus();
|
|
2352
|
+
authenticated = opAuthStatus.authenticated;
|
|
2353
|
+
} else {
|
|
2354
|
+
authenticated = false;
|
|
2355
|
+
}
|
|
2356
|
+
} else if (provider === "vault") {
|
|
2357
|
+
cliInstalled = await hasVaultCli3();
|
|
2358
|
+
authenticated = cliInstalled && await isVaultAuthenticated3();
|
|
2359
|
+
}
|
|
2360
|
+
const envrcExists = await hasEnvrc2(rootDir, secretsConfig);
|
|
2361
|
+
const hasServiceToken = hasOpServiceAccountToken();
|
|
2362
|
+
spinner.stop();
|
|
2363
|
+
const status = {
|
|
2364
|
+
provider,
|
|
2365
|
+
providerName: info.name,
|
|
2366
|
+
cliRequired: info.cliRequired,
|
|
2367
|
+
cliInstalled,
|
|
2368
|
+
authenticated,
|
|
2369
|
+
authMethod: opAuthStatus?.method,
|
|
2370
|
+
accountInfo: opAuthStatus?.accountInfo,
|
|
2371
|
+
hasServiceToken,
|
|
2372
|
+
envrcExists,
|
|
2373
|
+
envrcPath: secretsConfig.envrc?.path || ".envrc",
|
|
2374
|
+
secretsCount: secretsConfig.items ? Object.keys(secretsConfig.items).length : 0
|
|
2375
|
+
};
|
|
2376
|
+
if (options.json) {
|
|
2377
|
+
console.log(JSON.stringify(status, null, 2));
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
console.log();
|
|
2381
|
+
console.log(` ${logger7.brand("Secrets")} Provider Info`);
|
|
2382
|
+
console.log(` ${logger7.dim("\u2500".repeat(24))}`);
|
|
2383
|
+
console.log();
|
|
2384
|
+
console.log(` ${logger7.dim("Provider:")} ${info.name}`);
|
|
2385
|
+
if (info.cliRequired) {
|
|
2386
|
+
const cliIcon = cliInstalled ? logger7.brand("\u2713") : "\u2717";
|
|
2387
|
+
console.log(
|
|
2388
|
+
` ${logger7.dim("CLI:")} ${cliIcon} ${info.cliRequired} ${cliInstalled ? "" : logger7.dim("(not installed)")}`
|
|
2389
|
+
);
|
|
2390
|
+
if (cliInstalled && provider === "1password" && opAuthStatus) {
|
|
2391
|
+
const authIcon = authenticated ? logger7.brand("\u2713") : "\u2717";
|
|
2392
|
+
const methodLabel = opAuthStatus.method === "service-account" ? "Service Account" : opAuthStatus.method === "user" ? "User" : "Not authenticated";
|
|
2393
|
+
const accountLabel = opAuthStatus.accountInfo ? ` (${opAuthStatus.accountInfo})` : "";
|
|
2394
|
+
console.log(` ${logger7.dim("Auth:")} ${authIcon} ${methodLabel}${accountLabel}`);
|
|
2395
|
+
if (hasServiceToken) {
|
|
2396
|
+
console.log(
|
|
2397
|
+
` ${logger7.dim("Token:")} ${logger7.brand("\u2713")} OP_SERVICE_ACCOUNT_TOKEN set`
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
} else if (cliInstalled) {
|
|
2401
|
+
const authIcon = authenticated ? logger7.brand("\u2713") : "\u2717";
|
|
2402
|
+
console.log(
|
|
2403
|
+
` ${logger7.dim("Auth:")} ${authIcon} ${authenticated ? "Authenticated" : "Not authenticated"}`
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
if (info.installUrl && !cliInstalled) {
|
|
2407
|
+
console.log();
|
|
2408
|
+
console.log(` ${logger7.dim("Install:")} ${info.installUrl}`);
|
|
2409
|
+
}
|
|
2410
|
+
if (info.authCommand && cliInstalled && !authenticated) {
|
|
2411
|
+
console.log();
|
|
2412
|
+
console.log(` ${logger7.dim("Authenticate:")} ${info.authCommand}`);
|
|
2413
|
+
if (provider === "1password") {
|
|
2414
|
+
console.log(
|
|
2415
|
+
` ${logger7.dim("Or set:")} OP_SERVICE_ACCOUNT_TOKEN for non-interactive auth`
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
console.log();
|
|
2421
|
+
const envrcIcon = envrcExists ? logger7.brand("\u2713") : logger7.dim("\u25CB");
|
|
2422
|
+
console.log(
|
|
2423
|
+
` ${logger7.dim(".envrc:")} ${envrcIcon} ${envrcExists ? "Exists" : "Not generated"}`
|
|
2424
|
+
);
|
|
2425
|
+
console.log(` ${logger7.dim("Secrets:")} ${status.secretsCount} configured`);
|
|
2426
|
+
console.log();
|
|
2427
|
+
if (!envrcExists && status.secretsCount > 0) {
|
|
2428
|
+
logger7.info(`Run ${logger7.brand("rapid secrets generate")} to create .envrc`);
|
|
2429
|
+
console.log();
|
|
2430
|
+
}
|
|
2431
|
+
} catch (error) {
|
|
2432
|
+
spinner.fail("Failed to get provider info");
|
|
2433
|
+
logger7.error(error instanceof Error ? error.message : String(error));
|
|
2434
|
+
process.exit(1);
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
2437
|
+
secretsCommand.command("run").description("Run a command with secrets loaded into environment").argument("<command...>", "Command to run with secrets").option("--show", "Show which secrets are being loaded (names only)", false).action(async (commandArgs, options) => {
|
|
2438
|
+
try {
|
|
2439
|
+
const loaded = await loadConfig6();
|
|
2440
|
+
if (!loaded) {
|
|
2441
|
+
logger7.error("No rapid.json found");
|
|
2442
|
+
process.exit(1);
|
|
2443
|
+
}
|
|
2444
|
+
const { config } = loaded;
|
|
2445
|
+
const secretsConfig = config.secrets;
|
|
2446
|
+
if (!secretsConfig || !secretsConfig.items) {
|
|
2447
|
+
logger7.error("No secrets configured in rapid.json");
|
|
2448
|
+
process.exit(1);
|
|
2449
|
+
}
|
|
2450
|
+
const provider = secretsConfig.provider || "env";
|
|
2451
|
+
if (provider === "1password") {
|
|
2452
|
+
const hasOp = await hasOpCli3();
|
|
2453
|
+
if (!hasOp) {
|
|
2454
|
+
logger7.error("1Password CLI (op) not found");
|
|
2455
|
+
logger7.info("Install with: brew install 1password-cli");
|
|
2456
|
+
process.exit(1);
|
|
2457
|
+
}
|
|
2458
|
+
const authenticated = await isOpAuthenticated3();
|
|
2459
|
+
if (!authenticated) {
|
|
2460
|
+
logger7.error("1Password not authenticated");
|
|
2461
|
+
logger7.info("Run: eval $(op signin)");
|
|
2462
|
+
process.exit(1);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
const secrets = await loadSecrets2(secretsConfig);
|
|
2466
|
+
const secretCount = Object.keys(secrets).length;
|
|
2467
|
+
if (secretCount === 0) {
|
|
2468
|
+
logger7.warn("No secrets were loaded");
|
|
2469
|
+
} else if (options.show) {
|
|
2470
|
+
logger7.info(`Loaded ${secretCount} secret${secretCount !== 1 ? "s" : ""}:`);
|
|
2471
|
+
for (const name of Object.keys(secrets)) {
|
|
2472
|
+
console.log(` ${logger7.brand("\u2022")} ${name}`);
|
|
2473
|
+
}
|
|
2474
|
+
console.log();
|
|
2475
|
+
}
|
|
2476
|
+
const { execa: execa4 } = await import("execa");
|
|
2477
|
+
const [cmd, ...args] = commandArgs;
|
|
2478
|
+
if (!cmd) {
|
|
2479
|
+
logger7.error("No command specified");
|
|
2480
|
+
process.exit(1);
|
|
2481
|
+
}
|
|
2482
|
+
await execa4(cmd, args, {
|
|
2483
|
+
stdio: "inherit",
|
|
2484
|
+
env: {
|
|
2485
|
+
...process.env,
|
|
2486
|
+
...secrets
|
|
2487
|
+
}
|
|
2488
|
+
});
|
|
2489
|
+
} catch (error) {
|
|
2490
|
+
if (error.exitCode !== void 0) {
|
|
2491
|
+
process.exit(error.exitCode);
|
|
2492
|
+
}
|
|
2493
|
+
logger7.error(error instanceof Error ? error.message : String(error));
|
|
2494
|
+
process.exit(1);
|
|
2495
|
+
}
|
|
2496
|
+
});
|
|
2497
|
+
|
|
2498
|
+
// src/commands/auth.ts
|
|
2499
|
+
import { Command as Command8 } from "commander";
|
|
2500
|
+
import {
|
|
2501
|
+
logger as logger8,
|
|
2502
|
+
getAuthStatus as getAuthStatus2,
|
|
2503
|
+
detectAllCredentials
|
|
2504
|
+
} from "@a3t/rapid-core";
|
|
2505
|
+
import ora7 from "ora";
|
|
2506
|
+
var authCommand = new Command8("auth").description("Show authentication status from external tools").option("--json", "Output as JSON").option("--source <source>", "Filter by source (claude-code, codex, gemini-cli, aider, env)").option("--provider <provider>", "Filter by provider (anthropic, openai, google)").action(async (options) => {
|
|
2507
|
+
try {
|
|
2508
|
+
const spinner = ora7("Checking authentication...").start();
|
|
2509
|
+
const status = await getAuthStatus2();
|
|
2510
|
+
spinner.stop();
|
|
2511
|
+
if (options.json) {
|
|
2512
|
+
let credentials = status.sources;
|
|
2513
|
+
if (options.source) {
|
|
2514
|
+
credentials = credentials.filter((c) => c.source === options.source);
|
|
2515
|
+
}
|
|
2516
|
+
if (options.provider) {
|
|
2517
|
+
credentials = credentials.filter(
|
|
2518
|
+
(c) => c.provider === options.provider
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
const sanitized = credentials.map((c) => ({
|
|
2522
|
+
source: c.source,
|
|
2523
|
+
provider: c.provider,
|
|
2524
|
+
authType: c.authType,
|
|
2525
|
+
envVar: c.envVar,
|
|
2526
|
+
expiresAt: c.expiresAt?.toISOString(),
|
|
2527
|
+
accountInfo: c.accountInfo,
|
|
2528
|
+
configPath: c.configPath,
|
|
2529
|
+
hasValue: !!c.value
|
|
2530
|
+
}));
|
|
2531
|
+
console.log(
|
|
2532
|
+
JSON.stringify(
|
|
2533
|
+
{
|
|
2534
|
+
authenticated: sanitized.length > 0,
|
|
2535
|
+
sources: sanitized,
|
|
2536
|
+
warnings: status.warnings
|
|
2537
|
+
},
|
|
2538
|
+
null,
|
|
2539
|
+
2
|
|
2540
|
+
)
|
|
2541
|
+
);
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
console.log();
|
|
2545
|
+
console.log(` ${logger8.brand("RAPID")} Authentication`);
|
|
2546
|
+
console.log(` ${logger8.dim("\u2500".repeat(24))}`);
|
|
2547
|
+
console.log();
|
|
2548
|
+
if (!status.authenticated) {
|
|
2549
|
+
console.log(` ${logger8.dim("No authentication detected")}`);
|
|
2550
|
+
console.log();
|
|
2551
|
+
console.log(" To authenticate, use one of these methods:");
|
|
2552
|
+
console.log();
|
|
2553
|
+
console.log(` ${logger8.brand("Claude Pro/Max:")}`);
|
|
2554
|
+
console.log(" Run `claude` and sign in with your Anthropic account");
|
|
2555
|
+
console.log();
|
|
2556
|
+
console.log(` ${logger8.brand("OpenAI (ChatGPT Plus/Pro):")}`);
|
|
2557
|
+
console.log(" Run `codex` and sign in with ChatGPT");
|
|
2558
|
+
console.log();
|
|
2559
|
+
console.log(` ${logger8.brand("Gemini:")}`);
|
|
2560
|
+
console.log(" Run `gemini` and sign in with your Google account");
|
|
2561
|
+
console.log();
|
|
2562
|
+
console.log(` ${logger8.brand("API Keys:")}`);
|
|
2563
|
+
console.log(" Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY");
|
|
2564
|
+
console.log();
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
for (const cred of status.sources) {
|
|
2568
|
+
if (options.source && cred.source !== options.source) continue;
|
|
2569
|
+
if (options.provider && cred.provider !== options.provider) continue;
|
|
2570
|
+
const isPrimary = cred === status.preferredSource;
|
|
2571
|
+
const icon = isPrimary ? logger8.brand("\u25CF") : logger8.dim("\u25CB");
|
|
2572
|
+
const authIcon = cred.authType === "oauth" ? logger8.brand("OAuth") : logger8.dim("API Key");
|
|
2573
|
+
console.log(` ${icon} ${logger8.bold(cred.source)}`);
|
|
2574
|
+
console.log(` Provider: ${cred.provider}`);
|
|
2575
|
+
console.log(` Auth: ${authIcon}`);
|
|
2576
|
+
if (cred.accountInfo?.email) {
|
|
2577
|
+
console.log(` Account: ${cred.accountInfo.email}`);
|
|
2578
|
+
}
|
|
2579
|
+
if (cred.accountInfo?.organization) {
|
|
2580
|
+
console.log(` Org: ${cred.accountInfo.organization}`);
|
|
2581
|
+
}
|
|
2582
|
+
if (cred.accountInfo?.plan) {
|
|
2583
|
+
console.log(` Plan: ${logger8.brand(cred.accountInfo.plan)}`);
|
|
2584
|
+
}
|
|
2585
|
+
if (cred.expiresAt) {
|
|
2586
|
+
const now = /* @__PURE__ */ new Date();
|
|
2587
|
+
const expiresIn = Math.round((cred.expiresAt.getTime() - now.getTime()) / 1e3 / 60);
|
|
2588
|
+
if (expiresIn > 0) {
|
|
2589
|
+
console.log(` Expires: in ${expiresIn} minutes`);
|
|
2590
|
+
} else {
|
|
2591
|
+
console.log(` Expires: ${logger8.dim("EXPIRED")}`);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
if (cred.configPath) {
|
|
2595
|
+
console.log(` Config: ${logger8.dim(cred.configPath)}`);
|
|
2596
|
+
}
|
|
2597
|
+
if (cred.envVar) {
|
|
2598
|
+
console.log(` Env: ${logger8.dim(cred.envVar)}`);
|
|
2599
|
+
}
|
|
2600
|
+
if (isPrimary) {
|
|
2601
|
+
console.log(` ${logger8.brand("\u2192 Primary source")}`);
|
|
2602
|
+
}
|
|
2603
|
+
console.log();
|
|
2604
|
+
}
|
|
2605
|
+
if (status.warnings && status.warnings.length > 0) {
|
|
2606
|
+
console.log(` ${logger8.dim("Warnings:")}`);
|
|
2607
|
+
for (const warning of status.warnings) {
|
|
2608
|
+
console.log(` ! ${warning}`);
|
|
2609
|
+
}
|
|
2610
|
+
console.log();
|
|
2611
|
+
}
|
|
2612
|
+
console.log(` ${logger8.dim("Tip:")} RAPID will automatically use detected`);
|
|
2613
|
+
console.log(` ${logger8.dim(" ")} credentials when launching AI agents.`);
|
|
2614
|
+
console.log();
|
|
2615
|
+
} catch (error) {
|
|
2616
|
+
logger8.error(error instanceof Error ? error.message : String(error));
|
|
2617
|
+
process.exit(1);
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
authCommand.command("env").description("Show environment variables for detected credentials").option("--export", "Output as export statements").option("--json", "Output as JSON").action(async (options) => {
|
|
2621
|
+
try {
|
|
2622
|
+
const credentials = await detectAllCredentials();
|
|
2623
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
2624
|
+
for (const cred of credentials) {
|
|
2625
|
+
if (!cred.value) continue;
|
|
2626
|
+
const existing = byProvider.get(cred.provider);
|
|
2627
|
+
if (!existing || cred.authType === "oauth" && cred.envVar) {
|
|
2628
|
+
let envVar;
|
|
2629
|
+
switch (cred.provider) {
|
|
2630
|
+
case "anthropic":
|
|
2631
|
+
envVar = cred.authType === "oauth" ? "ANTHROPIC_AUTH_TOKEN" : "ANTHROPIC_API_KEY";
|
|
2632
|
+
break;
|
|
2633
|
+
case "openai":
|
|
2634
|
+
envVar = cred.authType === "oauth" ? "OPENAI_AUTH_TOKEN" : "OPENAI_API_KEY";
|
|
2635
|
+
break;
|
|
2636
|
+
case "google":
|
|
2637
|
+
envVar = cred.authType === "oauth" ? "GOOGLE_AUTH_TOKEN" : cred.envVar || "GEMINI_API_KEY";
|
|
2638
|
+
break;
|
|
2639
|
+
default:
|
|
2640
|
+
continue;
|
|
2641
|
+
}
|
|
2642
|
+
const masked = cred.value.slice(0, 8) + "..." + cred.value.slice(-4);
|
|
2643
|
+
byProvider.set(cred.provider, { envVar, masked });
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
if (options.json) {
|
|
2647
|
+
const result = {};
|
|
2648
|
+
for (const [, { envVar, masked }] of byProvider) {
|
|
2649
|
+
result[envVar] = masked;
|
|
2650
|
+
}
|
|
2651
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
if (options.export) {
|
|
2655
|
+
for (const [, { envVar }] of byProvider) {
|
|
2656
|
+
console.log(`# ${envVar} detected from external auth`);
|
|
2657
|
+
console.log(`# export ${envVar}="<your-token>"`);
|
|
2658
|
+
}
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
console.log();
|
|
2662
|
+
console.log(` ${logger8.brand("RAPID")} Auth Environment`);
|
|
2663
|
+
console.log(` ${logger8.dim("\u2500".repeat(24))}`);
|
|
2664
|
+
console.log();
|
|
2665
|
+
if (byProvider.size === 0) {
|
|
2666
|
+
console.log(` ${logger8.dim("No credentials detected")}`);
|
|
2667
|
+
console.log();
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
for (const [provider, { envVar, masked }] of byProvider) {
|
|
2671
|
+
console.log(` ${logger8.brand(provider)}`);
|
|
2672
|
+
console.log(` ${envVar}=${masked}`);
|
|
2673
|
+
console.log();
|
|
2674
|
+
}
|
|
2675
|
+
console.log(` ${logger8.dim("These will be automatically injected when running agents.")}`);
|
|
2676
|
+
console.log();
|
|
2677
|
+
} catch (error) {
|
|
2678
|
+
logger8.error(error instanceof Error ? error.message : String(error));
|
|
2679
|
+
process.exit(1);
|
|
2680
|
+
}
|
|
2681
|
+
});
|
|
2682
|
+
|
|
2683
|
+
// src/commands/mcp.ts
|
|
2684
|
+
import { Command as Command9 } from "commander";
|
|
2685
|
+
import { writeFile as writeFile4 } from "fs/promises";
|
|
2686
|
+
import { join as join5 } from "path";
|
|
2687
|
+
import {
|
|
2688
|
+
loadConfig as loadConfig7,
|
|
2689
|
+
logger as logger9,
|
|
2690
|
+
getMcpServers,
|
|
2691
|
+
getMcpServerStatus,
|
|
2692
|
+
addMcpServerFromTemplate as addMcpServerFromTemplate2,
|
|
2693
|
+
addMcpServer,
|
|
2694
|
+
removeMcpServer,
|
|
2695
|
+
enableMcpServer,
|
|
2696
|
+
disableMcpServer,
|
|
2697
|
+
writeMcpConfig as writeMcpConfig2,
|
|
2698
|
+
writeOpenCodeConfig as writeOpenCodeConfig2,
|
|
2699
|
+
MCP_SERVER_TEMPLATES as MCP_SERVER_TEMPLATES2,
|
|
2700
|
+
getMcpTemplate
|
|
2701
|
+
} from "@a3t/rapid-core";
|
|
2702
|
+
import ora8 from "ora";
|
|
2703
|
+
var mcpCommand = new Command9("mcp").description(
|
|
2704
|
+
"Manage MCP (Model Context Protocol) servers"
|
|
2705
|
+
);
|
|
2706
|
+
async function saveConfig(rootDir, config) {
|
|
2707
|
+
const configPath = join5(rootDir, "rapid.json");
|
|
2708
|
+
await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2709
|
+
}
|
|
2710
|
+
mcpCommand.command("list").description("List configured MCP servers").option("--json", "Output as JSON").option("--templates", "Show available templates instead of configured servers").action(async (options) => {
|
|
2711
|
+
try {
|
|
2712
|
+
if (options.templates) {
|
|
2713
|
+
if (options.json) {
|
|
2714
|
+
console.log(JSON.stringify(MCP_SERVER_TEMPLATES2, null, 2));
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
console.log();
|
|
2718
|
+
console.log(` ${logger9.brand("Available MCP Server Templates")}`);
|
|
2719
|
+
console.log(` ${logger9.dim("\u2500".repeat(40))}`);
|
|
2720
|
+
console.log();
|
|
2721
|
+
for (const [name, template] of Object.entries(MCP_SERVER_TEMPLATES2)) {
|
|
2722
|
+
const typeLabel = template.type === "remote" ? logger9.dim("(remote)") : logger9.dim("(stdio)");
|
|
2723
|
+
const secretsLabel = template.requiredSecrets.length > 0 ? logger9.dim(` - requires: ${template.requiredSecrets.join(", ")}`) : logger9.dim(" - no secrets required");
|
|
2724
|
+
console.log(` ${logger9.brand("\u2022")} ${name} ${typeLabel}`);
|
|
2725
|
+
console.log(` ${template.description}${secretsLabel}`);
|
|
2726
|
+
console.log();
|
|
2727
|
+
}
|
|
2728
|
+
logger9.info(`Use ${logger9.brand("rapid mcp add <name>")} to add a server`);
|
|
2729
|
+
console.log();
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
const loaded = await loadConfig7();
|
|
2733
|
+
if (!loaded) {
|
|
2734
|
+
logger9.error("No rapid.json found. Run `rapid init` first.");
|
|
2735
|
+
process.exit(1);
|
|
2736
|
+
}
|
|
2737
|
+
const { config } = loaded;
|
|
2738
|
+
const servers = getMcpServers(config);
|
|
2739
|
+
if (options.json) {
|
|
2740
|
+
console.log(JSON.stringify({ servers }, null, 2));
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
console.log();
|
|
2744
|
+
console.log(` ${logger9.brand("MCP Servers")}`);
|
|
2745
|
+
console.log(` ${logger9.dim("\u2500".repeat(40))}`);
|
|
2746
|
+
console.log();
|
|
2747
|
+
if (servers.length === 0) {
|
|
2748
|
+
console.log(` ${logger9.dim("No MCP servers configured")}`);
|
|
2749
|
+
console.log();
|
|
2750
|
+
logger9.info(`Use ${logger9.brand("rapid mcp add <name>")} to add a server`);
|
|
2751
|
+
logger9.info(`Use ${logger9.brand("rapid mcp list --templates")} to see available templates`);
|
|
2752
|
+
console.log();
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
for (const server of servers) {
|
|
2756
|
+
const icon = server.enabled ? logger9.brand("\u2713") : logger9.dim("\u25CB");
|
|
2757
|
+
const typeLabel = server.type === "remote" ? logger9.dim("(remote)") : logger9.dim("(stdio)");
|
|
2758
|
+
const statusLabel = server.enabled ? "" : logger9.dim(" [disabled]");
|
|
2759
|
+
const location = server.type === "remote" ? logger9.dim(server.url || "") : logger9.dim(server.command || "");
|
|
2760
|
+
console.log(` ${icon} ${server.name} ${typeLabel}${statusLabel}`);
|
|
2761
|
+
if (location) {
|
|
2762
|
+
console.log(` ${location}`);
|
|
2763
|
+
}
|
|
2764
|
+
console.log();
|
|
2765
|
+
}
|
|
2766
|
+
} catch (error) {
|
|
2767
|
+
logger9.error(error instanceof Error ? error.message : String(error));
|
|
2768
|
+
process.exit(1);
|
|
2769
|
+
}
|
|
2770
|
+
});
|
|
2771
|
+
mcpCommand.command("add").description("Add an MCP server").argument("<name>", "Server name (or template name)").option("--type <type>", "Server type: remote or stdio").option("--url <url>", "URL for remote servers").option("--command <cmd>", "Command for stdio servers").option("--args <args>", "Arguments for stdio command (comma-separated)").option("--header <header>", "HTTP header for remote servers (name=value)", collectHeaders, {}).action(async (name, options) => {
|
|
2772
|
+
const spinner = ora8(`Adding MCP server '${name}'...`).start();
|
|
2773
|
+
try {
|
|
2774
|
+
const loaded = await loadConfig7();
|
|
2775
|
+
if (!loaded) {
|
|
2776
|
+
spinner.fail("No rapid.json found. Run `rapid init` first.");
|
|
2777
|
+
process.exit(1);
|
|
2778
|
+
}
|
|
2779
|
+
let { config } = loaded;
|
|
2780
|
+
const { rootDir } = loaded;
|
|
2781
|
+
const existingServers = getMcpServers(config);
|
|
2782
|
+
if (existingServers.some((s) => s.name === name)) {
|
|
2783
|
+
spinner.fail(`MCP server '${name}' already exists`);
|
|
2784
|
+
logger9.info(`Use ${logger9.brand(`rapid mcp remove ${name}`)} to remove it first`);
|
|
2785
|
+
process.exit(1);
|
|
2786
|
+
}
|
|
2787
|
+
const template = getMcpTemplate(name);
|
|
2788
|
+
if (template && !options.type && !options.url && !options.command) {
|
|
2789
|
+
config = addMcpServerFromTemplate2(config, name);
|
|
2790
|
+
spinner.text = `Adding '${name}' from template...`;
|
|
2791
|
+
} else if (options.type || options.url || options.command) {
|
|
2792
|
+
const serverConfig = {
|
|
2793
|
+
enabled: true
|
|
2794
|
+
};
|
|
2795
|
+
if (options.type) {
|
|
2796
|
+
serverConfig.type = options.type;
|
|
2797
|
+
}
|
|
2798
|
+
if (options.url) {
|
|
2799
|
+
serverConfig.type = "remote";
|
|
2800
|
+
serverConfig.url = options.url;
|
|
2801
|
+
}
|
|
2802
|
+
if (options.header && Object.keys(options.header).length > 0) {
|
|
2803
|
+
serverConfig.headers = options.header;
|
|
2804
|
+
}
|
|
2805
|
+
if (options.command) {
|
|
2806
|
+
serverConfig.type = "stdio";
|
|
2807
|
+
serverConfig.command = options.command;
|
|
2808
|
+
}
|
|
2809
|
+
if (options.args) {
|
|
2810
|
+
serverConfig.args = options.args.split(",").map((a) => a.trim());
|
|
2811
|
+
}
|
|
2812
|
+
config = addMcpServer(config, name, serverConfig);
|
|
2813
|
+
} else if (template) {
|
|
2814
|
+
config = addMcpServerFromTemplate2(config, name);
|
|
2815
|
+
} else {
|
|
2816
|
+
spinner.fail(`Unknown MCP server template: ${name}`);
|
|
2817
|
+
logger9.info(`Use ${logger9.brand("rapid mcp list --templates")} to see available templates`);
|
|
2818
|
+
logger9.info("Or specify --type, --url, or --command for a custom server");
|
|
2819
|
+
process.exit(1);
|
|
2820
|
+
}
|
|
2821
|
+
await saveConfig(rootDir, config);
|
|
2822
|
+
await writeMcpConfig2(rootDir, config);
|
|
2823
|
+
await writeOpenCodeConfig2(rootDir, config);
|
|
2824
|
+
spinner.succeed(`Added MCP server '${name}'`);
|
|
2825
|
+
console.log();
|
|
2826
|
+
if (template?.requiredSecrets.length) {
|
|
2827
|
+
logger9.info("Required secrets:");
|
|
2828
|
+
for (const secret of template.requiredSecrets) {
|
|
2829
|
+
const ref = template.secretReferences?.[secret];
|
|
2830
|
+
console.log(` ${logger9.brand("\u2022")} ${secret}${ref ? logger9.dim(` (${ref})`) : ""}`);
|
|
2831
|
+
}
|
|
2832
|
+
console.log();
|
|
2833
|
+
logger9.info(`Add these to ${logger9.brand("rapid.json")} secrets.items section`);
|
|
2834
|
+
console.log();
|
|
2835
|
+
}
|
|
2836
|
+
} catch (error) {
|
|
2837
|
+
spinner.fail("Failed to add MCP server");
|
|
2838
|
+
logger9.error(error instanceof Error ? error.message : String(error));
|
|
2839
|
+
process.exit(1);
|
|
2840
|
+
}
|
|
2841
|
+
});
|
|
2842
|
+
mcpCommand.command("remove").description("Remove an MCP server").argument("<name>", "Server name to remove").action(async (name) => {
|
|
2843
|
+
const spinner = ora8(`Removing MCP server '${name}'...`).start();
|
|
2844
|
+
try {
|
|
2845
|
+
const loaded = await loadConfig7();
|
|
2846
|
+
if (!loaded) {
|
|
2847
|
+
spinner.fail("No rapid.json found");
|
|
2848
|
+
process.exit(1);
|
|
2849
|
+
}
|
|
2850
|
+
let { config } = loaded;
|
|
2851
|
+
const { rootDir } = loaded;
|
|
2852
|
+
config = removeMcpServer(config, name);
|
|
2853
|
+
await saveConfig(rootDir, config);
|
|
2854
|
+
await writeMcpConfig2(rootDir, config);
|
|
2855
|
+
await writeOpenCodeConfig2(rootDir, config);
|
|
2856
|
+
spinner.succeed(`Removed MCP server '${name}'`);
|
|
2857
|
+
console.log();
|
|
2858
|
+
} catch (error) {
|
|
2859
|
+
spinner.fail("Failed to remove MCP server");
|
|
2860
|
+
logger9.error(error instanceof Error ? error.message : String(error));
|
|
2861
|
+
process.exit(1);
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
mcpCommand.command("enable").description("Enable a disabled MCP server").argument("<name>", "Server name to enable").action(async (name) => {
|
|
2865
|
+
const spinner = ora8(`Enabling MCP server '${name}'...`).start();
|
|
2866
|
+
try {
|
|
2867
|
+
const loaded = await loadConfig7();
|
|
2868
|
+
if (!loaded) {
|
|
2869
|
+
spinner.fail("No rapid.json found");
|
|
2870
|
+
process.exit(1);
|
|
2871
|
+
}
|
|
2872
|
+
let { config } = loaded;
|
|
2873
|
+
const { rootDir } = loaded;
|
|
2874
|
+
config = enableMcpServer(config, name);
|
|
2875
|
+
await saveConfig(rootDir, config);
|
|
2876
|
+
await writeMcpConfig2(rootDir, config);
|
|
2877
|
+
await writeOpenCodeConfig2(rootDir, config);
|
|
2878
|
+
spinner.succeed(`Enabled MCP server '${name}'`);
|
|
2879
|
+
console.log();
|
|
2880
|
+
} catch (error) {
|
|
2881
|
+
spinner.fail("Failed to enable MCP server");
|
|
2882
|
+
logger9.error(error instanceof Error ? error.message : String(error));
|
|
2883
|
+
process.exit(1);
|
|
2884
|
+
}
|
|
2885
|
+
});
|
|
2886
|
+
mcpCommand.command("disable").description("Disable an MCP server (without removing)").argument("<name>", "Server name to disable").action(async (name) => {
|
|
2887
|
+
const spinner = ora8(`Disabling MCP server '${name}'...`).start();
|
|
2888
|
+
try {
|
|
2889
|
+
const loaded = await loadConfig7();
|
|
2890
|
+
if (!loaded) {
|
|
2891
|
+
spinner.fail("No rapid.json found");
|
|
2892
|
+
process.exit(1);
|
|
2893
|
+
}
|
|
2894
|
+
let { config } = loaded;
|
|
2895
|
+
const { rootDir } = loaded;
|
|
2896
|
+
config = disableMcpServer(config, name);
|
|
2897
|
+
await saveConfig(rootDir, config);
|
|
2898
|
+
await writeMcpConfig2(rootDir, config);
|
|
2899
|
+
await writeOpenCodeConfig2(rootDir, config);
|
|
2900
|
+
spinner.succeed(`Disabled MCP server '${name}'`);
|
|
2901
|
+
console.log();
|
|
2902
|
+
} catch (error) {
|
|
2903
|
+
spinner.fail("Failed to disable MCP server");
|
|
2904
|
+
logger9.error(error instanceof Error ? error.message : String(error));
|
|
2905
|
+
process.exit(1);
|
|
2906
|
+
}
|
|
2907
|
+
});
|
|
2908
|
+
mcpCommand.command("status").description("Show MCP server status").option("--json", "Output as JSON").action(async (options) => {
|
|
2909
|
+
try {
|
|
2910
|
+
const loaded = await loadConfig7();
|
|
2911
|
+
if (!loaded) {
|
|
2912
|
+
logger9.error("No rapid.json found. Run `rapid init` first.");
|
|
2913
|
+
process.exit(1);
|
|
2914
|
+
}
|
|
2915
|
+
const { config } = loaded;
|
|
2916
|
+
const servers = getMcpServerStatus(config);
|
|
2917
|
+
if (options.json) {
|
|
2918
|
+
console.log(JSON.stringify({ servers }, null, 2));
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
console.log();
|
|
2922
|
+
console.log(` ${logger9.brand("MCP Server Status")}`);
|
|
2923
|
+
console.log(` ${logger9.dim("\u2500".repeat(40))}`);
|
|
2924
|
+
console.log();
|
|
2925
|
+
if (servers.length === 0) {
|
|
2926
|
+
console.log(` ${logger9.dim("No MCP servers configured")}`);
|
|
2927
|
+
console.log();
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
let enabledCount = 0;
|
|
2931
|
+
let disabledCount = 0;
|
|
2932
|
+
for (const server of servers) {
|
|
2933
|
+
if (server.enabled) {
|
|
2934
|
+
enabledCount++;
|
|
2935
|
+
} else {
|
|
2936
|
+
disabledCount++;
|
|
2937
|
+
}
|
|
2938
|
+
const icon = server.status === "enabled" ? logger9.brand("\u2713") : server.status === "disabled" ? logger9.dim("\u25CB") : "\u2717";
|
|
2939
|
+
const statusLabel = server.status === "enabled" ? "enabled" : server.status === "disabled" ? logger9.dim("disabled") : logger9.dim(`error: ${server.error}`);
|
|
2940
|
+
const typeLabel = server.type === "remote" ? "remote" : "stdio";
|
|
2941
|
+
console.log(` ${icon} ${server.name}`);
|
|
2942
|
+
console.log(` ${logger9.dim("Type:")} ${typeLabel}`);
|
|
2943
|
+
console.log(` ${logger9.dim("Status:")} ${statusLabel}`);
|
|
2944
|
+
if (server.url) {
|
|
2945
|
+
console.log(` ${logger9.dim("URL:")} ${server.url}`);
|
|
2946
|
+
}
|
|
2947
|
+
if (server.command) {
|
|
2948
|
+
console.log(` ${logger9.dim("Cmd:")} ${server.command}`);
|
|
2949
|
+
}
|
|
2950
|
+
console.log();
|
|
2951
|
+
}
|
|
2952
|
+
console.log(` ${logger9.dim("Summary:")} ${enabledCount} enabled, ${disabledCount} disabled`);
|
|
2953
|
+
console.log();
|
|
2954
|
+
} catch (error) {
|
|
2955
|
+
logger9.error(error instanceof Error ? error.message : String(error));
|
|
2956
|
+
process.exit(1);
|
|
2957
|
+
}
|
|
2958
|
+
});
|
|
2959
|
+
mcpCommand.command("sync").description("Regenerate .mcp.json and opencode.json from rapid.json").action(async () => {
|
|
2960
|
+
const spinner = ora8("Syncing MCP configuration files...").start();
|
|
2961
|
+
try {
|
|
2962
|
+
const loaded = await loadConfig7();
|
|
2963
|
+
if (!loaded) {
|
|
2964
|
+
spinner.fail("No rapid.json found");
|
|
2965
|
+
process.exit(1);
|
|
2966
|
+
}
|
|
2967
|
+
const { config, rootDir } = loaded;
|
|
2968
|
+
await writeMcpConfig2(rootDir, config);
|
|
2969
|
+
await writeOpenCodeConfig2(rootDir, config);
|
|
2970
|
+
const servers = getMcpServers(config);
|
|
2971
|
+
const enabledCount = servers.filter((s) => s.enabled).length;
|
|
2972
|
+
spinner.succeed("MCP configuration synced");
|
|
2973
|
+
console.log();
|
|
2974
|
+
console.log(` ${logger9.dim("Files updated:")}`);
|
|
2975
|
+
console.log(` ${logger9.brand("\u2022")} .mcp.json`);
|
|
2976
|
+
console.log(` ${logger9.brand("\u2022")} opencode.json`);
|
|
2977
|
+
console.log();
|
|
2978
|
+
console.log(` ${logger9.dim("Servers:")} ${enabledCount} enabled`);
|
|
2979
|
+
console.log();
|
|
2980
|
+
} catch (error) {
|
|
2981
|
+
spinner.fail("Failed to sync MCP configuration");
|
|
2982
|
+
logger9.error(error instanceof Error ? error.message : String(error));
|
|
2983
|
+
process.exit(1);
|
|
2984
|
+
}
|
|
2985
|
+
});
|
|
2986
|
+
function collectHeaders(value, previous) {
|
|
2987
|
+
const [name, ...rest] = value.split("=");
|
|
2988
|
+
if (name && rest.length > 0) {
|
|
2989
|
+
previous[name] = rest.join("=");
|
|
2990
|
+
}
|
|
2991
|
+
return previous;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
// src/commands/update.ts
|
|
2995
|
+
import { Command as Command10 } from "commander";
|
|
2996
|
+
|
|
2997
|
+
// src/utils/update-checker.ts
|
|
2998
|
+
import updateNotifier from "update-notifier";
|
|
2999
|
+
import semver from "semver";
|
|
3000
|
+
import { execa as execa3 } from "execa";
|
|
3001
|
+
import { logger as logger10 } from "@a3t/rapid-core";
|
|
3002
|
+
import chalk from "chalk";
|
|
3003
|
+
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
3004
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3005
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
3006
|
+
import prompts from "prompts";
|
|
3007
|
+
var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
3008
|
+
function loadPackageJson() {
|
|
3009
|
+
const paths = [
|
|
3010
|
+
join6(__dirname, "../package.json"),
|
|
3011
|
+
// bundled: dist/ -> package root
|
|
3012
|
+
join6(__dirname, "../../package.json")
|
|
3013
|
+
// source: src/utils/ -> package root
|
|
3014
|
+
];
|
|
3015
|
+
for (const p of paths) {
|
|
3016
|
+
if (existsSync2(p)) {
|
|
3017
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
return { name: "@a3t/rapid", version: "0.0.0" };
|
|
3021
|
+
}
|
|
3022
|
+
var packageJson = loadPackageJson();
|
|
3023
|
+
var UpdateChecker = class {
|
|
3024
|
+
notifier;
|
|
3025
|
+
packageName = packageJson.name;
|
|
3026
|
+
constructor() {
|
|
3027
|
+
this.notifier = updateNotifier({
|
|
3028
|
+
pkg: packageJson,
|
|
3029
|
+
updateCheckInterval: 1e3 * 60 * 60 * 24,
|
|
3030
|
+
// Check daily
|
|
3031
|
+
shouldNotifyInNpmScript: true
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
/**
|
|
3035
|
+
* Check if an update is available
|
|
3036
|
+
*/
|
|
3037
|
+
hasUpdate() {
|
|
3038
|
+
return !!this.notifier.update;
|
|
3039
|
+
}
|
|
3040
|
+
/**
|
|
3041
|
+
* Get update information
|
|
3042
|
+
*/
|
|
3043
|
+
getUpdateInfo() {
|
|
3044
|
+
if (!this.notifier.update) return null;
|
|
3045
|
+
return {
|
|
3046
|
+
current: this.notifier.update.current,
|
|
3047
|
+
latest: this.notifier.update.latest,
|
|
3048
|
+
type: this.getUpdateType(this.notifier.update.current, this.notifier.update.latest)
|
|
3049
|
+
};
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Determine the type of update (major, minor, patch)
|
|
3053
|
+
*/
|
|
3054
|
+
getUpdateType(current, latest) {
|
|
3055
|
+
const diff = semver.diff(current, latest);
|
|
3056
|
+
if (diff === "premajor" || diff === "preminor" || diff === "prepatch") {
|
|
3057
|
+
return "prerelease";
|
|
3058
|
+
}
|
|
3059
|
+
return diff || "patch";
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Show update notification
|
|
3063
|
+
*/
|
|
3064
|
+
showNotification() {
|
|
3065
|
+
if (!this.notifier.update) return;
|
|
3066
|
+
const { current, latest } = this.notifier.update;
|
|
3067
|
+
const updateType = this.getUpdateType(current, latest);
|
|
3068
|
+
logger10.info(`Update available: ${logger10.dim(current)} \u2192 ${chalk.green(latest)}`);
|
|
3069
|
+
if (updateType === "major") {
|
|
3070
|
+
logger10.warn("This is a major version update with breaking changes.");
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Verify package signatures using npm audit signatures
|
|
3075
|
+
*/
|
|
3076
|
+
async verifySignatures() {
|
|
3077
|
+
try {
|
|
3078
|
+
logger10.info("Verifying package signatures...");
|
|
3079
|
+
const result = await execa3("npm", ["audit", "signatures"], {
|
|
3080
|
+
reject: false
|
|
3081
|
+
});
|
|
3082
|
+
if (result.exitCode === 0) {
|
|
3083
|
+
logger10.success("Package signatures verified successfully");
|
|
3084
|
+
return true;
|
|
3085
|
+
} else {
|
|
3086
|
+
logger10.warn("Package signature verification returned warnings");
|
|
3087
|
+
logger10.debug(result.stdout || result.stderr);
|
|
3088
|
+
return true;
|
|
3089
|
+
}
|
|
3090
|
+
} catch {
|
|
3091
|
+
logger10.debug("Signature verification not available (requires npm >= 9)");
|
|
3092
|
+
return true;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
/**
|
|
3096
|
+
* Perform the update with signature verification
|
|
3097
|
+
*/
|
|
3098
|
+
async performUpdate() {
|
|
3099
|
+
try {
|
|
3100
|
+
logger10.info("Updating RAPID CLI...");
|
|
3101
|
+
await execa3("npm", ["install", "-g", `${this.packageName}@latest`], {
|
|
3102
|
+
stdio: "inherit"
|
|
3103
|
+
});
|
|
3104
|
+
await this.verifySignatures();
|
|
3105
|
+
logger10.success("RAPID CLI updated successfully!");
|
|
3106
|
+
logger10.info("Package published with npm provenance - cryptographically verified");
|
|
3107
|
+
return true;
|
|
3108
|
+
} catch (error) {
|
|
3109
|
+
logger10.error("Failed to update RAPID CLI:", error);
|
|
3110
|
+
return false;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
/**
|
|
3114
|
+
* Check for updates and handle them according to version type
|
|
3115
|
+
*/
|
|
3116
|
+
async checkAndUpdate() {
|
|
3117
|
+
if (!this.hasUpdate()) return;
|
|
3118
|
+
const updateInfo = this.getUpdateInfo();
|
|
3119
|
+
if (!updateInfo) return;
|
|
3120
|
+
this.showNotification();
|
|
3121
|
+
if (updateInfo.type === "major") {
|
|
3122
|
+
logger10.warn(
|
|
3123
|
+
`This is a major version update (${updateInfo.current} \u2192 ${updateInfo.latest}) that may contain breaking changes.`
|
|
3124
|
+
);
|
|
3125
|
+
try {
|
|
3126
|
+
const response = await prompts({
|
|
3127
|
+
type: "confirm",
|
|
3128
|
+
name: "shouldUpdate",
|
|
3129
|
+
message: "Would you like to update to this major version?",
|
|
3130
|
+
initial: false
|
|
3131
|
+
});
|
|
3132
|
+
if (response.shouldUpdate) {
|
|
3133
|
+
logger10.info(`Updating to ${updateInfo.latest} (major version)...`);
|
|
3134
|
+
await this.performUpdate();
|
|
3135
|
+
} else {
|
|
3136
|
+
logger10.info("Skipping major version update.");
|
|
3137
|
+
logger10.info('You can update later with "rapid update --force"');
|
|
3138
|
+
}
|
|
3139
|
+
} catch {
|
|
3140
|
+
logger10.info(
|
|
3141
|
+
'Run "rapid update" to update manually, or use "rapid update --force" to update automatically.'
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3146
|
+
logger10.info(`Auto-updating to ${updateInfo.latest} (${updateInfo.type} version)...`);
|
|
3147
|
+
await this.performUpdate();
|
|
3148
|
+
}
|
|
3149
|
+
/**
|
|
3150
|
+
* Force update regardless of version type
|
|
3151
|
+
*/
|
|
3152
|
+
async forceUpdate() {
|
|
3153
|
+
if (!this.hasUpdate()) {
|
|
3154
|
+
logger10.info("No updates available.");
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
const updateInfo = this.getUpdateInfo();
|
|
3158
|
+
if (updateInfo) {
|
|
3159
|
+
this.showNotification();
|
|
3160
|
+
}
|
|
3161
|
+
await this.performUpdate();
|
|
3162
|
+
}
|
|
3163
|
+
};
|
|
3164
|
+
var updateChecker = new UpdateChecker();
|
|
3165
|
+
|
|
3166
|
+
// src/commands/update.ts
|
|
3167
|
+
import { logger as logger11 } from "@a3t/rapid-core";
|
|
3168
|
+
var updateCommand = new Command10("update").description("Check for and apply updates").option("--check", "Check for updates only").option("--force", "Force update even for major versions").action(async (options) => {
|
|
3169
|
+
try {
|
|
3170
|
+
if (options.check) {
|
|
3171
|
+
logger11.header("Checking for updates...");
|
|
3172
|
+
if (!updateChecker.hasUpdate()) {
|
|
3173
|
+
logger11.success("You are using the latest version!");
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
const updateInfo2 = updateChecker.getUpdateInfo();
|
|
3177
|
+
if (updateInfo2) {
|
|
3178
|
+
updateChecker.showNotification();
|
|
3179
|
+
if (updateInfo2.type === "major") {
|
|
3180
|
+
logger11.warn("This is a major version update with breaking changes.");
|
|
3181
|
+
logger11.info('Use "rapid update --force" to update.');
|
|
3182
|
+
} else {
|
|
3183
|
+
logger11.info('Use "rapid update" to apply the update.');
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
if (!updateChecker.hasUpdate()) {
|
|
3189
|
+
logger11.success("You are already using the latest version!");
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
const updateInfo = updateChecker.getUpdateInfo();
|
|
3193
|
+
if (updateInfo && updateInfo.type === "major" && !options.force) {
|
|
3194
|
+
logger11.warn("This is a major version update with breaking changes.");
|
|
3195
|
+
logger11.info("Use --force to update anyway.");
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
await updateChecker.forceUpdate();
|
|
3199
|
+
} catch (error) {
|
|
3200
|
+
logger11.error("Update failed:", error);
|
|
3201
|
+
process.exit(1);
|
|
3202
|
+
}
|
|
3203
|
+
});
|
|
3204
|
+
|
|
3205
|
+
// src/commands/worktree.ts
|
|
3206
|
+
import { Command as Command11 } from "commander";
|
|
3207
|
+
import { logger as logger12 } from "@a3t/rapid-core";
|
|
3208
|
+
import ora9 from "ora";
|
|
3209
|
+
function formatWorktree(wt, currentPath) {
|
|
3210
|
+
const isCurrent = wt.path === currentPath;
|
|
3211
|
+
const marker = isCurrent ? logger12.brand("*") : " ";
|
|
3212
|
+
const status = [];
|
|
3213
|
+
if (wt.isMain) status.push("main");
|
|
3214
|
+
if (wt.locked) status.push("locked");
|
|
3215
|
+
if (wt.prunable) status.push("prunable");
|
|
3216
|
+
if (!wt.exists) status.push("missing");
|
|
3217
|
+
const statusStr = status.length > 0 ? logger12.dim(` (${status.join(", ")})`) : "";
|
|
3218
|
+
const branchStr = wt.branch ? logger12.brand(wt.branch) : logger12.dim("detached");
|
|
3219
|
+
const headShort = wt.head?.substring(0, 7) ?? "";
|
|
3220
|
+
return `${marker} ${branchStr}${statusStr}
|
|
3221
|
+
${logger12.dim(wt.path)}
|
|
3222
|
+
${logger12.dim(`HEAD: ${headShort}`)}`;
|
|
3223
|
+
}
|
|
3224
|
+
var listCommand = new Command11("list").alias("ls").description("List all git worktrees").option("--json", "Output as JSON").action(async (options) => {
|
|
3225
|
+
try {
|
|
3226
|
+
const cwd = process.cwd();
|
|
3227
|
+
if (!await isGitRepo(cwd)) {
|
|
3228
|
+
logger12.error("Not a git repository");
|
|
3229
|
+
process.exit(1);
|
|
3230
|
+
}
|
|
3231
|
+
const gitRoot = await getGitRoot(cwd);
|
|
3232
|
+
const worktrees = await listWorktrees(gitRoot);
|
|
3233
|
+
if (options.json) {
|
|
3234
|
+
console.log(JSON.stringify(worktrees, null, 2));
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
if (worktrees.length === 0) {
|
|
3238
|
+
logger12.info("No worktrees found");
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
logger12.header("Git Worktrees");
|
|
3242
|
+
console.log();
|
|
3243
|
+
for (const wt of worktrees) {
|
|
3244
|
+
console.log(formatWorktree(wt, gitRoot));
|
|
3245
|
+
console.log();
|
|
3246
|
+
}
|
|
3247
|
+
const prunable = worktrees.filter((wt) => wt.prunable);
|
|
3248
|
+
if (prunable.length > 0) {
|
|
3249
|
+
logger12.warn(`${prunable.length} worktree(s) can be pruned. Run: rapid worktree prune`);
|
|
3250
|
+
}
|
|
3251
|
+
} catch (error) {
|
|
3252
|
+
logger12.error(error instanceof Error ? error.message : String(error));
|
|
3253
|
+
process.exit(1);
|
|
3254
|
+
}
|
|
3255
|
+
});
|
|
3256
|
+
var pruneCommand = new Command11("prune").description("Remove stale worktree references").option("--dry-run", "Show what would be pruned without removing").action(async (options) => {
|
|
3257
|
+
const spinner = ora9("Checking worktrees...").start();
|
|
3258
|
+
try {
|
|
3259
|
+
const cwd = process.cwd();
|
|
3260
|
+
if (!await isGitRepo(cwd)) {
|
|
3261
|
+
spinner.fail("Not a git repository");
|
|
3262
|
+
process.exit(1);
|
|
3263
|
+
}
|
|
3264
|
+
const gitRoot = await getGitRoot(cwd);
|
|
3265
|
+
const worktrees = await listWorktrees(gitRoot);
|
|
3266
|
+
const prunable = worktrees.filter((wt) => wt.prunable);
|
|
3267
|
+
if (prunable.length === 0) {
|
|
3268
|
+
spinner.succeed("No stale worktrees to prune");
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
if (options.dryRun) {
|
|
3272
|
+
spinner.info(`Would prune ${prunable.length} worktree(s):`);
|
|
3273
|
+
for (const wt of prunable) {
|
|
3274
|
+
console.log(` ${logger12.dim("\u2022")} ${wt.path}`);
|
|
3275
|
+
}
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
spinner.text = `Pruning ${prunable.length} worktree(s)...`;
|
|
3279
|
+
const result = await pruneWorktrees(gitRoot);
|
|
3280
|
+
if (result.success) {
|
|
3281
|
+
spinner.succeed(`Pruned ${result.pruned.length} worktree(s)`);
|
|
3282
|
+
for (const path of result.pruned) {
|
|
3283
|
+
console.log(` ${logger12.dim("\u2022")} ${path}`);
|
|
3284
|
+
}
|
|
3285
|
+
} else {
|
|
3286
|
+
spinner.fail(`Failed to prune: ${result.error}`);
|
|
3287
|
+
process.exit(1);
|
|
3288
|
+
}
|
|
3289
|
+
} catch (error) {
|
|
3290
|
+
spinner.fail(error instanceof Error ? error.message : String(error));
|
|
3291
|
+
process.exit(1);
|
|
3292
|
+
}
|
|
3293
|
+
});
|
|
3294
|
+
var removeCommand = new Command11("remove").alias("rm").description("Remove a worktree").argument("<path-or-branch>", "Worktree path or branch name").option("-f, --force", "Force removal even if worktree is dirty").action(async (pathOrBranch, options) => {
|
|
3295
|
+
const spinner = ora9("Finding worktree...").start();
|
|
3296
|
+
try {
|
|
3297
|
+
const cwd = process.cwd();
|
|
3298
|
+
if (!await isGitRepo(cwd)) {
|
|
3299
|
+
spinner.fail("Not a git repository");
|
|
3300
|
+
process.exit(1);
|
|
3301
|
+
}
|
|
3302
|
+
const gitRoot = await getGitRoot(cwd);
|
|
3303
|
+
const worktrees = await listWorktrees(gitRoot);
|
|
3304
|
+
const worktree = worktrees.find(
|
|
3305
|
+
(wt) => wt.path === pathOrBranch || wt.path.endsWith(pathOrBranch) || wt.branch === pathOrBranch
|
|
3306
|
+
);
|
|
3307
|
+
if (!worktree) {
|
|
3308
|
+
spinner.fail(`Worktree not found: ${pathOrBranch}`);
|
|
3309
|
+
logger12.info("Available worktrees:");
|
|
3310
|
+
for (const wt of worktrees) {
|
|
3311
|
+
console.log(` ${wt.branch || wt.path}`);
|
|
3312
|
+
}
|
|
3313
|
+
process.exit(1);
|
|
3314
|
+
}
|
|
3315
|
+
if (worktree.isMain) {
|
|
3316
|
+
spinner.fail("Cannot remove the main worktree");
|
|
3317
|
+
process.exit(1);
|
|
3318
|
+
}
|
|
3319
|
+
if (worktree.locked && !options.force) {
|
|
3320
|
+
spinner.fail("Worktree is locked. Use --force to remove anyway.");
|
|
3321
|
+
process.exit(1);
|
|
3322
|
+
}
|
|
3323
|
+
spinner.text = `Removing worktree: ${worktree.path}...`;
|
|
3324
|
+
const result = await removeWorktree(gitRoot, worktree.path, { force: options.force });
|
|
3325
|
+
if (result.success) {
|
|
3326
|
+
spinner.succeed(`Removed worktree: ${worktree.path}`);
|
|
3327
|
+
} else {
|
|
3328
|
+
spinner.fail(`Failed to remove: ${result.error}`);
|
|
3329
|
+
process.exit(1);
|
|
3330
|
+
}
|
|
3331
|
+
} catch (error) {
|
|
3332
|
+
spinner.fail(error instanceof Error ? error.message : String(error));
|
|
3333
|
+
process.exit(1);
|
|
3334
|
+
}
|
|
3335
|
+
});
|
|
3336
|
+
var cleanupCommand = new Command11("cleanup").description("Remove worktrees for branches that have been merged").option("--dry-run", "Show what would be removed without removing").action(async (options) => {
|
|
3337
|
+
const spinner = ora9("Analyzing worktrees...").start();
|
|
3338
|
+
try {
|
|
3339
|
+
const cwd = process.cwd();
|
|
3340
|
+
if (!await isGitRepo(cwd)) {
|
|
3341
|
+
spinner.fail("Not a git repository");
|
|
3342
|
+
process.exit(1);
|
|
3343
|
+
}
|
|
3344
|
+
const gitRoot = await getGitRoot(cwd);
|
|
3345
|
+
if (options.dryRun) {
|
|
3346
|
+
const worktrees = await listWorktrees(gitRoot);
|
|
3347
|
+
spinner.info("Dry run - would remove worktrees for merged branches");
|
|
3348
|
+
const nonMain = worktrees.filter((wt) => !wt.isMain && wt.branch);
|
|
3349
|
+
if (nonMain.length === 0) {
|
|
3350
|
+
console.log(" No feature branch worktrees found");
|
|
3351
|
+
} else {
|
|
3352
|
+
console.log(" Feature branch worktrees:");
|
|
3353
|
+
for (const wt of nonMain) {
|
|
3354
|
+
console.log(` ${logger12.dim("\u2022")} ${wt.branch} - ${wt.path}`);
|
|
3355
|
+
}
|
|
3356
|
+
console.log();
|
|
3357
|
+
logger12.info("Run without --dry-run to remove worktrees for merged branches");
|
|
3358
|
+
}
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
spinner.text = "Removing worktrees for merged branches...";
|
|
3362
|
+
const result = await cleanupMergedWorktrees(gitRoot);
|
|
3363
|
+
if (result.removed.length === 0) {
|
|
3364
|
+
spinner.succeed("No worktrees to clean up");
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
spinner.succeed(`Cleaned up ${result.removed.length} worktree(s)`);
|
|
3368
|
+
for (const path of result.removed) {
|
|
3369
|
+
console.log(` ${logger12.dim("\u2022")} ${path}`);
|
|
3370
|
+
}
|
|
3371
|
+
if (result.errors.length > 0) {
|
|
3372
|
+
console.log();
|
|
3373
|
+
logger12.warn("Some worktrees could not be removed:");
|
|
3374
|
+
for (const err of result.errors) {
|
|
3375
|
+
console.log(` ${logger12.dim("\u2022")} ${err}`);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
} catch (error) {
|
|
3379
|
+
spinner.fail(error instanceof Error ? error.message : String(error));
|
|
3380
|
+
process.exit(1);
|
|
3381
|
+
}
|
|
3382
|
+
});
|
|
3383
|
+
var worktreeCommand = new Command11("worktree").alias("wt").description("Manage git worktrees for isolated development").addCommand(listCommand).addCommand(pruneCommand).addCommand(removeCommand).addCommand(cleanupCommand);
|
|
3384
|
+
worktreeCommand.action(async () => {
|
|
3385
|
+
await listCommand.parseAsync([], { from: "user" });
|
|
3386
|
+
});
|
|
3387
|
+
|
|
3388
|
+
// src/commands/lima.ts
|
|
3389
|
+
import { Command as Command12 } from "commander";
|
|
3390
|
+
import { logger as logger13 } from "@a3t/rapid-core";
|
|
3391
|
+
import ora10 from "ora";
|
|
3392
|
+
async function checkLimaAvailable() {
|
|
3393
|
+
if (!isMacOS()) {
|
|
3394
|
+
logger13.error("Lima is only available on macOS");
|
|
3395
|
+
return false;
|
|
3396
|
+
}
|
|
3397
|
+
if (!await hasLima()) {
|
|
3398
|
+
logger13.error("Lima is not installed");
|
|
3399
|
+
logger13.blank();
|
|
3400
|
+
logger13.info("Install Lima with:");
|
|
3401
|
+
console.log(` ${logger13.dim("$")} brew install lima`);
|
|
3402
|
+
logger13.blank();
|
|
3403
|
+
logger13.info("For more information: https://lima-vm.io");
|
|
3404
|
+
return false;
|
|
3405
|
+
}
|
|
3406
|
+
return true;
|
|
3407
|
+
}
|
|
3408
|
+
var statusCommand2 = new Command12("status").description("Show Lima VM status").option("--json", "Output as JSON").action(async (options) => {
|
|
3409
|
+
if (!await checkLimaAvailable()) {
|
|
3410
|
+
process.exit(1);
|
|
3411
|
+
}
|
|
3412
|
+
const instance = await getInstance();
|
|
3413
|
+
if (options.json) {
|
|
3414
|
+
console.log(JSON.stringify(instance, null, 2));
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
if (!instance) {
|
|
3418
|
+
logger13.info(`Lima VM (${RAPID_LIMA_INSTANCE}) is not created`);
|
|
3419
|
+
logger13.blank();
|
|
3420
|
+
logger13.info("Start the VM with:");
|
|
3421
|
+
console.log(` ${logger13.dim("$")} rapid lima start`);
|
|
3422
|
+
logger13.blank();
|
|
3423
|
+
logger13.info("Or use:");
|
|
3424
|
+
console.log(` ${logger13.dim("$")} rapid dev --local`);
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
logger13.header("Lima VM Status");
|
|
3428
|
+
console.log();
|
|
3429
|
+
console.log(` ${logger13.dim("Name:")} ${instance.name}`);
|
|
3430
|
+
console.log(
|
|
3431
|
+
` ${logger13.dim("Status:")} ${instance.status === "Running" ? logger13.brand(instance.status) : instance.status}`
|
|
3432
|
+
);
|
|
3433
|
+
console.log(` ${logger13.dim("Arch:")} ${instance.arch}`);
|
|
3434
|
+
console.log(` ${logger13.dim("CPUs:")} ${instance.cpus}`);
|
|
3435
|
+
console.log(` ${logger13.dim("Memory:")} ${instance.memory}`);
|
|
3436
|
+
console.log(` ${logger13.dim("Disk:")} ${instance.disk}`);
|
|
3437
|
+
if (instance.sshLocalPort) {
|
|
3438
|
+
console.log(` ${logger13.dim("SSH Port:")} ${instance.sshLocalPort}`);
|
|
3439
|
+
}
|
|
3440
|
+
console.log();
|
|
3441
|
+
if (instance.status === "Running") {
|
|
3442
|
+
logger13.info("To open a shell:");
|
|
3443
|
+
console.log(` ${logger13.dim("$")} rapid lima shell`);
|
|
3444
|
+
} else {
|
|
3445
|
+
logger13.info("To start the VM:");
|
|
3446
|
+
console.log(` ${logger13.dim("$")} rapid lima start`);
|
|
3447
|
+
}
|
|
3448
|
+
console.log();
|
|
3449
|
+
});
|
|
3450
|
+
var startCommand2 = new Command12("start").description("Start the Lima VM").option("--cpus <n>", "Number of CPUs", "4").option("--memory <size>", "Memory size", "8GiB").option("--disk <size>", "Disk size", "50GiB").action(async (options) => {
|
|
3451
|
+
if (!await checkLimaAvailable()) {
|
|
3452
|
+
process.exit(1);
|
|
3453
|
+
}
|
|
3454
|
+
const spinner = ora10("Starting Lima VM...").start();
|
|
3455
|
+
const projectDir = process.cwd();
|
|
3456
|
+
const result = await startInstance(projectDir, {
|
|
3457
|
+
cpus: parseInt(options.cpus, 10),
|
|
3458
|
+
memory: options.memory,
|
|
3459
|
+
disk: options.disk,
|
|
3460
|
+
timeout: 600
|
|
3461
|
+
});
|
|
3462
|
+
if (!result.success) {
|
|
3463
|
+
spinner.fail("Failed to start Lima VM");
|
|
3464
|
+
logger13.error(result.error ?? "Unknown error");
|
|
3465
|
+
process.exit(1);
|
|
3466
|
+
}
|
|
3467
|
+
spinner.succeed("Lima VM started");
|
|
3468
|
+
logger13.blank();
|
|
3469
|
+
const sshSpinner = ora10("Checking SSH agent forwarding...").start();
|
|
3470
|
+
const sshResult = await setupGitSsh();
|
|
3471
|
+
if (sshResult.success) {
|
|
3472
|
+
sshSpinner.succeed("SSH agent forwarding is working");
|
|
3473
|
+
} else {
|
|
3474
|
+
sshSpinner.warn("SSH agent forwarding may not be working");
|
|
3475
|
+
logger13.dim(sshResult.error ?? "Make sure ssh-agent is running on the host");
|
|
3476
|
+
}
|
|
3477
|
+
logger13.blank();
|
|
3478
|
+
logger13.info("To open a shell:");
|
|
3479
|
+
console.log(` ${logger13.dim("$")} rapid lima shell`);
|
|
3480
|
+
console.log();
|
|
3481
|
+
});
|
|
3482
|
+
var stopCommand2 = new Command12("stop").description("Stop the Lima VM").option("-f, --force", "Force stop").action(async (options) => {
|
|
3483
|
+
if (!await checkLimaAvailable()) {
|
|
3484
|
+
process.exit(1);
|
|
3485
|
+
}
|
|
3486
|
+
const instance = await getInstance();
|
|
3487
|
+
if (!instance) {
|
|
3488
|
+
logger13.info("Lima VM is not created");
|
|
3489
|
+
return;
|
|
3490
|
+
}
|
|
3491
|
+
if (instance.status !== "Running") {
|
|
3492
|
+
logger13.info("Lima VM is already stopped");
|
|
3493
|
+
return;
|
|
3494
|
+
}
|
|
3495
|
+
const spinner = ora10("Stopping Lima VM...").start();
|
|
3496
|
+
const result = await stopInstance(RAPID_LIMA_INSTANCE, {
|
|
3497
|
+
force: options.force
|
|
3498
|
+
});
|
|
3499
|
+
if (!result.success) {
|
|
3500
|
+
spinner.fail("Failed to stop Lima VM");
|
|
3501
|
+
logger13.error(result.error ?? "Unknown error");
|
|
3502
|
+
process.exit(1);
|
|
3503
|
+
}
|
|
3504
|
+
spinner.succeed("Lima VM stopped");
|
|
3505
|
+
});
|
|
3506
|
+
var shellCommand = new Command12("shell").description("Open a shell in the Lima VM").option("-c, --command <cmd>", "Command to run instead of interactive shell").action(async (options) => {
|
|
3507
|
+
if (!await checkLimaAvailable()) {
|
|
3508
|
+
process.exit(1);
|
|
3509
|
+
}
|
|
3510
|
+
const instance = await getInstance();
|
|
3511
|
+
if (!instance || instance.status !== "Running") {
|
|
3512
|
+
logger13.error("Lima VM is not running");
|
|
3513
|
+
logger13.info("Start with: rapid lima start");
|
|
3514
|
+
process.exit(1);
|
|
3515
|
+
}
|
|
3516
|
+
await shellInLima({
|
|
3517
|
+
cwd: process.cwd(),
|
|
3518
|
+
command: options.command
|
|
3519
|
+
});
|
|
3520
|
+
});
|
|
3521
|
+
var deleteCommand = new Command12("delete").description("Delete the Lima VM").option("-f, --force", "Force delete without confirmation").action(async (options) => {
|
|
3522
|
+
if (!await checkLimaAvailable()) {
|
|
3523
|
+
process.exit(1);
|
|
3524
|
+
}
|
|
3525
|
+
const instance = await getInstance();
|
|
3526
|
+
if (!instance) {
|
|
3527
|
+
logger13.info("Lima VM does not exist");
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
if (!options.force) {
|
|
3531
|
+
logger13.warn("This will permanently delete the Lima VM and all its data.");
|
|
3532
|
+
logger13.info(`Use ${logger13.brand("--force")} to confirm deletion.`);
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
const spinner = ora10("Deleting Lima VM...").start();
|
|
3536
|
+
const result = await deleteInstance(RAPID_LIMA_INSTANCE, { force: true });
|
|
3537
|
+
if (!result.success) {
|
|
3538
|
+
spinner.fail("Failed to delete Lima VM");
|
|
3539
|
+
logger13.error(result.error ?? "Unknown error");
|
|
3540
|
+
process.exit(1);
|
|
3541
|
+
}
|
|
3542
|
+
spinner.succeed("Lima VM deleted");
|
|
3543
|
+
});
|
|
3544
|
+
var listCommand2 = new Command12("list").alias("ls").description("List all Lima instances").option("--json", "Output as JSON").action(async (options) => {
|
|
3545
|
+
if (!await checkLimaAvailable()) {
|
|
3546
|
+
process.exit(1);
|
|
3547
|
+
}
|
|
3548
|
+
const instances = await listInstances();
|
|
3549
|
+
if (options.json) {
|
|
3550
|
+
console.log(JSON.stringify(instances, null, 2));
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
if (instances.length === 0) {
|
|
3554
|
+
logger13.info("No Lima instances found");
|
|
3555
|
+
return;
|
|
3556
|
+
}
|
|
3557
|
+
logger13.header("Lima Instances");
|
|
3558
|
+
console.log();
|
|
3559
|
+
for (const inst of instances) {
|
|
3560
|
+
const isRapid = inst.name === RAPID_LIMA_INSTANCE;
|
|
3561
|
+
const statusColor = inst.status === "Running" ? logger13.brand : logger13.dim;
|
|
3562
|
+
console.log(
|
|
3563
|
+
` ${isRapid ? logger13.brand("*") : " "} ${inst.name} ${statusColor(`(${inst.status})`)}`
|
|
3564
|
+
);
|
|
3565
|
+
console.log(` ${logger13.dim(`${inst.cpus} CPUs, ${inst.memory}, ${inst.disk}`)}`);
|
|
3566
|
+
}
|
|
3567
|
+
console.log();
|
|
3568
|
+
});
|
|
3569
|
+
var limaCommand = new Command12("lima").description("Manage Lima VM for local development (macOS)").addCommand(statusCommand2).addCommand(startCommand2).addCommand(stopCommand2).addCommand(shellCommand).addCommand(deleteCommand).addCommand(listCommand2);
|
|
3570
|
+
limaCommand.action(async () => {
|
|
3571
|
+
await statusCommand2.parseAsync([], { from: "user" });
|
|
3572
|
+
});
|
|
3573
|
+
|
|
3574
|
+
// src/index.ts
|
|
3575
|
+
var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
|
|
3576
|
+
var packageJson2 = JSON.parse(readFileSync2(join7(__dirname2, "../package.json"), "utf-8"));
|
|
3577
|
+
var VERSION = packageJson2.version;
|
|
3578
|
+
var program = new Command13();
|
|
3579
|
+
program.name("rapid").description("AI-assisted development with dev containers").version(VERSION, "-v, --version", "Show version").option("--verbose", "Verbose output").option("-q, --quiet", "Minimal output").option("--config <path>", "Path to rapid.json").hook("preAction", async (thisCommand) => {
|
|
3580
|
+
const opts = thisCommand.opts();
|
|
3581
|
+
if (opts.verbose) {
|
|
3582
|
+
setLogLevel("debug");
|
|
3583
|
+
} else if (opts.quiet) {
|
|
3584
|
+
setLogLevel("error");
|
|
3585
|
+
}
|
|
3586
|
+
if (thisCommand.name() === "update" || thisCommand.name() === "version") {
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
try {
|
|
3590
|
+
await updateChecker.checkAndUpdate();
|
|
3591
|
+
} catch (error) {
|
|
3592
|
+
logger14.debug("Update check failed:", error);
|
|
3593
|
+
}
|
|
3594
|
+
});
|
|
3595
|
+
program.addCommand(initCommand);
|
|
3596
|
+
program.addCommand(startCommand);
|
|
3597
|
+
program.addCommand(devCommand);
|
|
3598
|
+
program.addCommand(stopCommand);
|
|
3599
|
+
program.addCommand(statusCommand);
|
|
3600
|
+
program.addCommand(agentCommand);
|
|
3601
|
+
program.addCommand(secretsCommand);
|
|
3602
|
+
program.addCommand(authCommand);
|
|
3603
|
+
program.addCommand(mcpCommand);
|
|
3604
|
+
program.addCommand(updateCommand);
|
|
3605
|
+
program.addCommand(worktreeCommand);
|
|
3606
|
+
program.addCommand(limaCommand);
|
|
3607
|
+
program.action(() => {
|
|
3608
|
+
console.log();
|
|
3609
|
+
console.log(` ${logger14.brand("RAPID")} ${logger14.dim(`v${VERSION}`)}`);
|
|
3610
|
+
console.log(` ${logger14.dim("AI-assisted development with dev containers")}`);
|
|
3611
|
+
console.log();
|
|
3612
|
+
program.help();
|
|
3613
|
+
});
|
|
3614
|
+
|
|
3615
|
+
export {
|
|
3616
|
+
program
|
|
3617
|
+
};
|
|
3618
|
+
//# sourceMappingURL=chunk-VWXN2TG5.js.map
|