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,521 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ConfigSchema,
4
+ FileNotFoundError,
5
+ SprintSchema,
6
+ TasksSchema,
7
+ ValidationError,
8
+ appendToFile,
9
+ assertSafeCwd,
10
+ ensureDir,
11
+ fileExists,
12
+ getConfigPath,
13
+ getProgressFilePath,
14
+ getSprintDir,
15
+ getSprintFilePath,
16
+ getSprintsDir,
17
+ getTasksFilePath,
18
+ listDirs,
19
+ readTextFile,
20
+ readValidatedJson,
21
+ removeDir,
22
+ writeValidatedJson
23
+ } from "./chunk-6PYTKGB5.mjs";
24
+ import {
25
+ log
26
+ } from "./chunk-QBXHAXHI.mjs";
27
+
28
+ // src/store/config.ts
29
+ var DEFAULT_CONFIG = {
30
+ currentSprint: null,
31
+ aiProvider: null,
32
+ editor: null
33
+ };
34
+ async function getConfig() {
35
+ const configPath = getConfigPath();
36
+ if (!await fileExists(configPath)) {
37
+ return DEFAULT_CONFIG;
38
+ }
39
+ return readValidatedJson(configPath, ConfigSchema);
40
+ }
41
+ async function saveConfig(config) {
42
+ await writeValidatedJson(getConfigPath(), config, ConfigSchema);
43
+ }
44
+ async function getCurrentSprint() {
45
+ const config = await getConfig();
46
+ return config.currentSprint;
47
+ }
48
+ async function setCurrentSprint(sprintId) {
49
+ const config = await getConfig();
50
+ config.currentSprint = sprintId;
51
+ await saveConfig(config);
52
+ }
53
+ async function getAiProvider() {
54
+ const config = await getConfig();
55
+ return config.aiProvider ?? null;
56
+ }
57
+ async function setAiProvider(provider) {
58
+ const config = await getConfig();
59
+ config.aiProvider = provider;
60
+ await saveConfig(config);
61
+ }
62
+ async function getEditor() {
63
+ const config = await getConfig();
64
+ return config.editor ?? null;
65
+ }
66
+ async function setEditor(editor) {
67
+ const config = await getConfig();
68
+ config.editor = editor;
69
+ await saveConfig(config);
70
+ }
71
+
72
+ // src/utils/ids.ts
73
+ import { randomBytes } from "crypto";
74
+ function generateUuid8() {
75
+ return randomBytes(4).toString("hex");
76
+ }
77
+ function slugify(input, maxLength = 40) {
78
+ return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, maxLength).replace(/-$/, "");
79
+ }
80
+ function generateSprintId(name) {
81
+ const now = /* @__PURE__ */ new Date();
82
+ const date = now.toISOString().slice(0, 10).replace(/-/g, "");
83
+ const time = now.toISOString().slice(11, 19).replace(/:/g, "");
84
+ const slug = name ? slugify(name) : generateUuid8();
85
+ return `${date}-${time}-${slug || generateUuid8()}`;
86
+ }
87
+
88
+ // src/store/progress.ts
89
+ import { execSync } from "child_process";
90
+
91
+ // src/utils/file-lock.ts
92
+ import { mkdir, readFile, unlink, writeFile } from "fs/promises";
93
+ import { dirname } from "path";
94
+ var LockAcquisitionError = class extends Error {
95
+ lockPath;
96
+ constructor(message, lockPath) {
97
+ super(message);
98
+ this.name = "LockAcquisitionError";
99
+ this.lockPath = lockPath;
100
+ }
101
+ };
102
+ var parsed = parseInt(process.env["RALPHCTL_LOCK_TIMEOUT_MS"] ?? "", 10);
103
+ var LOCK_TIMEOUT_MS = parsed > 0 && parsed <= 36e5 ? parsed : 3e4;
104
+ var RETRY_DELAY_MS = 50;
105
+ var MAX_RETRIES = 100;
106
+ function getLockPath(filePath) {
107
+ return `${filePath}.lock`;
108
+ }
109
+ async function sleep(ms) {
110
+ return new Promise((resolve) => setTimeout(resolve, ms));
111
+ }
112
+ async function isLockStale(lockPath) {
113
+ try {
114
+ const content = await readFile(lockPath, "utf-8");
115
+ const info = JSON.parse(content);
116
+ const age = Date.now() - info.timestamp;
117
+ if (age > LOCK_TIMEOUT_MS) {
118
+ return true;
119
+ }
120
+ try {
121
+ process.kill(info.pid, 0);
122
+ return false;
123
+ } catch {
124
+ return true;
125
+ }
126
+ } catch {
127
+ return true;
128
+ }
129
+ }
130
+ async function acquireLock(filePath) {
131
+ const lockPath = getLockPath(filePath);
132
+ const lockInfo = {
133
+ pid: process.pid,
134
+ timestamp: Date.now()
135
+ };
136
+ let retries = 0;
137
+ while (retries < MAX_RETRIES) {
138
+ try {
139
+ await mkdir(dirname(lockPath), { recursive: true });
140
+ await writeFile(lockPath, JSON.stringify(lockInfo), { flag: "wx", mode: 384 });
141
+ return async () => {
142
+ try {
143
+ await unlink(lockPath);
144
+ } catch {
145
+ }
146
+ };
147
+ } catch (err) {
148
+ if (err instanceof Error && "code" in err && err.code === "EEXIST") {
149
+ if (await isLockStale(lockPath)) {
150
+ try {
151
+ await unlink(lockPath);
152
+ } catch {
153
+ }
154
+ continue;
155
+ }
156
+ retries++;
157
+ await sleep(RETRY_DELAY_MS);
158
+ continue;
159
+ }
160
+ throw err;
161
+ }
162
+ }
163
+ throw new LockAcquisitionError(`Failed to acquire lock after ${String(MAX_RETRIES)} retries`, lockPath);
164
+ }
165
+ async function withFileLock(filePath, fn) {
166
+ const release = await acquireLock(filePath);
167
+ try {
168
+ return await fn();
169
+ } finally {
170
+ await release();
171
+ }
172
+ }
173
+
174
+ // src/store/progress.ts
175
+ async function logProgress(message, options = {}) {
176
+ const id = await resolveSprintId(options.sprintId);
177
+ const sprint = await getSprint(id);
178
+ assertSprintStatus(sprint, ["active"], "log progress");
179
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
180
+ const projectMarker = options.projectPath ? `**Project:** ${options.projectPath}
181
+
182
+ ` : "";
183
+ const entry = `## ${timestamp}
184
+
185
+ ${projectMarker}${message}
186
+
187
+ ---
188
+
189
+ `;
190
+ const progressPath = getProgressFilePath(id);
191
+ await withFileLock(progressPath, async () => {
192
+ await appendToFile(progressPath, entry);
193
+ });
194
+ }
195
+ function isExecError(err) {
196
+ return err instanceof Error && typeof err["status"] === "number";
197
+ }
198
+ function isNodeError(err) {
199
+ return err instanceof Error && typeof err["code"] === "string";
200
+ }
201
+ function getGitCommitInfo(projectPath) {
202
+ try {
203
+ assertSafeCwd(projectPath);
204
+ const output = execSync("git log -1 --pretty=format:%H\\ %s", {
205
+ cwd: projectPath,
206
+ encoding: "utf-8",
207
+ stdio: ["pipe", "pipe", "pipe"]
208
+ }).trim();
209
+ const spaceIndex = output.indexOf(" ");
210
+ return {
211
+ hash: output.slice(0, spaceIndex),
212
+ message: output.slice(spaceIndex + 1)
213
+ };
214
+ } catch (err) {
215
+ if (isExecError(err) && err.status === 128) {
216
+ return null;
217
+ }
218
+ if (isNodeError(err) && err.code === "ENOENT") {
219
+ return null;
220
+ }
221
+ const detail = err instanceof Error ? err.message : String(err);
222
+ log.warn(`Failed to get git info for ${projectPath}: ${detail}`);
223
+ return null;
224
+ }
225
+ }
226
+ async function logBaselines(options) {
227
+ const { sprintId, sprintName, projectPaths } = options;
228
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
229
+ const lines = [
230
+ `## ${timestamp}`,
231
+ "",
232
+ "### Sprint Baseline State",
233
+ "",
234
+ `Sprint: ${sprintName} (${sprintId})`,
235
+ `Activated: ${timestamp}`,
236
+ "",
237
+ "#### Project Git State at Activation",
238
+ ""
239
+ ];
240
+ const uniquePaths = [...new Set(projectPaths)];
241
+ for (const path of uniquePaths) {
242
+ const commitInfo = getGitCommitInfo(path);
243
+ if (commitInfo) {
244
+ lines.push(`- **${path}**`);
245
+ lines.push(` \`${commitInfo.hash} ${commitInfo.message}\``);
246
+ } else {
247
+ lines.push(`- **${path}**`);
248
+ lines.push(` *(not a git repository or unable to retrieve state)*`);
249
+ }
250
+ }
251
+ lines.push("");
252
+ lines.push("---");
253
+ lines.push("");
254
+ await appendToFile(getProgressFilePath(sprintId), lines.join("\n"));
255
+ }
256
+ async function getProgress(sprintId) {
257
+ const id = await resolveSprintId(sprintId);
258
+ try {
259
+ return await readTextFile(getProgressFilePath(id));
260
+ } catch (err) {
261
+ if (err instanceof FileNotFoundError) {
262
+ return "";
263
+ }
264
+ throw err;
265
+ }
266
+ }
267
+ function summarizeProgressForContext(progress, projectPath, maxEntries = 3) {
268
+ const filtered = filterProgressByProject(progress, projectPath);
269
+ if (!filtered.trim()) {
270
+ return "";
271
+ }
272
+ const entries = filtered.split(/\n---\n/).filter((e) => e.trim());
273
+ const recent = entries.slice(-maxEntries);
274
+ const summaries = [];
275
+ for (const entry of recent) {
276
+ const headerMatch = /^##\s+(.+)$/m.exec(entry);
277
+ const header = headerMatch?.[1] ?? "Unknown entry";
278
+ const learnings = extractSection(entry, "Learnings and Context");
279
+ const notes = extractSection(entry, "Notes for Next Tasks");
280
+ if (learnings || notes) {
281
+ const parts = [`**${header}**`];
282
+ if (learnings) {
283
+ parts.push(`**Learnings:** ${learnings}`);
284
+ }
285
+ if (notes) {
286
+ parts.push(`**Notes for next tasks:** ${notes}`);
287
+ }
288
+ summaries.push(parts.join("\n"));
289
+ }
290
+ }
291
+ if (summaries.length === 0) {
292
+ return "";
293
+ }
294
+ return summaries.join("\n\n");
295
+ }
296
+ function extractSection(entry, sectionName) {
297
+ const regex = new RegExp(`###\\s+${sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\n([\\s\\S]*?)(?=###|$)`);
298
+ const match = regex.exec(entry);
299
+ if (!match?.[1]) return null;
300
+ const content = match[1].trim();
301
+ return content || null;
302
+ }
303
+ function filterProgressByProject(progress, projectPath) {
304
+ if (!progress.trim()) {
305
+ return "";
306
+ }
307
+ const entries = progress.split(/\n---\n/).filter((e) => e.trim());
308
+ const filtered = entries.filter((entry) => {
309
+ const visibleMatch = /\*\*Project:\*\*\s*(.+?)(?:\n|$)/.exec(entry);
310
+ if (visibleMatch?.[1]) {
311
+ return visibleMatch[1].trim() === projectPath;
312
+ }
313
+ const htmlMatch = /<!--\s*project:\s*(.+?)\s*-->/.exec(entry);
314
+ if (htmlMatch?.[1]) {
315
+ return htmlMatch[1] === projectPath;
316
+ }
317
+ return true;
318
+ });
319
+ if (filtered.length === 0) {
320
+ return "";
321
+ }
322
+ return filtered.join("\n---\n") + "\n\n---\n\n";
323
+ }
324
+
325
+ // src/store/sprint.ts
326
+ var SprintNotFoundError = class extends Error {
327
+ sprintId;
328
+ constructor(sprintId) {
329
+ super(`Sprint not found: ${sprintId}`);
330
+ this.name = "SprintNotFoundError";
331
+ this.sprintId = sprintId;
332
+ }
333
+ };
334
+ var SprintStatusError = class extends Error {
335
+ currentStatus;
336
+ operation;
337
+ constructor(message, currentStatus, operation) {
338
+ super(message);
339
+ this.name = "SprintStatusError";
340
+ this.currentStatus = currentStatus;
341
+ this.operation = operation;
342
+ }
343
+ };
344
+ var NoCurrentSprintError = class extends Error {
345
+ constructor() {
346
+ super("No sprint specified and no current sprint set.");
347
+ this.name = "NoCurrentSprintError";
348
+ }
349
+ };
350
+ function assertSprintStatus(sprint, allowedStatuses, operation) {
351
+ if (!allowedStatuses.includes(sprint.status)) {
352
+ const statusText = allowedStatuses.join(" or ");
353
+ const hints = {
354
+ "add tickets": "Close the current sprint and create a new one for additional work.",
355
+ "remove tickets": "Sprint must be in draft status to remove tickets.",
356
+ "add tasks": "Close the current sprint and create a new one for additional work.",
357
+ "remove tasks": "Sprint must be in draft status to remove tasks.",
358
+ "reorder tasks": "Sprint must be in draft status to reorder tasks.",
359
+ refine: "Refinement can only be done on draft sprints.",
360
+ plan: "Planning can only be done on draft sprints.",
361
+ activate: "Sprint must be in draft status to activate.",
362
+ start: "Sprint must be draft or active to start.",
363
+ "update task status": "Task status can only be updated during active execution.",
364
+ "log progress": "Progress can only be logged during active execution.",
365
+ close: "Sprint must be active to close."
366
+ };
367
+ const hint = hints[operation] ?? "";
368
+ const hintText = hint ? `
369
+ Hint: ${hint}` : "";
370
+ throw new SprintStatusError(
371
+ `Cannot ${operation}: sprint status is '${sprint.status}' (must be ${statusText}).${hintText}`,
372
+ sprint.status,
373
+ operation
374
+ );
375
+ }
376
+ }
377
+ async function createSprint(name) {
378
+ const id = generateSprintId(name);
379
+ const now = (/* @__PURE__ */ new Date()).toISOString();
380
+ const displayName = name ?? id.slice(16);
381
+ const sprint = {
382
+ id,
383
+ name: displayName,
384
+ status: "draft",
385
+ createdAt: now,
386
+ activatedAt: null,
387
+ closedAt: null,
388
+ tickets: [],
389
+ checkRanAt: {},
390
+ branch: null
391
+ };
392
+ const sprintDir = getSprintDir(id);
393
+ await ensureDir(sprintDir);
394
+ await writeValidatedJson(getSprintFilePath(id), sprint, SprintSchema);
395
+ await writeValidatedJson(getTasksFilePath(id), [], TasksSchema);
396
+ await appendToFile(getProgressFilePath(id), `# Sprint: ${displayName}
397
+
398
+ Created: ${now}
399
+
400
+ ---
401
+
402
+ `);
403
+ return sprint;
404
+ }
405
+ async function findActiveSprint() {
406
+ const sprints = await listSprints();
407
+ return sprints.find((s) => s.status === "active") ?? null;
408
+ }
409
+ async function getSprint(sprintId) {
410
+ const sprintPath = getSprintFilePath(sprintId);
411
+ if (!await fileExists(sprintPath)) {
412
+ throw new SprintNotFoundError(sprintId);
413
+ }
414
+ return readValidatedJson(sprintPath, SprintSchema);
415
+ }
416
+ async function saveSprint(sprint) {
417
+ await writeValidatedJson(getSprintFilePath(sprint.id), sprint, SprintSchema);
418
+ }
419
+ async function listSprints() {
420
+ const sprintsDir = getSprintsDir();
421
+ const dirs = await listDirs(sprintsDir);
422
+ const sprints = [];
423
+ for (const dir of dirs) {
424
+ try {
425
+ const sprint = await getSprint(dir);
426
+ sprints.push(sprint);
427
+ } catch (err) {
428
+ if (err instanceof ValidationError || err instanceof SprintNotFoundError) {
429
+ continue;
430
+ }
431
+ throw err;
432
+ }
433
+ }
434
+ return sprints.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
435
+ }
436
+ async function activateSprint(sprintId) {
437
+ const sprint = await getSprint(sprintId);
438
+ assertSprintStatus(sprint, ["draft"], "activate");
439
+ sprint.status = "active";
440
+ sprint.activatedAt = (/* @__PURE__ */ new Date()).toISOString();
441
+ await saveSprint(sprint);
442
+ const tasks = await readValidatedJson(getTasksFilePath(sprintId), TasksSchema);
443
+ const projectPaths = tasks.map((t) => t.projectPath).filter((p) => !!p);
444
+ if (projectPaths.length > 0) {
445
+ await logBaselines({
446
+ sprintId,
447
+ sprintName: sprint.name,
448
+ projectPaths
449
+ });
450
+ }
451
+ return sprint;
452
+ }
453
+ async function closeSprint(sprintId) {
454
+ const sprint = await getSprint(sprintId);
455
+ assertSprintStatus(sprint, ["active"], "close");
456
+ sprint.status = "closed";
457
+ sprint.closedAt = (/* @__PURE__ */ new Date()).toISOString();
458
+ sprint.checkRanAt = {};
459
+ await saveSprint(sprint);
460
+ return sprint;
461
+ }
462
+ async function deleteSprint(sprintId) {
463
+ const sprint = await getSprint(sprintId);
464
+ const sprintDir = getSprintDir(sprintId);
465
+ await removeDir(sprintDir);
466
+ return sprint;
467
+ }
468
+ async function getCurrentSprintOrThrow() {
469
+ const currentSprintId = await getCurrentSprint();
470
+ if (!currentSprintId) {
471
+ throw new Error('No current sprint. Use "ralphctl sprint create" to create one.');
472
+ }
473
+ return getSprint(currentSprintId);
474
+ }
475
+ async function getActiveSprintOrThrow() {
476
+ const activeSprint = await findActiveSprint();
477
+ if (!activeSprint) {
478
+ throw new Error('No active sprint. Use "ralphctl sprint start" to start a draft sprint.');
479
+ }
480
+ return activeSprint;
481
+ }
482
+ async function resolveSprintId(sprintId) {
483
+ if (sprintId) {
484
+ return sprintId;
485
+ }
486
+ const currentSprintId = await getCurrentSprint();
487
+ if (!currentSprintId) {
488
+ throw new NoCurrentSprintError();
489
+ }
490
+ return currentSprintId;
491
+ }
492
+
493
+ export {
494
+ getConfig,
495
+ getCurrentSprint,
496
+ setCurrentSprint,
497
+ getAiProvider,
498
+ setAiProvider,
499
+ getEditor,
500
+ setEditor,
501
+ generateUuid8,
502
+ withFileLock,
503
+ logProgress,
504
+ getProgress,
505
+ summarizeProgressForContext,
506
+ SprintNotFoundError,
507
+ SprintStatusError,
508
+ NoCurrentSprintError,
509
+ assertSprintStatus,
510
+ createSprint,
511
+ findActiveSprint,
512
+ getSprint,
513
+ saveSprint,
514
+ listSprints,
515
+ activateSprint,
516
+ closeSprint,
517
+ deleteSprint,
518
+ getCurrentSprintOrThrow,
519
+ getActiveSprintOrThrow,
520
+ resolveSprintId
521
+ };
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createSprint,
4
+ setCurrentSprint
5
+ } from "./chunk-EKMZZRWI.mjs";
6
+ import {
7
+ emoji,
8
+ field,
9
+ formatSprintStatus,
10
+ icons,
11
+ showNextStep,
12
+ showRandomQuote,
13
+ showSuccess
14
+ } from "./chunk-QBXHAXHI.mjs";
15
+
16
+ // src/commands/sprint/create.ts
17
+ import { confirm, input } from "@inquirer/prompts";
18
+ async function sprintCreateCommand(options = {}) {
19
+ let name;
20
+ if (options.interactive === false) {
21
+ const trimmed = options.name?.trim();
22
+ name = trimmed && trimmed.length > 0 ? trimmed : void 0;
23
+ } else {
24
+ const inputName = await input({
25
+ message: `${icons.sprint} Sprint name (optional):`,
26
+ default: options.name?.trim()
27
+ });
28
+ const trimmed = inputName.trim();
29
+ name = trimmed.length > 0 ? trimmed : void 0;
30
+ }
31
+ const sprint = await createSprint(name);
32
+ let setAsCurrent = true;
33
+ if (options.interactive) {
34
+ setAsCurrent = await confirm({
35
+ message: `${emoji.donut} Set as current sprint?`,
36
+ default: true
37
+ });
38
+ }
39
+ if (setAsCurrent) {
40
+ await setCurrentSprint(sprint.id);
41
+ }
42
+ showSuccess("Sprint created!", [
43
+ ["ID", sprint.id],
44
+ ["Name", sprint.name],
45
+ ["Status", formatSprintStatus(sprint.status)]
46
+ ]);
47
+ showRandomQuote();
48
+ if (setAsCurrent) {
49
+ console.log(field("Current", "Yes (this sprint is now active target)"));
50
+ showNextStep("ralphctl ticket add --project <name>", "add tickets to this sprint");
51
+ } else {
52
+ console.log(field("Current", "No"));
53
+ showNextStep(`ralphctl sprint current ${sprint.id}`, "set as current later");
54
+ }
55
+ }
56
+
57
+ export {
58
+ sprintCreateCommand
59
+ };