ralphctl 0.1.0 → 0.1.2
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 +58 -24
- package/dist/add-HGJCLWED.mjs +14 -0
- package/dist/add-MRGCS3US.mjs +14 -0
- package/dist/chunk-6PYTKGB5.mjs +316 -0
- package/dist/chunk-7TG3EAQ2.mjs +20 -0
- package/dist/chunk-EKMZZRWI.mjs +521 -0
- package/dist/chunk-JON4GCLR.mjs +59 -0
- package/dist/chunk-LOR7QBXX.mjs +3683 -0
- package/dist/chunk-MNMQC36F.mjs +556 -0
- package/dist/chunk-MRKOFVTM.mjs +537 -0
- package/dist/chunk-NTWO2LXB.mjs +52 -0
- package/dist/chunk-QBXHAXHI.mjs +562 -0
- package/dist/chunk-WGHJI3OI.mjs +214 -0
- package/dist/cli.mjs +4245 -0
- package/dist/create-MG7E7PLQ.mjs +10 -0
- package/dist/handle-UG5M2OON.mjs +22 -0
- package/dist/multiline-OHSNFCRG.mjs +40 -0
- package/dist/project-NT3L4FTB.mjs +28 -0
- package/dist/resolver-WSFWKACM.mjs +153 -0
- package/dist/sprint-4VHDLGFN.mjs +37 -0
- package/dist/wizard-LRELAN2J.mjs +196 -0
- package/package.json +19 -28
- package/CHANGELOG.md +0 -94
- package/bin/ralphctl +0 -13
- package/src/ai/executor.ts +0 -973
- package/src/ai/lifecycle.ts +0 -45
- package/src/ai/parser.ts +0 -40
- package/src/ai/permissions.ts +0 -207
- package/src/ai/process-manager.ts +0 -248
- package/src/ai/prompts/index.ts +0 -89
- package/src/ai/rate-limiter.ts +0 -89
- package/src/ai/runner.ts +0 -478
- package/src/ai/session.ts +0 -319
- package/src/ai/task-context.ts +0 -270
- package/src/cli-metadata.ts +0 -7
- package/src/cli.ts +0 -65
- package/src/commands/completion/index.ts +0 -33
- package/src/commands/config/config.ts +0 -58
- package/src/commands/config/index.ts +0 -33
- package/src/commands/dashboard/dashboard.ts +0 -5
- package/src/commands/dashboard/index.ts +0 -6
- package/src/commands/doctor/doctor.ts +0 -271
- package/src/commands/doctor/index.ts +0 -25
- package/src/commands/progress/index.ts +0 -25
- package/src/commands/progress/log.ts +0 -64
- package/src/commands/progress/show.ts +0 -14
- package/src/commands/project/add.ts +0 -336
- package/src/commands/project/index.ts +0 -104
- package/src/commands/project/list.ts +0 -31
- package/src/commands/project/remove.ts +0 -43
- package/src/commands/project/repo.ts +0 -118
- package/src/commands/project/show.ts +0 -49
- package/src/commands/sprint/close.ts +0 -180
- package/src/commands/sprint/context.ts +0 -109
- package/src/commands/sprint/create.ts +0 -60
- package/src/commands/sprint/current.ts +0 -75
- package/src/commands/sprint/delete.ts +0 -72
- package/src/commands/sprint/health.ts +0 -229
- package/src/commands/sprint/ideate.ts +0 -496
- package/src/commands/sprint/index.ts +0 -226
- package/src/commands/sprint/list.ts +0 -86
- package/src/commands/sprint/plan-utils.ts +0 -207
- package/src/commands/sprint/plan.ts +0 -549
- package/src/commands/sprint/refine.ts +0 -359
- package/src/commands/sprint/requirements.ts +0 -58
- package/src/commands/sprint/show.ts +0 -140
- package/src/commands/sprint/start.ts +0 -119
- package/src/commands/sprint/switch.ts +0 -20
- package/src/commands/task/add.ts +0 -316
- package/src/commands/task/import.ts +0 -150
- package/src/commands/task/index.ts +0 -123
- package/src/commands/task/list.ts +0 -145
- package/src/commands/task/next.ts +0 -45
- package/src/commands/task/remove.ts +0 -47
- package/src/commands/task/reorder.ts +0 -45
- package/src/commands/task/show.ts +0 -111
- package/src/commands/task/status.ts +0 -99
- package/src/commands/ticket/add.ts +0 -265
- package/src/commands/ticket/edit.ts +0 -166
- package/src/commands/ticket/index.ts +0 -114
- package/src/commands/ticket/list.ts +0 -128
- package/src/commands/ticket/refine-utils.ts +0 -89
- package/src/commands/ticket/refine.ts +0 -268
- package/src/commands/ticket/remove.ts +0 -48
- package/src/commands/ticket/show.ts +0 -74
- package/src/completion/handle.ts +0 -30
- package/src/completion/resolver.ts +0 -241
- package/src/interactive/dashboard.ts +0 -268
- package/src/interactive/escapable.ts +0 -81
- package/src/interactive/file-browser.ts +0 -153
- package/src/interactive/index.ts +0 -429
- package/src/interactive/menu.ts +0 -403
- package/src/interactive/selectors.ts +0 -273
- package/src/interactive/wizard.ts +0 -221
- package/src/providers/claude.ts +0 -53
- package/src/providers/copilot.ts +0 -86
- package/src/providers/index.ts +0 -43
- package/src/providers/types.ts +0 -85
- package/src/schemas/index.ts +0 -130
- package/src/store/config.ts +0 -74
- package/src/store/progress.ts +0 -230
- package/src/store/project.ts +0 -276
- package/src/store/sprint.ts +0 -229
- package/src/store/task.ts +0 -443
- package/src/store/ticket.ts +0 -178
- package/src/theme/index.ts +0 -215
- package/src/theme/ui.ts +0 -872
- package/src/utils/detect-scripts.ts +0 -247
- package/src/utils/editor-input.ts +0 -41
- package/src/utils/editor.ts +0 -37
- package/src/utils/exit-codes.ts +0 -27
- package/src/utils/file-lock.ts +0 -135
- package/src/utils/git.ts +0 -185
- package/src/utils/ids.ts +0 -37
- package/src/utils/issue-fetch.ts +0 -244
- package/src/utils/json-extract.ts +0 -62
- package/src/utils/multiline.ts +0 -61
- package/src/utils/path-selector.ts +0 -236
- package/src/utils/paths.ts +0 -108
- package/src/utils/provider.ts +0 -34
- package/src/utils/requirements-export.ts +0 -63
- package/src/utils/storage.ts +0 -107
- package/tsconfig.json +0 -25
- /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
- /package/{src/ai → dist}/prompts/ideate.md +0 -0
- /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
- /package/{src/ai → dist}/prompts/plan-common.md +0 -0
- /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
- /package/{src/ai → dist}/prompts/task-execution.md +0 -0
- /package/{src/ai → dist}/prompts/ticket-refine.md +0 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
escapableSelect
|
|
4
|
+
} from "./chunk-NTWO2LXB.mjs";
|
|
5
|
+
import {
|
|
6
|
+
EXIT_ERROR,
|
|
7
|
+
exitWithCode
|
|
8
|
+
} from "./chunk-7TG3EAQ2.mjs";
|
|
9
|
+
import {
|
|
10
|
+
ProjectExistsError,
|
|
11
|
+
createProject
|
|
12
|
+
} from "./chunk-WGHJI3OI.mjs";
|
|
13
|
+
import {
|
|
14
|
+
expandTilde,
|
|
15
|
+
validateProjectPath
|
|
16
|
+
} from "./chunk-6PYTKGB5.mjs";
|
|
17
|
+
import {
|
|
18
|
+
createSpinner,
|
|
19
|
+
emoji,
|
|
20
|
+
error,
|
|
21
|
+
field,
|
|
22
|
+
log,
|
|
23
|
+
muted,
|
|
24
|
+
showError,
|
|
25
|
+
showNextStep,
|
|
26
|
+
showSuccess,
|
|
27
|
+
showTip,
|
|
28
|
+
showWarning
|
|
29
|
+
} from "./chunk-QBXHAXHI.mjs";
|
|
30
|
+
|
|
31
|
+
// src/commands/project/add.ts
|
|
32
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
33
|
+
import { basename, join as join3, resolve as resolve2 } from "path";
|
|
34
|
+
import { input, select } from "@inquirer/prompts";
|
|
35
|
+
|
|
36
|
+
// src/interactive/file-browser.ts
|
|
37
|
+
import { readdirSync, statSync } from "fs";
|
|
38
|
+
import { homedir } from "os";
|
|
39
|
+
import { dirname, join, resolve } from "path";
|
|
40
|
+
function listDirectories(dirPath) {
|
|
41
|
+
try {
|
|
42
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
43
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function hasSubdirectories(dirPath) {
|
|
49
|
+
try {
|
|
50
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
51
|
+
return entries.some((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isGitRepo(dirPath) {
|
|
57
|
+
try {
|
|
58
|
+
const gitDir = join(dirPath, ".git");
|
|
59
|
+
return statSync(gitDir).isDirectory();
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function browseDirectory(message = "Browse to directory:", startPath) {
|
|
65
|
+
let currentPath = startPath ? resolve(startPath) : homedir();
|
|
66
|
+
while (true) {
|
|
67
|
+
const dirs = listDirectories(currentPath);
|
|
68
|
+
const choices = [];
|
|
69
|
+
choices.push({
|
|
70
|
+
name: `${emoji.donut} Select this directory`,
|
|
71
|
+
value: "__SELECT__",
|
|
72
|
+
description: currentPath
|
|
73
|
+
});
|
|
74
|
+
const parentDir = dirname(currentPath);
|
|
75
|
+
if (parentDir !== currentPath) {
|
|
76
|
+
choices.push({
|
|
77
|
+
name: "\u2191 Parent directory",
|
|
78
|
+
value: "__PARENT__",
|
|
79
|
+
description: parentDir
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (currentPath !== homedir()) {
|
|
83
|
+
choices.push({
|
|
84
|
+
name: "\u2302 Home directory",
|
|
85
|
+
value: "__HOME__",
|
|
86
|
+
description: homedir()
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
for (const dir of dirs) {
|
|
90
|
+
const fullPath = join(currentPath, dir);
|
|
91
|
+
const hasChildren = hasSubdirectories(fullPath);
|
|
92
|
+
const isRepo = isGitRepo(fullPath);
|
|
93
|
+
let icon = " ";
|
|
94
|
+
if (isRepo) {
|
|
95
|
+
icon = "\u2699 ";
|
|
96
|
+
} else if (hasChildren) {
|
|
97
|
+
icon = "\u25B8 ";
|
|
98
|
+
}
|
|
99
|
+
choices.push({
|
|
100
|
+
name: `${icon}${dir}`,
|
|
101
|
+
value: fullPath,
|
|
102
|
+
description: isRepo ? "git repo" : void 0
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
choices.push({
|
|
106
|
+
name: muted("Cancel"),
|
|
107
|
+
value: "__CANCEL__"
|
|
108
|
+
});
|
|
109
|
+
try {
|
|
110
|
+
const selected = await escapableSelect({
|
|
111
|
+
message: `${emoji.donut} ${message}
|
|
112
|
+
${muted(currentPath)}`,
|
|
113
|
+
choices,
|
|
114
|
+
pageSize: 15,
|
|
115
|
+
loop: false
|
|
116
|
+
});
|
|
117
|
+
if (selected === null) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
switch (selected) {
|
|
121
|
+
case "__SELECT__":
|
|
122
|
+
return currentPath;
|
|
123
|
+
case "__PARENT__":
|
|
124
|
+
currentPath = parentDir;
|
|
125
|
+
break;
|
|
126
|
+
case "__HOME__":
|
|
127
|
+
currentPath = homedir();
|
|
128
|
+
break;
|
|
129
|
+
case "__CANCEL__":
|
|
130
|
+
return null;
|
|
131
|
+
default:
|
|
132
|
+
currentPath = selected;
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (err.name === "ExitPromptError") {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/utils/detect-scripts.ts
|
|
144
|
+
import { existsSync, readFileSync } from "fs";
|
|
145
|
+
import { join as join2 } from "path";
|
|
146
|
+
function detectNodePackageManager(projectPath) {
|
|
147
|
+
if (existsSync(join2(projectPath, "pnpm-lock.yaml"))) return "pnpm";
|
|
148
|
+
if (existsSync(join2(projectPath, "yarn.lock"))) return "yarn";
|
|
149
|
+
return "npm";
|
|
150
|
+
}
|
|
151
|
+
var NODE_PRIMARY_GROUPS = [
|
|
152
|
+
{ label: "linting", aliases: ["lint", "eslint", "lint:check"] },
|
|
153
|
+
{ label: "type checking", aliases: ["typecheck", "type-check", "tsc", "check-types"] },
|
|
154
|
+
{ label: "tests", aliases: ["test", "test:unit", "test:run", "vitest", "jest"] }
|
|
155
|
+
];
|
|
156
|
+
var NODE_FALLBACK_GROUPS = [
|
|
157
|
+
{ label: "build", aliases: ["build", "compile"] }
|
|
158
|
+
];
|
|
159
|
+
function readPackageJsonScripts(projectPath) {
|
|
160
|
+
try {
|
|
161
|
+
const raw = readFileSync(join2(projectPath, "package.json"), "utf-8");
|
|
162
|
+
const pkg = JSON.parse(raw);
|
|
163
|
+
return pkg.scripts ?? {};
|
|
164
|
+
} catch {
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
var nodeDetector = {
|
|
169
|
+
type: "node",
|
|
170
|
+
label: "Node.js",
|
|
171
|
+
detect: (path) => existsSync(join2(path, "package.json")),
|
|
172
|
+
getInstallCommand: (path) => {
|
|
173
|
+
const pm = detectNodePackageManager(path);
|
|
174
|
+
return `${pm} install`;
|
|
175
|
+
},
|
|
176
|
+
getCandidates: (path) => {
|
|
177
|
+
const scripts = readPackageJsonScripts(path);
|
|
178
|
+
const pm = detectNodePackageManager(path);
|
|
179
|
+
const run = pm === "npm" ? "npm run" : pm;
|
|
180
|
+
const candidates = [];
|
|
181
|
+
for (const group of NODE_PRIMARY_GROUPS) {
|
|
182
|
+
const match = group.aliases.find((name) => name in scripts);
|
|
183
|
+
if (match) {
|
|
184
|
+
candidates.push({ label: group.label, command: `${run} ${match}`, selected: true });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (candidates.length === 0) {
|
|
188
|
+
for (const group of NODE_FALLBACK_GROUPS) {
|
|
189
|
+
const match = group.aliases.find((name) => name in scripts);
|
|
190
|
+
if (match) {
|
|
191
|
+
candidates.push({ label: group.label, command: `${run} ${match}`, selected: false });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return candidates;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
var pythonDetector = {
|
|
199
|
+
type: "python",
|
|
200
|
+
label: "Python",
|
|
201
|
+
detect: (path) => existsSync(join2(path, "pyproject.toml")) || existsSync(join2(path, "setup.py")),
|
|
202
|
+
getInstallCommand: (path) => {
|
|
203
|
+
if (existsSync(join2(path, "uv.lock"))) return "uv sync";
|
|
204
|
+
if (existsSync(join2(path, "requirements.txt"))) return "pip install -r requirements.txt";
|
|
205
|
+
if (existsSync(join2(path, "pyproject.toml"))) return "pip install -e .";
|
|
206
|
+
return null;
|
|
207
|
+
},
|
|
208
|
+
getCandidates: () => [{ label: "tests", command: "pytest", selected: true }]
|
|
209
|
+
};
|
|
210
|
+
var goDetector = {
|
|
211
|
+
type: "go",
|
|
212
|
+
label: "Go",
|
|
213
|
+
detect: (path) => existsSync(join2(path, "go.mod")),
|
|
214
|
+
getInstallCommand: () => "go mod download",
|
|
215
|
+
getCandidates: () => [
|
|
216
|
+
{ label: "tests", command: "go test ./...", selected: true },
|
|
217
|
+
{ label: "vet", command: "go vet ./...", selected: true }
|
|
218
|
+
]
|
|
219
|
+
};
|
|
220
|
+
var rustDetector = {
|
|
221
|
+
type: "rust",
|
|
222
|
+
label: "Rust",
|
|
223
|
+
detect: (path) => existsSync(join2(path, "Cargo.toml")),
|
|
224
|
+
getInstallCommand: () => "cargo build",
|
|
225
|
+
getCandidates: () => [
|
|
226
|
+
{ label: "tests", command: "cargo test", selected: true },
|
|
227
|
+
{ label: "clippy", command: "cargo clippy", selected: false }
|
|
228
|
+
]
|
|
229
|
+
};
|
|
230
|
+
var gradleDetector = {
|
|
231
|
+
type: "java-gradle",
|
|
232
|
+
label: "Java (Gradle)",
|
|
233
|
+
detect: (path) => existsSync(join2(path, "build.gradle")) || existsSync(join2(path, "build.gradle.kts")),
|
|
234
|
+
getInstallCommand: () => null,
|
|
235
|
+
getCandidates: () => [{ label: "clean build", command: "./gradlew clean build", selected: true }]
|
|
236
|
+
};
|
|
237
|
+
var mavenDetector = {
|
|
238
|
+
type: "java-maven",
|
|
239
|
+
label: "Java (Maven)",
|
|
240
|
+
detect: (path) => existsSync(join2(path, "pom.xml")),
|
|
241
|
+
getInstallCommand: () => null,
|
|
242
|
+
getCandidates: () => [{ label: "clean install", command: "mvn clean install", selected: true }]
|
|
243
|
+
};
|
|
244
|
+
var makefileDetector = {
|
|
245
|
+
type: "makefile",
|
|
246
|
+
label: "Makefile",
|
|
247
|
+
detect: (path) => existsSync(join2(path, "Makefile")),
|
|
248
|
+
getInstallCommand: () => null,
|
|
249
|
+
getCandidates: () => [{ label: "check/test", command: "make check || make test", selected: true }]
|
|
250
|
+
};
|
|
251
|
+
var ECOSYSTEM_REGISTRY = [
|
|
252
|
+
nodeDetector,
|
|
253
|
+
pythonDetector,
|
|
254
|
+
goDetector,
|
|
255
|
+
rustDetector,
|
|
256
|
+
gradleDetector,
|
|
257
|
+
mavenDetector,
|
|
258
|
+
makefileDetector
|
|
259
|
+
];
|
|
260
|
+
function detectCheckScriptCandidates(projectPath) {
|
|
261
|
+
for (const detector of ECOSYSTEM_REGISTRY) {
|
|
262
|
+
if (detector.detect(projectPath)) {
|
|
263
|
+
return {
|
|
264
|
+
type: detector.type,
|
|
265
|
+
typeLabel: detector.label,
|
|
266
|
+
installCommand: detector.getInstallCommand(projectPath),
|
|
267
|
+
candidates: detector.getCandidates(projectPath)
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
function suggestCheckScript(projectPath) {
|
|
274
|
+
const result = detectCheckScriptCandidates(projectPath);
|
|
275
|
+
if (!result) return null;
|
|
276
|
+
const parts = [];
|
|
277
|
+
if (result.installCommand) parts.push(result.installCommand);
|
|
278
|
+
const selected = result.candidates.filter((c) => c.selected).map((c) => c.command);
|
|
279
|
+
parts.push(...selected);
|
|
280
|
+
return parts.length > 0 ? parts.join(" && ") : null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/commands/project/add.ts
|
|
284
|
+
function validateSlug(slug) {
|
|
285
|
+
return /^[a-z0-9-]+$/.test(slug);
|
|
286
|
+
}
|
|
287
|
+
function isGitRepo2(path) {
|
|
288
|
+
try {
|
|
289
|
+
const gitDir = join3(path, ".git");
|
|
290
|
+
return existsSync2(gitDir) && statSync2(gitDir).isDirectory();
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function hasAiInstructions(repoPath) {
|
|
296
|
+
return existsSync2(join3(repoPath, "CLAUDE.md")) || existsSync2(join3(repoPath, ".github", "copilot-instructions.md"));
|
|
297
|
+
}
|
|
298
|
+
async function addCheckScriptToRepository(repo) {
|
|
299
|
+
let suggested = null;
|
|
300
|
+
try {
|
|
301
|
+
const detection = detectCheckScriptCandidates(repo.path);
|
|
302
|
+
if (detection) {
|
|
303
|
+
log.success(` Detected: ${detection.typeLabel}`);
|
|
304
|
+
suggested = suggestCheckScript(repo.path);
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
const checkInput = await input({
|
|
309
|
+
message: " Check script (optional):",
|
|
310
|
+
default: suggested ?? void 0
|
|
311
|
+
});
|
|
312
|
+
const checkScript = checkInput.trim() || void 0;
|
|
313
|
+
return {
|
|
314
|
+
...repo,
|
|
315
|
+
checkScript
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
async function projectAddCommand(options = {}) {
|
|
319
|
+
let name;
|
|
320
|
+
let displayName;
|
|
321
|
+
let repositories;
|
|
322
|
+
let description;
|
|
323
|
+
if (options.interactive === false) {
|
|
324
|
+
const errors = [];
|
|
325
|
+
const trimmedName = options.name?.trim();
|
|
326
|
+
const trimmedDisplayName = options.displayName?.trim();
|
|
327
|
+
if (!trimmedName) {
|
|
328
|
+
errors.push("--name is required");
|
|
329
|
+
} else if (!validateSlug(trimmedName)) {
|
|
330
|
+
errors.push("--name must be a slug (lowercase, numbers, hyphens only)");
|
|
331
|
+
}
|
|
332
|
+
if (!trimmedDisplayName) {
|
|
333
|
+
errors.push("--display-name is required");
|
|
334
|
+
}
|
|
335
|
+
if (!options.paths || options.paths.length === 0) {
|
|
336
|
+
errors.push("--path is required (at least one)");
|
|
337
|
+
}
|
|
338
|
+
if (options.paths) {
|
|
339
|
+
const spinner = options.paths.length > 1 ? createSpinner("Validating repository paths...").start() : null;
|
|
340
|
+
for (const path of options.paths) {
|
|
341
|
+
const resolved = resolve2(expandTilde(path.trim()));
|
|
342
|
+
const validation = await validateProjectPath(resolved);
|
|
343
|
+
if (validation !== true) {
|
|
344
|
+
errors.push(`--path ${path}: ${validation}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
spinner?.succeed("Paths validated");
|
|
348
|
+
}
|
|
349
|
+
if (errors.length > 0 || !trimmedName || !trimmedDisplayName || !options.paths) {
|
|
350
|
+
showError("Validation failed");
|
|
351
|
+
for (const e of errors) {
|
|
352
|
+
log.item(error(e));
|
|
353
|
+
}
|
|
354
|
+
console.log("");
|
|
355
|
+
exitWithCode(EXIT_ERROR);
|
|
356
|
+
}
|
|
357
|
+
name = trimmedName;
|
|
358
|
+
displayName = trimmedDisplayName;
|
|
359
|
+
repositories = options.paths.map((p) => {
|
|
360
|
+
const resolved = resolve2(expandTilde(p.trim()));
|
|
361
|
+
const repo = { name: basename(resolved), path: resolved };
|
|
362
|
+
if (options.checkScript) repo.checkScript = options.checkScript;
|
|
363
|
+
return repo;
|
|
364
|
+
});
|
|
365
|
+
const trimmedDesc = options.description?.trim();
|
|
366
|
+
description = trimmedDesc === "" ? void 0 : trimmedDesc;
|
|
367
|
+
} else {
|
|
368
|
+
name = await input({
|
|
369
|
+
message: "Project name (slug):",
|
|
370
|
+
default: options.name?.trim(),
|
|
371
|
+
validate: (v) => {
|
|
372
|
+
const trimmed = v.trim();
|
|
373
|
+
if (trimmed.length === 0) return "Name is required";
|
|
374
|
+
if (!validateSlug(trimmed)) return "Must be lowercase with hyphens only";
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
name = name.trim();
|
|
379
|
+
displayName = await input({
|
|
380
|
+
message: "Display name:",
|
|
381
|
+
default: options.displayName?.trim() ?? name,
|
|
382
|
+
validate: (v) => v.trim().length > 0 ? true : "Display name is required"
|
|
383
|
+
});
|
|
384
|
+
displayName = displayName.trim();
|
|
385
|
+
repositories = [];
|
|
386
|
+
if (options.paths) {
|
|
387
|
+
for (const p of options.paths) {
|
|
388
|
+
const resolved = resolve2(expandTilde(p.trim()));
|
|
389
|
+
const validation = await validateProjectPath(resolved);
|
|
390
|
+
if (validation === true) {
|
|
391
|
+
repositories.push({ name: basename(resolved), path: resolved });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (repositories.length === 0) {
|
|
396
|
+
const pathMethod = await select({
|
|
397
|
+
message: `${emoji.donut} How to specify repository path?`,
|
|
398
|
+
choices: [
|
|
399
|
+
{ name: "Browse filesystem", value: "browse", description: "Navigate from home folder" },
|
|
400
|
+
{ name: "Use current directory", value: "cwd", description: process.cwd() },
|
|
401
|
+
{ name: "Type path manually", value: "manual" }
|
|
402
|
+
]
|
|
403
|
+
});
|
|
404
|
+
let firstPath;
|
|
405
|
+
if (pathMethod === "browse") {
|
|
406
|
+
const browsed = await browseDirectory("Select repository directory:");
|
|
407
|
+
if (!browsed) {
|
|
408
|
+
showError("No directory selected");
|
|
409
|
+
exitWithCode(EXIT_ERROR);
|
|
410
|
+
}
|
|
411
|
+
firstPath = browsed;
|
|
412
|
+
} else if (pathMethod === "cwd") {
|
|
413
|
+
firstPath = process.cwd();
|
|
414
|
+
} else {
|
|
415
|
+
firstPath = await input({
|
|
416
|
+
message: "Repository path:",
|
|
417
|
+
default: process.cwd(),
|
|
418
|
+
validate: async (v) => {
|
|
419
|
+
const result = await validateProjectPath(v.trim());
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
firstPath = firstPath.trim();
|
|
424
|
+
}
|
|
425
|
+
const resolved = resolve2(expandTilde(firstPath));
|
|
426
|
+
const validation = await validateProjectPath(resolved);
|
|
427
|
+
if (validation !== true) {
|
|
428
|
+
showError(`Invalid path: ${validation}`);
|
|
429
|
+
exitWithCode(EXIT_ERROR);
|
|
430
|
+
}
|
|
431
|
+
repositories.push({ name: basename(resolved), path: resolved });
|
|
432
|
+
}
|
|
433
|
+
const firstRepo = repositories[0];
|
|
434
|
+
if (firstRepo) {
|
|
435
|
+
if (!isGitRepo2(firstRepo.path)) {
|
|
436
|
+
showWarning("Path is not a git repository");
|
|
437
|
+
}
|
|
438
|
+
if (!hasAiInstructions(firstRepo.path)) {
|
|
439
|
+
showTip("Add CLAUDE.md or .github/copilot-instructions.md for better AI assistance");
|
|
440
|
+
}
|
|
441
|
+
log.info(`
|
|
442
|
+
Configuring: ${firstRepo.name}`);
|
|
443
|
+
repositories[0] = await addCheckScriptToRepository(firstRepo);
|
|
444
|
+
}
|
|
445
|
+
let addMore = true;
|
|
446
|
+
while (addMore) {
|
|
447
|
+
const addAction = await select({
|
|
448
|
+
message: `${emoji.donut} Add another repository?`,
|
|
449
|
+
choices: [
|
|
450
|
+
{ name: "No, done adding repositories", value: "done" },
|
|
451
|
+
{ name: "Browse filesystem", value: "browse" },
|
|
452
|
+
{ name: "Type path manually", value: "manual" }
|
|
453
|
+
]
|
|
454
|
+
});
|
|
455
|
+
if (addAction === "done") {
|
|
456
|
+
addMore = false;
|
|
457
|
+
} else if (addAction === "browse") {
|
|
458
|
+
const browsed = await browseDirectory("Select repository directory:");
|
|
459
|
+
if (browsed) {
|
|
460
|
+
const resolved = resolve2(expandTilde(browsed));
|
|
461
|
+
const validation = await validateProjectPath(resolved);
|
|
462
|
+
if (validation === true) {
|
|
463
|
+
const newRepo = { name: basename(resolved), path: resolved };
|
|
464
|
+
log.success(`Added: ${newRepo.name}`);
|
|
465
|
+
const repoWithScripts = await addCheckScriptToRepository(newRepo);
|
|
466
|
+
repositories.push(repoWithScripts);
|
|
467
|
+
} else {
|
|
468
|
+
log.error(`Invalid path: ${validation}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
const additionalPath = await input({
|
|
473
|
+
message: "Repository path:"
|
|
474
|
+
});
|
|
475
|
+
if (additionalPath.trim() === "") {
|
|
476
|
+
addMore = false;
|
|
477
|
+
} else {
|
|
478
|
+
const resolved = resolve2(expandTilde(additionalPath.trim()));
|
|
479
|
+
const validation = await validateProjectPath(resolved);
|
|
480
|
+
if (validation === true) {
|
|
481
|
+
const newRepo = { name: basename(resolved), path: resolved };
|
|
482
|
+
log.success(`Added: ${newRepo.name}`);
|
|
483
|
+
const repoWithScripts = await addCheckScriptToRepository(newRepo);
|
|
484
|
+
repositories.push(repoWithScripts);
|
|
485
|
+
} else {
|
|
486
|
+
log.error(`Invalid path: ${validation}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
description = await input({
|
|
492
|
+
message: "Description (optional):",
|
|
493
|
+
default: options.description?.trim()
|
|
494
|
+
});
|
|
495
|
+
const trimmedDescInteractive = description.trim();
|
|
496
|
+
description = trimmedDescInteractive === "" ? void 0 : trimmedDescInteractive;
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
const project = {
|
|
500
|
+
name,
|
|
501
|
+
displayName,
|
|
502
|
+
repositories,
|
|
503
|
+
description
|
|
504
|
+
};
|
|
505
|
+
const created = await createProject(project);
|
|
506
|
+
showSuccess("Project added!", [
|
|
507
|
+
["Name", created.name],
|
|
508
|
+
["Display Name", created.displayName]
|
|
509
|
+
]);
|
|
510
|
+
if (created.description) {
|
|
511
|
+
console.log(field("Description", created.description));
|
|
512
|
+
}
|
|
513
|
+
console.log(field("Repositories", ""));
|
|
514
|
+
for (const repo of created.repositories) {
|
|
515
|
+
log.item(`${repo.name} \u2192 ${repo.path}`);
|
|
516
|
+
if (repo.checkScript) {
|
|
517
|
+
console.log(` Check: ${repo.checkScript}`);
|
|
518
|
+
} else {
|
|
519
|
+
console.log(` Check: ${muted("(not configured)")}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
console.log("");
|
|
523
|
+
} catch (err) {
|
|
524
|
+
if (err instanceof ProjectExistsError) {
|
|
525
|
+
showError(`Project "${name}" already exists.`);
|
|
526
|
+
showNextStep(`ralphctl project remove ${name}`, "remove existing project first");
|
|
527
|
+
log.newline();
|
|
528
|
+
} else {
|
|
529
|
+
throw err;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export {
|
|
535
|
+
addCheckScriptToRepository,
|
|
536
|
+
projectAddCommand
|
|
537
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/interactive/escapable.ts
|
|
4
|
+
import readline from "readline";
|
|
5
|
+
import { select } from "@inquirer/prompts";
|
|
6
|
+
import { bold, dim } from "colorette";
|
|
7
|
+
function defaultKeysHelpTip(keys) {
|
|
8
|
+
return keys.map(([key, action]) => `${bold(key)} ${dim(action)}`).join(dim(" \u2022 "));
|
|
9
|
+
}
|
|
10
|
+
function withEscapeHint(config, escLabel = "back") {
|
|
11
|
+
const originalTip = config.theme?.style?.keysHelpTip;
|
|
12
|
+
return {
|
|
13
|
+
...config,
|
|
14
|
+
theme: {
|
|
15
|
+
...config.theme,
|
|
16
|
+
style: {
|
|
17
|
+
...config.theme?.style,
|
|
18
|
+
keysHelpTip: (keys) => {
|
|
19
|
+
const allKeys = [...keys, ["esc", escLabel]];
|
|
20
|
+
return originalTip ? originalTip(allKeys) : defaultKeysHelpTip(allKeys);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function escapableSelect(config, options) {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
readline.emitKeypressEvents(process.stdin);
|
|
29
|
+
const onKeypress = (_ch, key) => {
|
|
30
|
+
if (key?.name === "escape") {
|
|
31
|
+
controller.abort();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
process.stdin.on("keypress", onKeypress);
|
|
35
|
+
try {
|
|
36
|
+
const result = await select(withEscapeHint(config, options?.escLabel ?? "back"), {
|
|
37
|
+
signal: controller.signal
|
|
38
|
+
});
|
|
39
|
+
return result;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err instanceof Error && err.name === "AbortPromptError") {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
} finally {
|
|
46
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
escapableSelect
|
|
52
|
+
};
|