ralphctl 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -16
- package/dist/absolute-path-WUTZQ37D.mjs +8 -0
- package/dist/chunk-6RDMCLWU.mjs +108 -0
- package/dist/chunk-HIU74KTO.mjs +1046 -0
- package/dist/chunk-S3PTDH57.mjs +78 -0
- package/dist/chunk-WV4D2CPG.mjs +26 -0
- package/dist/cli.mjs +22413 -717
- package/dist/manifest.json +24 -0
- package/dist/prompt-adapter-JQICGVX7.mjs +7 -0
- package/dist/prompts/ideate.md +3 -1
- package/dist/prompts/plan-auto.md +23 -8
- package/dist/prompts/plan-common-examples.md +3 -3
- package/dist/prompts/plan-common.md +6 -5
- package/dist/prompts/plan-interactive.md +30 -7
- package/dist/prompts/repo-onboard.md +154 -64
- package/dist/prompts/signals-task.md +3 -0
- package/dist/prompts/sprint-feedback.md +3 -0
- package/dist/prompts/task-evaluation.md +74 -53
- package/dist/prompts/task-execution.md +65 -21
- package/dist/prompts/ticket-refine.md +11 -8
- package/dist/prompts/validation-checklist.md +3 -2
- package/dist/skills/default/abstraction-first/SKILL.md +45 -0
- package/dist/skills/default/alignment/SKILL.md +46 -0
- package/dist/skills/default/iterative-review/SKILL.md +48 -0
- package/dist/skills/exec/.gitkeep +0 -0
- package/dist/skills/plan/.gitkeep +0 -0
- package/dist/skills/refine/.gitkeep +0 -0
- package/dist/storage-paths-IPNZZM5D.mjs +15 -0
- package/dist/validation-error-QT6Q7FYU.mjs +7 -0
- package/package.json +9 -4
- package/dist/add-67UFUI54.mjs +0 -17
- package/dist/add-DVPVHENV.mjs +0 -18
- package/dist/bootstrap-FMHG6DRY.mjs +0 -11
- package/dist/chunk-62HYDA7L.mjs +0 -1128
- package/dist/chunk-747KW2RW.mjs +0 -24
- package/dist/chunk-BSB4EDGR.mjs +0 -260
- package/dist/chunk-BT5FKIZX.mjs +0 -787
- package/dist/chunk-CBMFRQ4Y.mjs +0 -441
- package/dist/chunk-CFUVE2BP.mjs +0 -16
- package/dist/chunk-D6QZNEYN.mjs +0 -5520
- package/dist/chunk-FNAAA32W.mjs +0 -103
- package/dist/chunk-GQ2WFKBN.mjs +0 -269
- package/dist/chunk-IWXBJD2D.mjs +0 -27
- package/dist/chunk-OGEXYSFS.mjs +0 -228
- package/dist/chunk-VAZ3LJBI.mjs +0 -179
- package/dist/chunk-WDMLPXOD.mjs +0 -363
- package/dist/chunk-XN2UIHBY.mjs +0 -589
- package/dist/chunk-ZE2BRQA2.mjs +0 -5542
- package/dist/create-Z635FQKO.mjs +0 -15
- package/dist/handle-23EFF3BE.mjs +0 -22
- package/dist/mount-NCYR22SN.mjs +0 -7434
- package/dist/project-DQHF4ISP.mjs +0 -34
- package/dist/prompts/check-script-discover.md +0 -69
- package/dist/prompts/ideate-auto.md +0 -195
- package/dist/prompts/task-evaluation-resume.md +0 -41
- package/dist/resolver-OVPYVW6Q.mjs +0 -163
- package/dist/sprint-4E26AB5F.mjs +0 -38
- package/dist/start-T34NI3LF.mjs +0 -19
package/dist/chunk-BT5FKIZX.mjs
DELETED
|
@@ -1,787 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
ProviderAiSessionAdapter,
|
|
4
|
-
SignalParser,
|
|
5
|
-
buildCheckScriptDiscoverPrompt
|
|
6
|
-
} from "./chunk-62HYDA7L.mjs";
|
|
7
|
-
import {
|
|
8
|
-
EXIT_ERROR,
|
|
9
|
-
exitWithCode
|
|
10
|
-
} from "./chunk-CFUVE2BP.mjs";
|
|
11
|
-
import {
|
|
12
|
-
getPrompt
|
|
13
|
-
} from "./chunk-747KW2RW.mjs";
|
|
14
|
-
import {
|
|
15
|
-
createProject
|
|
16
|
-
} from "./chunk-BSB4EDGR.mjs";
|
|
17
|
-
import {
|
|
18
|
-
createSpinner,
|
|
19
|
-
emoji,
|
|
20
|
-
error,
|
|
21
|
-
field,
|
|
22
|
-
getConfig,
|
|
23
|
-
log,
|
|
24
|
-
muted,
|
|
25
|
-
showError,
|
|
26
|
-
showNextStep,
|
|
27
|
-
showSuccess,
|
|
28
|
-
showTip,
|
|
29
|
-
showWarning
|
|
30
|
-
} from "./chunk-XN2UIHBY.mjs";
|
|
31
|
-
import {
|
|
32
|
-
ensureError,
|
|
33
|
-
wrapAsync
|
|
34
|
-
} from "./chunk-IWXBJD2D.mjs";
|
|
35
|
-
import {
|
|
36
|
-
expandTilde,
|
|
37
|
-
validateProjectPath
|
|
38
|
-
} from "./chunk-WDMLPXOD.mjs";
|
|
39
|
-
import {
|
|
40
|
-
IOError,
|
|
41
|
-
ProjectExistsError,
|
|
42
|
-
ValidationError
|
|
43
|
-
} from "./chunk-VAZ3LJBI.mjs";
|
|
44
|
-
|
|
45
|
-
// src/integration/cli/commands/project/add.ts
|
|
46
|
-
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
47
|
-
import { basename, join as join3, resolve as resolve2 } from "path";
|
|
48
|
-
import { Result as Result4 } from "typescript-result";
|
|
49
|
-
|
|
50
|
-
// src/integration/ui/prompts/file-browser-impl.ts
|
|
51
|
-
import { readdirSync, statSync } from "fs";
|
|
52
|
-
import { homedir } from "os";
|
|
53
|
-
import { dirname, join, resolve } from "path";
|
|
54
|
-
import { Result } from "typescript-result";
|
|
55
|
-
|
|
56
|
-
// src/business/ports/prompt.ts
|
|
57
|
-
var PromptCancelledError = class extends Error {
|
|
58
|
-
constructor(message = "Prompt cancelled by user") {
|
|
59
|
-
super(message);
|
|
60
|
-
this.name = "PromptCancelledError";
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// src/integration/ui/prompts/escapable.ts
|
|
65
|
-
function isChoice(item) {
|
|
66
|
-
return "value" in item && "name" in item;
|
|
67
|
-
}
|
|
68
|
-
async function escapableSelect(config) {
|
|
69
|
-
const choices = config.choices.map(
|
|
70
|
-
(item) => {
|
|
71
|
-
if (isChoice(item)) {
|
|
72
|
-
return {
|
|
73
|
-
label: item.name,
|
|
74
|
-
value: item.value,
|
|
75
|
-
description: item.description,
|
|
76
|
-
disabled: item.disabled
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
label: item.separator,
|
|
81
|
-
value: void 0,
|
|
82
|
-
disabled: true
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
);
|
|
86
|
-
try {
|
|
87
|
-
return await getPrompt().select({
|
|
88
|
-
message: config.message,
|
|
89
|
-
choices,
|
|
90
|
-
default: config.default
|
|
91
|
-
});
|
|
92
|
-
} catch (err) {
|
|
93
|
-
if (err instanceof PromptCancelledError) return null;
|
|
94
|
-
throw err;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// src/integration/ui/prompts/file-browser-impl.ts
|
|
99
|
-
function listDirectories(dirPath) {
|
|
100
|
-
const r = Result.try(() => readdirSync(dirPath, { withFileTypes: true }));
|
|
101
|
-
if (!r.ok) return [];
|
|
102
|
-
return r.value.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
103
|
-
}
|
|
104
|
-
function hasSubdirectories(dirPath) {
|
|
105
|
-
const r = Result.try(() => readdirSync(dirPath, { withFileTypes: true }));
|
|
106
|
-
if (!r.ok) return false;
|
|
107
|
-
return r.value.some((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
108
|
-
}
|
|
109
|
-
function isGitRepo(dirPath) {
|
|
110
|
-
const r = Result.try(() => statSync(join(dirPath, ".git")));
|
|
111
|
-
if (!r.ok) return false;
|
|
112
|
-
return r.value.isDirectory();
|
|
113
|
-
}
|
|
114
|
-
async function browseDirectory(message = "Browse to directory:", startPath) {
|
|
115
|
-
let currentPath = startPath ? resolve(startPath) : homedir();
|
|
116
|
-
while (true) {
|
|
117
|
-
const dirs = listDirectories(currentPath);
|
|
118
|
-
const choices = [];
|
|
119
|
-
choices.push({
|
|
120
|
-
name: `${emoji.donut} Select this directory`,
|
|
121
|
-
value: "__SELECT__",
|
|
122
|
-
description: currentPath
|
|
123
|
-
});
|
|
124
|
-
const parentDir = dirname(currentPath);
|
|
125
|
-
if (parentDir !== currentPath) {
|
|
126
|
-
choices.push({
|
|
127
|
-
name: "\u2191 Parent directory",
|
|
128
|
-
value: "__PARENT__",
|
|
129
|
-
description: parentDir
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
if (currentPath !== homedir()) {
|
|
133
|
-
choices.push({
|
|
134
|
-
name: "\u2302 Home directory",
|
|
135
|
-
value: "__HOME__",
|
|
136
|
-
description: homedir()
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
for (const dir of dirs) {
|
|
140
|
-
const fullPath = join(currentPath, dir);
|
|
141
|
-
const hasChildren = hasSubdirectories(fullPath);
|
|
142
|
-
const isRepo = isGitRepo(fullPath);
|
|
143
|
-
let icon = " ";
|
|
144
|
-
if (isRepo) {
|
|
145
|
-
icon = "\u2699 ";
|
|
146
|
-
} else if (hasChildren) {
|
|
147
|
-
icon = "\u25B8 ";
|
|
148
|
-
}
|
|
149
|
-
choices.push({
|
|
150
|
-
name: `${icon}${dir}`,
|
|
151
|
-
value: fullPath,
|
|
152
|
-
description: isRepo ? "git repo" : void 0
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
choices.push({
|
|
156
|
-
name: muted("Cancel"),
|
|
157
|
-
value: "__CANCEL__"
|
|
158
|
-
});
|
|
159
|
-
const selectResult = await wrapAsync(
|
|
160
|
-
() => escapableSelect({
|
|
161
|
-
message: `${emoji.donut} ${message}
|
|
162
|
-
${muted(currentPath)}`,
|
|
163
|
-
choices
|
|
164
|
-
}),
|
|
165
|
-
ensureError
|
|
166
|
-
);
|
|
167
|
-
if (!selectResult.ok) {
|
|
168
|
-
if (selectResult.error.name === "ExitPromptError") return null;
|
|
169
|
-
throw selectResult.error;
|
|
170
|
-
}
|
|
171
|
-
const selected = selectResult.value;
|
|
172
|
-
if (selected === null) {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
switch (selected) {
|
|
176
|
-
case "__SELECT__":
|
|
177
|
-
return currentPath;
|
|
178
|
-
case "__PARENT__":
|
|
179
|
-
currentPath = parentDir;
|
|
180
|
-
break;
|
|
181
|
-
case "__HOME__":
|
|
182
|
-
currentPath = homedir();
|
|
183
|
-
break;
|
|
184
|
-
case "__CANCEL__":
|
|
185
|
-
return null;
|
|
186
|
-
default:
|
|
187
|
-
currentPath = selected;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// src/integration/external/detect-scripts.ts
|
|
193
|
-
import { existsSync, readFileSync } from "fs";
|
|
194
|
-
import { join as join2 } from "path";
|
|
195
|
-
import { Result as Result2 } from "typescript-result";
|
|
196
|
-
function detectNodePackageManager(projectPath) {
|
|
197
|
-
if (existsSync(join2(projectPath, "pnpm-lock.yaml"))) return "pnpm";
|
|
198
|
-
if (existsSync(join2(projectPath, "yarn.lock"))) return "yarn";
|
|
199
|
-
return "npm";
|
|
200
|
-
}
|
|
201
|
-
var NODE_PRIMARY_GROUPS = [
|
|
202
|
-
{ label: "linting", aliases: ["lint", "eslint", "lint:check"] },
|
|
203
|
-
{ label: "type checking", aliases: ["typecheck", "type-check", "tsc", "check-types"] },
|
|
204
|
-
{ label: "tests", aliases: ["test", "test:unit", "test:run", "vitest", "jest"] }
|
|
205
|
-
];
|
|
206
|
-
var NODE_FALLBACK_GROUPS = [
|
|
207
|
-
{ label: "build", aliases: ["build", "compile"] }
|
|
208
|
-
];
|
|
209
|
-
function safeReadPackageJsonScripts(projectPath) {
|
|
210
|
-
try {
|
|
211
|
-
const raw = readFileSync(join2(projectPath, "package.json"), "utf-8");
|
|
212
|
-
const pkg = JSON.parse(raw);
|
|
213
|
-
return Result2.ok(pkg.scripts ?? {});
|
|
214
|
-
} catch {
|
|
215
|
-
return Result2.error(new IOError("Failed to read package.json"));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
function readPackageJsonScripts(projectPath) {
|
|
219
|
-
const result = safeReadPackageJsonScripts(projectPath);
|
|
220
|
-
if (!result.ok) return {};
|
|
221
|
-
return result.value;
|
|
222
|
-
}
|
|
223
|
-
var nodeDetector = {
|
|
224
|
-
type: "node",
|
|
225
|
-
label: "Node.js",
|
|
226
|
-
detect: (path) => existsSync(join2(path, "package.json")),
|
|
227
|
-
getInstallCommand: (path) => {
|
|
228
|
-
const pm = detectNodePackageManager(path);
|
|
229
|
-
return `${pm} install`;
|
|
230
|
-
},
|
|
231
|
-
getCandidates: (path) => {
|
|
232
|
-
const scripts = readPackageJsonScripts(path);
|
|
233
|
-
const pm = detectNodePackageManager(path);
|
|
234
|
-
const run = pm === "npm" ? "npm run" : pm;
|
|
235
|
-
const candidates = [];
|
|
236
|
-
for (const group of NODE_PRIMARY_GROUPS) {
|
|
237
|
-
const match = group.aliases.find((name) => name in scripts);
|
|
238
|
-
if (match) {
|
|
239
|
-
candidates.push({ label: group.label, command: `${run} ${match}`, selected: true });
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (candidates.length === 0) {
|
|
243
|
-
for (const group of NODE_FALLBACK_GROUPS) {
|
|
244
|
-
const match = group.aliases.find((name) => name in scripts);
|
|
245
|
-
if (match) {
|
|
246
|
-
candidates.push({ label: group.label, command: `${run} ${match}`, selected: false });
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return candidates;
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
var pythonDetector = {
|
|
254
|
-
type: "python",
|
|
255
|
-
label: "Python",
|
|
256
|
-
detect: (path) => existsSync(join2(path, "pyproject.toml")) || existsSync(join2(path, "setup.py")),
|
|
257
|
-
getInstallCommand: (path) => {
|
|
258
|
-
if (existsSync(join2(path, "uv.lock"))) return "uv sync";
|
|
259
|
-
if (existsSync(join2(path, "requirements.txt"))) return "pip install -r requirements.txt";
|
|
260
|
-
if (existsSync(join2(path, "pyproject.toml"))) return "pip install -e .";
|
|
261
|
-
return null;
|
|
262
|
-
},
|
|
263
|
-
getCandidates: () => [{ label: "tests", command: "pytest", selected: true }]
|
|
264
|
-
};
|
|
265
|
-
var goDetector = {
|
|
266
|
-
type: "go",
|
|
267
|
-
label: "Go",
|
|
268
|
-
detect: (path) => existsSync(join2(path, "go.mod")),
|
|
269
|
-
getInstallCommand: () => "go mod download",
|
|
270
|
-
getCandidates: () => [
|
|
271
|
-
{ label: "tests", command: "go test ./...", selected: true },
|
|
272
|
-
{ label: "vet", command: "go vet ./...", selected: true }
|
|
273
|
-
]
|
|
274
|
-
};
|
|
275
|
-
var rustDetector = {
|
|
276
|
-
type: "rust",
|
|
277
|
-
label: "Rust",
|
|
278
|
-
detect: (path) => existsSync(join2(path, "Cargo.toml")),
|
|
279
|
-
getInstallCommand: () => "cargo build",
|
|
280
|
-
getCandidates: () => [
|
|
281
|
-
{ label: "tests", command: "cargo test", selected: true },
|
|
282
|
-
{ label: "clippy", command: "cargo clippy", selected: false }
|
|
283
|
-
]
|
|
284
|
-
};
|
|
285
|
-
var gradleDetector = {
|
|
286
|
-
type: "java-gradle",
|
|
287
|
-
label: "Java (Gradle)",
|
|
288
|
-
detect: (path) => existsSync(join2(path, "build.gradle")) || existsSync(join2(path, "build.gradle.kts")),
|
|
289
|
-
getInstallCommand: () => null,
|
|
290
|
-
getCandidates: () => [{ label: "clean build", command: "./gradlew clean build", selected: true }]
|
|
291
|
-
};
|
|
292
|
-
var mavenDetector = {
|
|
293
|
-
type: "java-maven",
|
|
294
|
-
label: "Java (Maven)",
|
|
295
|
-
detect: (path) => existsSync(join2(path, "pom.xml")),
|
|
296
|
-
getInstallCommand: () => null,
|
|
297
|
-
getCandidates: () => [{ label: "clean install", command: "mvn clean install", selected: true }]
|
|
298
|
-
};
|
|
299
|
-
var makefileDetector = {
|
|
300
|
-
type: "makefile",
|
|
301
|
-
label: "Makefile",
|
|
302
|
-
detect: (path) => existsSync(join2(path, "Makefile")),
|
|
303
|
-
getInstallCommand: () => null,
|
|
304
|
-
getCandidates: () => [{ label: "check/test", command: "make check || make test", selected: true }]
|
|
305
|
-
};
|
|
306
|
-
var ECOSYSTEM_REGISTRY = [
|
|
307
|
-
nodeDetector,
|
|
308
|
-
pythonDetector,
|
|
309
|
-
goDetector,
|
|
310
|
-
rustDetector,
|
|
311
|
-
gradleDetector,
|
|
312
|
-
mavenDetector,
|
|
313
|
-
makefileDetector
|
|
314
|
-
];
|
|
315
|
-
function detectCheckScriptCandidates(projectPath) {
|
|
316
|
-
for (const detector of ECOSYSTEM_REGISTRY) {
|
|
317
|
-
if (detector.detect(projectPath)) {
|
|
318
|
-
return {
|
|
319
|
-
type: detector.type,
|
|
320
|
-
typeLabel: detector.label,
|
|
321
|
-
installCommand: detector.getInstallCommand(projectPath),
|
|
322
|
-
candidates: detector.getCandidates(projectPath)
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
function suggestCheckScript(projectPath) {
|
|
329
|
-
const result = detectCheckScriptCandidates(projectPath);
|
|
330
|
-
if (!result) return null;
|
|
331
|
-
const parts = [];
|
|
332
|
-
if (result.installCommand) parts.push(result.installCommand);
|
|
333
|
-
const selected = result.candidates.filter((c) => c.selected).map((c) => c.command);
|
|
334
|
-
parts.push(...selected);
|
|
335
|
-
return parts.length > 0 ? parts.join(" && ") : null;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// src/integration/ai/discover-check-script.ts
|
|
339
|
-
var DISCOVERY_TIMEOUT_MS = 3e4;
|
|
340
|
-
async function discoverCheckScriptWithAi(repoPath, aiSession, signalParser) {
|
|
341
|
-
const prompt = buildCheckScriptDiscoverPrompt(repoPath);
|
|
342
|
-
const session = aiSession.spawnHeadless(prompt, { cwd: repoPath });
|
|
343
|
-
const timeout = new Promise((resolve3) => {
|
|
344
|
-
setTimeout(() => {
|
|
345
|
-
resolve3(null);
|
|
346
|
-
}, DISCOVERY_TIMEOUT_MS).unref();
|
|
347
|
-
});
|
|
348
|
-
try {
|
|
349
|
-
const result = await Promise.race([session, timeout]);
|
|
350
|
-
if (!result) return null;
|
|
351
|
-
const signals = signalParser.parseSignals(result.output);
|
|
352
|
-
const discovery = signals.find((s) => s.type === "check-script-discovery");
|
|
353
|
-
return discovery ? discovery.command : null;
|
|
354
|
-
} catch {
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// src/integration/config/schema-provider.ts
|
|
360
|
-
import { Result as Result3 } from "typescript-result";
|
|
361
|
-
|
|
362
|
-
// src/domain/config-schema.ts
|
|
363
|
-
var ConfigSchemaDefinition = {
|
|
364
|
-
currentSprint: {
|
|
365
|
-
key: "currentSprint",
|
|
366
|
-
label: "Current Sprint",
|
|
367
|
-
type: "string",
|
|
368
|
-
default: null,
|
|
369
|
-
description: "Currently active sprint ID (set by `sprint start`, cleared on `sprint close`)",
|
|
370
|
-
validation: (val) => val === null || typeof val === "string" && val.length > 0,
|
|
371
|
-
scope: "global"
|
|
372
|
-
},
|
|
373
|
-
aiProvider: {
|
|
374
|
-
key: "aiProvider",
|
|
375
|
-
label: "AI Provider",
|
|
376
|
-
type: "enum",
|
|
377
|
-
enum: ["claude", "copilot"],
|
|
378
|
-
default: null,
|
|
379
|
-
description: "AI provider for task execution (Claude Code or GitHub Copilot CLI)",
|
|
380
|
-
validation: (val) => val === null || typeof val === "string" && ["claude", "copilot"].includes(val),
|
|
381
|
-
scope: "global"
|
|
382
|
-
},
|
|
383
|
-
evaluationIterations: {
|
|
384
|
-
key: "evaluationIterations",
|
|
385
|
-
label: "Evaluation Iterations",
|
|
386
|
-
type: "integer",
|
|
387
|
-
min: 0,
|
|
388
|
-
max: 10,
|
|
389
|
-
default: 1,
|
|
390
|
-
description: "Number of fix-attempt iterations after initial evaluation; 0 = disabled. Higher values allow more refinement rounds.",
|
|
391
|
-
validation: (val) => typeof val === "number" && Number.isInteger(val) && val >= 0 && val <= 10,
|
|
392
|
-
scope: "sprint"
|
|
393
|
-
},
|
|
394
|
-
aiCheckScriptDiscovery: {
|
|
395
|
-
key: "aiCheckScriptDiscovery",
|
|
396
|
-
label: "AI Check-Script Discovery",
|
|
397
|
-
type: "boolean",
|
|
398
|
-
default: false,
|
|
399
|
-
description: "When static ecosystem detection cannot suggest a check script during `project add`, ask the configured AI provider to inspect the repo and propose one. User approval is always required before saving. Disabled by default \u2014 enable with `ralphctl config set aiCheckScriptDiscovery true`.",
|
|
400
|
-
validation: (val) => typeof val === "boolean",
|
|
401
|
-
scope: "user"
|
|
402
|
-
}
|
|
403
|
-
// Phase 2+ additions can be added here as single schema entries
|
|
404
|
-
// maxTaskTurns: { ... },
|
|
405
|
-
// checkScriptTimeout: { ... },
|
|
406
|
-
};
|
|
407
|
-
function getSchemaEntry(key) {
|
|
408
|
-
return ConfigSchemaDefinition[key];
|
|
409
|
-
}
|
|
410
|
-
function getAllSchemaEntries() {
|
|
411
|
-
return Object.values(ConfigSchemaDefinition);
|
|
412
|
-
}
|
|
413
|
-
function getDefaultValue(key) {
|
|
414
|
-
return ConfigSchemaDefinition[key].default;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// src/integration/config/schema-provider.ts
|
|
418
|
-
function getAllConfigSchemaEntries() {
|
|
419
|
-
return getAllSchemaEntries();
|
|
420
|
-
}
|
|
421
|
-
function getConfigSchemaEntry(key) {
|
|
422
|
-
return getSchemaEntry(key);
|
|
423
|
-
}
|
|
424
|
-
function getConfigDefaultValue(key) {
|
|
425
|
-
return getDefaultValue(key);
|
|
426
|
-
}
|
|
427
|
-
function validateConfigValue(key, value) {
|
|
428
|
-
if (!(key in ConfigSchemaDefinition)) {
|
|
429
|
-
return Result3.error(new ValidationError(`Unknown config key: ${key}`, key));
|
|
430
|
-
}
|
|
431
|
-
const schemaKey = key;
|
|
432
|
-
const entry = getConfigSchemaEntry(schemaKey);
|
|
433
|
-
if (!entry.validation(value)) {
|
|
434
|
-
let errorMsg = `Invalid value for ${key}`;
|
|
435
|
-
if (entry.type === "enum" && entry.enum) {
|
|
436
|
-
errorMsg += `: must be one of ${entry.enum.join(", ")}`;
|
|
437
|
-
} else if (entry.type === "integer" || entry.type === "number") {
|
|
438
|
-
const constraints = [];
|
|
439
|
-
if (entry.min !== void 0) constraints.push(`min: ${String(entry.min)}`);
|
|
440
|
-
if (entry.max !== void 0) constraints.push(`max: ${String(entry.max)}`);
|
|
441
|
-
if (constraints.length > 0) errorMsg += ` (${constraints.join(", ")})`;
|
|
442
|
-
}
|
|
443
|
-
return Result3.error(new ValidationError(errorMsg, key));
|
|
444
|
-
}
|
|
445
|
-
return Result3.ok(value);
|
|
446
|
-
}
|
|
447
|
-
function parseConfigValue(key, stringValue) {
|
|
448
|
-
if (!(key in ConfigSchemaDefinition)) {
|
|
449
|
-
return Result3.error(new ValidationError(`Unknown config key: ${key}`, key));
|
|
450
|
-
}
|
|
451
|
-
const schemaKey = key;
|
|
452
|
-
const entry = getConfigSchemaEntry(schemaKey);
|
|
453
|
-
let parsedValue;
|
|
454
|
-
try {
|
|
455
|
-
switch (entry.type) {
|
|
456
|
-
case "string":
|
|
457
|
-
parsedValue = stringValue === "null" ? null : stringValue;
|
|
458
|
-
break;
|
|
459
|
-
case "integer":
|
|
460
|
-
parsedValue = stringValue === "null" ? null : Number.parseInt(stringValue, 10);
|
|
461
|
-
if (Number.isNaN(parsedValue)) {
|
|
462
|
-
return Result3.error(new ValidationError(`Expected integer for ${key}, got ${stringValue}`, key));
|
|
463
|
-
}
|
|
464
|
-
break;
|
|
465
|
-
case "number":
|
|
466
|
-
parsedValue = stringValue === "null" ? null : Number.parseFloat(stringValue);
|
|
467
|
-
if (Number.isNaN(parsedValue)) {
|
|
468
|
-
return Result3.error(new ValidationError(`Expected number for ${key}, got ${stringValue}`, key));
|
|
469
|
-
}
|
|
470
|
-
break;
|
|
471
|
-
case "boolean":
|
|
472
|
-
if (stringValue.toLowerCase() === "true" || stringValue === "1") {
|
|
473
|
-
parsedValue = true;
|
|
474
|
-
} else if (stringValue.toLowerCase() === "false" || stringValue === "0") {
|
|
475
|
-
parsedValue = false;
|
|
476
|
-
} else if (stringValue === "null") {
|
|
477
|
-
parsedValue = null;
|
|
478
|
-
} else {
|
|
479
|
-
return Result3.error(new ValidationError(`Expected boolean for ${key}, got ${stringValue}`, key));
|
|
480
|
-
}
|
|
481
|
-
break;
|
|
482
|
-
case "enum":
|
|
483
|
-
if (stringValue === "null") {
|
|
484
|
-
parsedValue = null;
|
|
485
|
-
} else {
|
|
486
|
-
parsedValue = stringValue;
|
|
487
|
-
}
|
|
488
|
-
break;
|
|
489
|
-
}
|
|
490
|
-
} catch (err) {
|
|
491
|
-
return Result3.error(
|
|
492
|
-
new ValidationError(`Failed to parse ${key}: ${err instanceof Error ? err.message : String(err)}`, key)
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
return validateConfigValue(key, parsedValue);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// src/integration/cli/commands/project/add.ts
|
|
499
|
-
function validateSlug(slug) {
|
|
500
|
-
return /^[a-z0-9-]+$/.test(slug);
|
|
501
|
-
}
|
|
502
|
-
function isGitRepo2(path) {
|
|
503
|
-
const gitDir = join3(path, ".git");
|
|
504
|
-
const r = Result4.try(() => existsSync2(gitDir) && statSync2(gitDir).isDirectory());
|
|
505
|
-
return r.ok ? r.value : false;
|
|
506
|
-
}
|
|
507
|
-
function hasAiInstructions(repoPath) {
|
|
508
|
-
return existsSync2(join3(repoPath, "CLAUDE.md")) || existsSync2(join3(repoPath, ".github", "copilot-instructions.md"));
|
|
509
|
-
}
|
|
510
|
-
async function addCheckScriptToRepository(repo) {
|
|
511
|
-
let suggested = null;
|
|
512
|
-
const detectR = Result4.try(() => detectCheckScriptCandidates(repo.path));
|
|
513
|
-
if (detectR.ok && detectR.value) {
|
|
514
|
-
log.success(` Detected: ${detectR.value.typeLabel}`);
|
|
515
|
-
suggested = suggestCheckScript(repo.path);
|
|
516
|
-
}
|
|
517
|
-
suggested ??= await tryAiCheckScriptDiscovery(repo.path);
|
|
518
|
-
const checkInput = await getPrompt().input({
|
|
519
|
-
message: " Check script (optional):",
|
|
520
|
-
default: suggested ?? void 0
|
|
521
|
-
});
|
|
522
|
-
const checkScript = checkInput.trim() || void 0;
|
|
523
|
-
return {
|
|
524
|
-
...repo,
|
|
525
|
-
checkScript
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
async function tryAiCheckScriptDiscovery(repoPath) {
|
|
529
|
-
const config = await getConfig();
|
|
530
|
-
const enabled = config.aiCheckScriptDiscovery ?? getConfigDefaultValue("aiCheckScriptDiscovery");
|
|
531
|
-
if (!enabled) return null;
|
|
532
|
-
if (!config.aiProvider) return null;
|
|
533
|
-
const wantAi = await getPrompt().confirm({
|
|
534
|
-
message: " No ecosystem detected. Ask AI to inspect the repo and suggest a check script?",
|
|
535
|
-
default: true
|
|
536
|
-
});
|
|
537
|
-
if (!wantAi) return null;
|
|
538
|
-
const spinner = createSpinner(" Asking AI to discover check script...").start();
|
|
539
|
-
const aiSession = new ProviderAiSessionAdapter();
|
|
540
|
-
const signalParser = new SignalParser();
|
|
541
|
-
const discoverR = await wrapAsync(async () => {
|
|
542
|
-
await aiSession.ensureReady();
|
|
543
|
-
return discoverCheckScriptWithAi(repoPath, aiSession, signalParser);
|
|
544
|
-
}, ensureError);
|
|
545
|
-
if (!discoverR.ok) {
|
|
546
|
-
spinner.fail("AI discovery unavailable");
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
const suggestion = discoverR.value;
|
|
550
|
-
if (suggestion) {
|
|
551
|
-
spinner.succeed("AI suggestion ready (review before saving)");
|
|
552
|
-
} else {
|
|
553
|
-
spinner.fail("AI returned no suggestion");
|
|
554
|
-
}
|
|
555
|
-
return suggestion;
|
|
556
|
-
}
|
|
557
|
-
async function projectAddCommand(options = {}) {
|
|
558
|
-
let name;
|
|
559
|
-
let displayName;
|
|
560
|
-
let repositories;
|
|
561
|
-
let description;
|
|
562
|
-
if (options.interactive === false) {
|
|
563
|
-
const errors = [];
|
|
564
|
-
const trimmedName = options.name?.trim();
|
|
565
|
-
const trimmedDisplayName = options.displayName?.trim();
|
|
566
|
-
if (!trimmedName) {
|
|
567
|
-
errors.push("--name is required");
|
|
568
|
-
} else if (!validateSlug(trimmedName)) {
|
|
569
|
-
errors.push("--name must be a slug (lowercase, numbers, hyphens only)");
|
|
570
|
-
}
|
|
571
|
-
if (!trimmedDisplayName) {
|
|
572
|
-
errors.push("--display-name is required");
|
|
573
|
-
}
|
|
574
|
-
if (!options.paths || options.paths.length === 0) {
|
|
575
|
-
errors.push("--path is required (at least one)");
|
|
576
|
-
}
|
|
577
|
-
if (options.paths) {
|
|
578
|
-
const spinner = options.paths.length > 1 ? createSpinner("Validating repository paths...").start() : null;
|
|
579
|
-
for (const path of options.paths) {
|
|
580
|
-
const resolved = resolve2(expandTilde(path.trim()));
|
|
581
|
-
const validation = await validateProjectPath(resolved);
|
|
582
|
-
if (!validation.ok) {
|
|
583
|
-
errors.push(`--path ${path}: ${validation.error.message}`);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
spinner?.succeed("Paths validated");
|
|
587
|
-
}
|
|
588
|
-
if (errors.length > 0 || !trimmedName || !trimmedDisplayName || !options.paths) {
|
|
589
|
-
showError("Validation failed");
|
|
590
|
-
for (const e of errors) {
|
|
591
|
-
log.item(error(e));
|
|
592
|
-
}
|
|
593
|
-
console.log("");
|
|
594
|
-
exitWithCode(EXIT_ERROR);
|
|
595
|
-
}
|
|
596
|
-
name = trimmedName;
|
|
597
|
-
displayName = trimmedDisplayName;
|
|
598
|
-
repositories = options.paths.map((p) => {
|
|
599
|
-
const resolved = resolve2(expandTilde(p.trim()));
|
|
600
|
-
const repo = { name: basename(resolved), path: resolved };
|
|
601
|
-
if (options.checkScript) repo.checkScript = options.checkScript;
|
|
602
|
-
return repo;
|
|
603
|
-
});
|
|
604
|
-
const trimmedDesc = options.description?.trim();
|
|
605
|
-
description = trimmedDesc === "" ? void 0 : trimmedDesc;
|
|
606
|
-
} else {
|
|
607
|
-
name = await getPrompt().input({
|
|
608
|
-
message: "Project name (slug):",
|
|
609
|
-
default: options.name?.trim(),
|
|
610
|
-
validate: (v) => {
|
|
611
|
-
const trimmed = v.trim();
|
|
612
|
-
if (trimmed.length === 0) return "Name is required";
|
|
613
|
-
if (!validateSlug(trimmed)) return "Must be lowercase with hyphens only";
|
|
614
|
-
return true;
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
name = name.trim();
|
|
618
|
-
displayName = await getPrompt().input({
|
|
619
|
-
message: "Display name:",
|
|
620
|
-
default: options.displayName?.trim() ?? name,
|
|
621
|
-
validate: (v) => v.trim().length > 0 ? true : "Display name is required"
|
|
622
|
-
});
|
|
623
|
-
displayName = displayName.trim();
|
|
624
|
-
repositories = [];
|
|
625
|
-
if (options.paths) {
|
|
626
|
-
for (const p of options.paths) {
|
|
627
|
-
const resolved = resolve2(expandTilde(p.trim()));
|
|
628
|
-
const validation = await validateProjectPath(resolved);
|
|
629
|
-
if (validation.ok) {
|
|
630
|
-
repositories.push({ name: basename(resolved), path: resolved });
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
if (repositories.length === 0) {
|
|
635
|
-
const pathMethod = await getPrompt().select({
|
|
636
|
-
message: `${emoji.donut} How to specify repository path?`,
|
|
637
|
-
choices: [
|
|
638
|
-
{ label: "Browse filesystem", value: "browse", description: "Navigate from home folder" },
|
|
639
|
-
{ label: "Use current directory", value: "cwd", description: process.cwd() },
|
|
640
|
-
{ label: "Type path manually", value: "manual" }
|
|
641
|
-
]
|
|
642
|
-
});
|
|
643
|
-
let firstPath;
|
|
644
|
-
if (pathMethod === "browse") {
|
|
645
|
-
const browsed = await browseDirectory("Select repository directory:");
|
|
646
|
-
if (!browsed) {
|
|
647
|
-
showError("No directory selected");
|
|
648
|
-
exitWithCode(EXIT_ERROR);
|
|
649
|
-
}
|
|
650
|
-
firstPath = browsed;
|
|
651
|
-
} else if (pathMethod === "cwd") {
|
|
652
|
-
firstPath = process.cwd();
|
|
653
|
-
} else {
|
|
654
|
-
firstPath = await getPrompt().input({
|
|
655
|
-
message: "Repository path:",
|
|
656
|
-
default: process.cwd(),
|
|
657
|
-
validate: async (v) => {
|
|
658
|
-
const result = await validateProjectPath(v.trim());
|
|
659
|
-
return result.ok ? true : result.error.message;
|
|
660
|
-
}
|
|
661
|
-
});
|
|
662
|
-
firstPath = firstPath.trim();
|
|
663
|
-
}
|
|
664
|
-
const resolved = resolve2(expandTilde(firstPath));
|
|
665
|
-
const validation = await validateProjectPath(resolved);
|
|
666
|
-
if (!validation.ok) {
|
|
667
|
-
showError(`Invalid path: ${validation.error.message}`);
|
|
668
|
-
exitWithCode(EXIT_ERROR);
|
|
669
|
-
}
|
|
670
|
-
repositories.push({ name: basename(resolved), path: resolved });
|
|
671
|
-
}
|
|
672
|
-
const firstRepo = repositories[0];
|
|
673
|
-
if (firstRepo) {
|
|
674
|
-
if (!isGitRepo2(firstRepo.path)) {
|
|
675
|
-
showWarning("Path is not a git repository");
|
|
676
|
-
}
|
|
677
|
-
if (!hasAiInstructions(firstRepo.path)) {
|
|
678
|
-
showTip("Add CLAUDE.md or .github/copilot-instructions.md for better AI assistance");
|
|
679
|
-
}
|
|
680
|
-
log.info(`
|
|
681
|
-
Configuring: ${firstRepo.name ?? basename(firstRepo.path)}`);
|
|
682
|
-
repositories[0] = await addCheckScriptToRepository(firstRepo);
|
|
683
|
-
}
|
|
684
|
-
let addMore = true;
|
|
685
|
-
while (addMore) {
|
|
686
|
-
const addAction = await getPrompt().select({
|
|
687
|
-
message: `${emoji.donut} Add another repository?`,
|
|
688
|
-
choices: [
|
|
689
|
-
{ label: "No, done adding repositories", value: "done" },
|
|
690
|
-
{ label: "Browse filesystem", value: "browse" },
|
|
691
|
-
{ label: "Type path manually", value: "manual" }
|
|
692
|
-
]
|
|
693
|
-
});
|
|
694
|
-
if (addAction === "done") {
|
|
695
|
-
addMore = false;
|
|
696
|
-
} else if (addAction === "browse") {
|
|
697
|
-
const browsed = await browseDirectory("Select repository directory:");
|
|
698
|
-
if (browsed) {
|
|
699
|
-
const resolved = resolve2(expandTilde(browsed));
|
|
700
|
-
const validation = await validateProjectPath(resolved);
|
|
701
|
-
if (validation.ok) {
|
|
702
|
-
const newRepo = { name: basename(resolved), path: resolved };
|
|
703
|
-
log.success(`Added: ${newRepo.name}`);
|
|
704
|
-
const repoWithScripts = await addCheckScriptToRepository(newRepo);
|
|
705
|
-
repositories.push(repoWithScripts);
|
|
706
|
-
} else {
|
|
707
|
-
log.error(`Invalid path: ${validation.error.message}`);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
} else {
|
|
711
|
-
const additionalPath = await getPrompt().input({
|
|
712
|
-
message: "Repository path:"
|
|
713
|
-
});
|
|
714
|
-
if (additionalPath.trim() === "") {
|
|
715
|
-
addMore = false;
|
|
716
|
-
} else {
|
|
717
|
-
const resolved = resolve2(expandTilde(additionalPath.trim()));
|
|
718
|
-
const validation = await validateProjectPath(resolved);
|
|
719
|
-
if (validation.ok) {
|
|
720
|
-
const newRepo = { name: basename(resolved), path: resolved };
|
|
721
|
-
log.success(`Added: ${newRepo.name}`);
|
|
722
|
-
const repoWithScripts = await addCheckScriptToRepository(newRepo);
|
|
723
|
-
repositories.push(repoWithScripts);
|
|
724
|
-
} else {
|
|
725
|
-
log.error(`Invalid path: ${validation.error.message}`);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
description = await getPrompt().input({
|
|
731
|
-
message: "Description (optional):",
|
|
732
|
-
default: options.description?.trim()
|
|
733
|
-
});
|
|
734
|
-
const trimmedDescInteractive = description.trim();
|
|
735
|
-
description = trimmedDescInteractive === "" ? void 0 : trimmedDescInteractive;
|
|
736
|
-
}
|
|
737
|
-
const project = {
|
|
738
|
-
name,
|
|
739
|
-
displayName,
|
|
740
|
-
repositories,
|
|
741
|
-
description
|
|
742
|
-
};
|
|
743
|
-
const createR = await wrapAsync(() => createProject(project), ensureError);
|
|
744
|
-
if (!createR.ok) {
|
|
745
|
-
if (createR.error instanceof ProjectExistsError) {
|
|
746
|
-
showError(`Project "${name}" already exists.`);
|
|
747
|
-
showNextStep(`ralphctl project remove ${name}`, "remove existing project first");
|
|
748
|
-
log.newline();
|
|
749
|
-
} else {
|
|
750
|
-
throw createR.error;
|
|
751
|
-
}
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
const created = createR.value;
|
|
755
|
-
showSuccess("Project added!", [
|
|
756
|
-
["Name", created.name],
|
|
757
|
-
["Display Name", created.displayName]
|
|
758
|
-
]);
|
|
759
|
-
if (created.description) {
|
|
760
|
-
console.log(field("Description", created.description));
|
|
761
|
-
}
|
|
762
|
-
console.log(field("Repositories", ""));
|
|
763
|
-
for (const repo of created.repositories) {
|
|
764
|
-
log.item(`${repo.name} \u2192 ${repo.path}`);
|
|
765
|
-
if (repo.checkScript) {
|
|
766
|
-
console.log(` Check: ${repo.checkScript}`);
|
|
767
|
-
} else {
|
|
768
|
-
console.log(` Check: ${muted("(not configured)")}`);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
console.log("");
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
export {
|
|
775
|
-
PromptCancelledError,
|
|
776
|
-
escapableSelect,
|
|
777
|
-
detectCheckScriptCandidates,
|
|
778
|
-
suggestCheckScript,
|
|
779
|
-
discoverCheckScriptWithAi,
|
|
780
|
-
getAllSchemaEntries,
|
|
781
|
-
getAllConfigSchemaEntries,
|
|
782
|
-
getConfigDefaultValue,
|
|
783
|
-
validateConfigValue,
|
|
784
|
-
parseConfigValue,
|
|
785
|
-
addCheckScriptToRepository,
|
|
786
|
-
projectAddCommand
|
|
787
|
-
};
|