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.
Files changed (130) hide show
  1. package/README.md +58 -24
  2. package/dist/add-HGJCLWED.mjs +14 -0
  3. package/dist/add-MRGCS3US.mjs +14 -0
  4. package/dist/chunk-6PYTKGB5.mjs +316 -0
  5. package/dist/chunk-7TG3EAQ2.mjs +20 -0
  6. package/dist/chunk-EKMZZRWI.mjs +521 -0
  7. package/dist/chunk-JON4GCLR.mjs +59 -0
  8. package/dist/chunk-LOR7QBXX.mjs +3683 -0
  9. package/dist/chunk-MNMQC36F.mjs +556 -0
  10. package/dist/chunk-MRKOFVTM.mjs +537 -0
  11. package/dist/chunk-NTWO2LXB.mjs +52 -0
  12. package/dist/chunk-QBXHAXHI.mjs +562 -0
  13. package/dist/chunk-WGHJI3OI.mjs +214 -0
  14. package/dist/cli.mjs +4245 -0
  15. package/dist/create-MG7E7PLQ.mjs +10 -0
  16. package/dist/handle-UG5M2OON.mjs +22 -0
  17. package/dist/multiline-OHSNFCRG.mjs +40 -0
  18. package/dist/project-NT3L4FTB.mjs +28 -0
  19. package/dist/resolver-WSFWKACM.mjs +153 -0
  20. package/dist/sprint-4VHDLGFN.mjs +37 -0
  21. package/dist/wizard-LRELAN2J.mjs +196 -0
  22. package/package.json +19 -28
  23. package/CHANGELOG.md +0 -94
  24. package/bin/ralphctl +0 -13
  25. package/src/ai/executor.ts +0 -973
  26. package/src/ai/lifecycle.ts +0 -45
  27. package/src/ai/parser.ts +0 -40
  28. package/src/ai/permissions.ts +0 -207
  29. package/src/ai/process-manager.ts +0 -248
  30. package/src/ai/prompts/index.ts +0 -89
  31. package/src/ai/rate-limiter.ts +0 -89
  32. package/src/ai/runner.ts +0 -478
  33. package/src/ai/session.ts +0 -319
  34. package/src/ai/task-context.ts +0 -270
  35. package/src/cli-metadata.ts +0 -7
  36. package/src/cli.ts +0 -65
  37. package/src/commands/completion/index.ts +0 -33
  38. package/src/commands/config/config.ts +0 -58
  39. package/src/commands/config/index.ts +0 -33
  40. package/src/commands/dashboard/dashboard.ts +0 -5
  41. package/src/commands/dashboard/index.ts +0 -6
  42. package/src/commands/doctor/doctor.ts +0 -271
  43. package/src/commands/doctor/index.ts +0 -25
  44. package/src/commands/progress/index.ts +0 -25
  45. package/src/commands/progress/log.ts +0 -64
  46. package/src/commands/progress/show.ts +0 -14
  47. package/src/commands/project/add.ts +0 -336
  48. package/src/commands/project/index.ts +0 -104
  49. package/src/commands/project/list.ts +0 -31
  50. package/src/commands/project/remove.ts +0 -43
  51. package/src/commands/project/repo.ts +0 -118
  52. package/src/commands/project/show.ts +0 -49
  53. package/src/commands/sprint/close.ts +0 -180
  54. package/src/commands/sprint/context.ts +0 -109
  55. package/src/commands/sprint/create.ts +0 -60
  56. package/src/commands/sprint/current.ts +0 -75
  57. package/src/commands/sprint/delete.ts +0 -72
  58. package/src/commands/sprint/health.ts +0 -229
  59. package/src/commands/sprint/ideate.ts +0 -496
  60. package/src/commands/sprint/index.ts +0 -226
  61. package/src/commands/sprint/list.ts +0 -86
  62. package/src/commands/sprint/plan-utils.ts +0 -207
  63. package/src/commands/sprint/plan.ts +0 -549
  64. package/src/commands/sprint/refine.ts +0 -359
  65. package/src/commands/sprint/requirements.ts +0 -58
  66. package/src/commands/sprint/show.ts +0 -140
  67. package/src/commands/sprint/start.ts +0 -119
  68. package/src/commands/sprint/switch.ts +0 -20
  69. package/src/commands/task/add.ts +0 -316
  70. package/src/commands/task/import.ts +0 -150
  71. package/src/commands/task/index.ts +0 -123
  72. package/src/commands/task/list.ts +0 -145
  73. package/src/commands/task/next.ts +0 -45
  74. package/src/commands/task/remove.ts +0 -47
  75. package/src/commands/task/reorder.ts +0 -45
  76. package/src/commands/task/show.ts +0 -111
  77. package/src/commands/task/status.ts +0 -99
  78. package/src/commands/ticket/add.ts +0 -265
  79. package/src/commands/ticket/edit.ts +0 -166
  80. package/src/commands/ticket/index.ts +0 -114
  81. package/src/commands/ticket/list.ts +0 -128
  82. package/src/commands/ticket/refine-utils.ts +0 -89
  83. package/src/commands/ticket/refine.ts +0 -268
  84. package/src/commands/ticket/remove.ts +0 -48
  85. package/src/commands/ticket/show.ts +0 -74
  86. package/src/completion/handle.ts +0 -30
  87. package/src/completion/resolver.ts +0 -241
  88. package/src/interactive/dashboard.ts +0 -268
  89. package/src/interactive/escapable.ts +0 -81
  90. package/src/interactive/file-browser.ts +0 -153
  91. package/src/interactive/index.ts +0 -429
  92. package/src/interactive/menu.ts +0 -403
  93. package/src/interactive/selectors.ts +0 -273
  94. package/src/interactive/wizard.ts +0 -221
  95. package/src/providers/claude.ts +0 -53
  96. package/src/providers/copilot.ts +0 -86
  97. package/src/providers/index.ts +0 -43
  98. package/src/providers/types.ts +0 -85
  99. package/src/schemas/index.ts +0 -130
  100. package/src/store/config.ts +0 -74
  101. package/src/store/progress.ts +0 -230
  102. package/src/store/project.ts +0 -276
  103. package/src/store/sprint.ts +0 -229
  104. package/src/store/task.ts +0 -443
  105. package/src/store/ticket.ts +0 -178
  106. package/src/theme/index.ts +0 -215
  107. package/src/theme/ui.ts +0 -872
  108. package/src/utils/detect-scripts.ts +0 -247
  109. package/src/utils/editor-input.ts +0 -41
  110. package/src/utils/editor.ts +0 -37
  111. package/src/utils/exit-codes.ts +0 -27
  112. package/src/utils/file-lock.ts +0 -135
  113. package/src/utils/git.ts +0 -185
  114. package/src/utils/ids.ts +0 -37
  115. package/src/utils/issue-fetch.ts +0 -244
  116. package/src/utils/json-extract.ts +0 -62
  117. package/src/utils/multiline.ts +0 -61
  118. package/src/utils/path-selector.ts +0 -236
  119. package/src/utils/paths.ts +0 -108
  120. package/src/utils/provider.ts +0 -34
  121. package/src/utils/requirements-export.ts +0 -63
  122. package/src/utils/storage.ts +0 -107
  123. package/tsconfig.json +0 -25
  124. /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
  125. /package/{src/ai → dist}/prompts/ideate.md +0 -0
  126. /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
  127. /package/{src/ai → dist}/prompts/plan-common.md +0 -0
  128. /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
  129. /package/{src/ai → dist}/prompts/task-execution.md +0 -0
  130. /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
+ };