ralphctl 0.1.2 → 0.1.3

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.
@@ -2,11 +2,13 @@
2
2
  import {
3
3
  addSingleTicketInteractive,
4
4
  ticketAddCommand
5
- } from "./chunk-MNMQC36F.mjs";
5
+ } from "./chunk-F2MMCTB5.mjs";
6
6
  import "./chunk-7TG3EAQ2.mjs";
7
- import "./chunk-WGHJI3OI.mjs";
8
- import "./chunk-EKMZZRWI.mjs";
9
- import "./chunk-6PYTKGB5.mjs";
7
+ import "./chunk-PDI6HBZ7.mjs";
8
+ import "./chunk-LFDW6MWF.mjs";
9
+ import "./chunk-OEUJDSHY.mjs";
10
+ import "./chunk-W3TY22IS.mjs";
11
+ import "./chunk-EDJX7TT6.mjs";
10
12
  import "./chunk-QBXHAXHI.mjs";
11
13
  export {
12
14
  addSingleTicketInteractive,
@@ -2,11 +2,13 @@
2
2
  import {
3
3
  addCheckScriptToRepository,
4
4
  projectAddCommand
5
- } from "./chunk-MRKOFVTM.mjs";
6
- import "./chunk-NTWO2LXB.mjs";
5
+ } from "./chunk-YIB7QYU4.mjs";
6
+ import "./chunk-7LZ6GOGN.mjs";
7
7
  import "./chunk-7TG3EAQ2.mjs";
8
- import "./chunk-WGHJI3OI.mjs";
9
- import "./chunk-6PYTKGB5.mjs";
8
+ import "./chunk-PDI6HBZ7.mjs";
9
+ import "./chunk-OEUJDSHY.mjs";
10
+ import "./chunk-W3TY22IS.mjs";
11
+ import "./chunk-EDJX7TT6.mjs";
10
12
  import "./chunk-QBXHAXHI.mjs";
11
13
  export {
12
14
  addCheckScriptToRepository,
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ ensureError,
4
+ wrapAsync
5
+ } from "./chunk-OEUJDSHY.mjs";
2
6
 
3
7
  // src/interactive/escapable.ts
4
8
  import readline from "readline";
@@ -32,19 +36,16 @@ async function escapableSelect(config, options) {
32
36
  }
33
37
  };
34
38
  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);
39
+ const r = await wrapAsync(
40
+ () => select(withEscapeHint(config, options?.escLabel ?? "back"), { signal: controller.signal }),
41
+ ensureError
42
+ );
43
+ process.stdin.removeListener("keypress", onKeypress);
44
+ if (!r.ok) {
45
+ if (r.error.name === "AbortPromptError") return null;
46
+ throw r.error;
47
47
  }
48
+ return r.value;
48
49
  }
49
50
 
50
51
  export {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  createSprint,
4
4
  setCurrentSprint
5
- } from "./chunk-EKMZZRWI.mjs";
5
+ } from "./chunk-LFDW6MWF.mjs";
6
6
  import {
7
7
  emoji,
8
8
  field,
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/errors.ts
4
+ var DomainError = class extends Error {
5
+ cause;
6
+ constructor(message, cause) {
7
+ super(message, cause ? { cause } : void 0);
8
+ this.name = this.constructor.name;
9
+ this.cause = cause;
10
+ }
11
+ };
12
+ var IOError = class extends DomainError {
13
+ code = "IO_ERROR";
14
+ };
15
+ var StorageError = class extends DomainError {
16
+ code = "STORAGE_ERROR";
17
+ };
18
+ var LockError = class extends DomainError {
19
+ code = "LOCK_ERROR";
20
+ lockPath;
21
+ constructor(message, lockPath, cause) {
22
+ super(message, cause);
23
+ this.lockPath = lockPath;
24
+ }
25
+ };
26
+ var ParseError = class extends DomainError {
27
+ code = "PARSE_ERROR";
28
+ };
29
+ var ValidationError = class extends DomainError {
30
+ code = "VALIDATION_ERROR";
31
+ path;
32
+ constructor(message, path, cause) {
33
+ super(message, cause);
34
+ this.path = path;
35
+ }
36
+ };
37
+ function detectSpawnRateLimit(stderr) {
38
+ const patterns = [/rate.?limit/i, /\b429\b/, /too many requests/i, /overloaded/i, /\b529\b/];
39
+ const isRateLimited = patterns.some((p) => p.test(stderr));
40
+ if (!isRateLimited) return { rateLimited: false, retryAfterMs: null };
41
+ const retryMatch = /retry.?after:?\s*(\d+)/i.exec(stderr);
42
+ const retryAfterMs = retryMatch?.[1] ? parseInt(retryMatch[1], 10) * 1e3 : null;
43
+ return { rateLimited: true, retryAfterMs };
44
+ }
45
+ var SpawnError = class extends DomainError {
46
+ code = "SPAWN_ERROR";
47
+ stderr;
48
+ exitCode;
49
+ rateLimited;
50
+ retryAfterMs;
51
+ sessionId;
52
+ constructor(message, stderr, exitCode, sessionId, cause) {
53
+ super(message, cause);
54
+ this.stderr = stderr;
55
+ this.exitCode = exitCode;
56
+ this.sessionId = sessionId ?? null;
57
+ const rl = detectSpawnRateLimit(stderr);
58
+ this.rateLimited = rl.rateLimited;
59
+ this.retryAfterMs = rl.retryAfterMs;
60
+ }
61
+ };
62
+ var SprintNotFoundError = class extends DomainError {
63
+ code = "SPRINT_NOT_FOUND";
64
+ sprintId;
65
+ constructor(sprintId) {
66
+ super(`Sprint not found: ${sprintId}`);
67
+ this.sprintId = sprintId;
68
+ }
69
+ };
70
+ var TaskNotFoundError = class extends DomainError {
71
+ code = "TASK_NOT_FOUND";
72
+ taskId;
73
+ constructor(taskId) {
74
+ super(`Task not found: ${taskId}`);
75
+ this.taskId = taskId;
76
+ }
77
+ };
78
+ var TicketNotFoundError = class extends DomainError {
79
+ code = "TICKET_NOT_FOUND";
80
+ ticketId;
81
+ constructor(ticketId) {
82
+ super(`Ticket not found: ${ticketId}`);
83
+ this.ticketId = ticketId;
84
+ }
85
+ };
86
+ var ProjectNotFoundError = class extends DomainError {
87
+ code = "PROJECT_NOT_FOUND";
88
+ projectName;
89
+ constructor(projectName) {
90
+ super(`Project not found: ${projectName}`);
91
+ this.projectName = projectName;
92
+ }
93
+ };
94
+ var ProjectExistsError = class extends DomainError {
95
+ code = "PROJECT_EXISTS";
96
+ projectName;
97
+ constructor(projectName) {
98
+ super(`Project already exists: ${projectName}`);
99
+ this.projectName = projectName;
100
+ }
101
+ };
102
+ var StatusError = class extends DomainError {
103
+ code = "STATUS_ERROR";
104
+ };
105
+ var SprintStatusError = class extends StatusError {
106
+ currentStatus;
107
+ operation;
108
+ constructor(message, currentStatus, operation) {
109
+ super(message);
110
+ this.currentStatus = currentStatus;
111
+ this.operation = operation;
112
+ }
113
+ };
114
+ var NoCurrentSprintError = class extends StatusError {
115
+ constructor() {
116
+ super("No sprint specified and no current sprint set.");
117
+ }
118
+ };
119
+ var DependencyCycleError = class extends DomainError {
120
+ code = "DEPENDENCY_CYCLE";
121
+ cycle;
122
+ constructor(cycle) {
123
+ super(`Dependency cycle detected: ${cycle.join(" \u2192 ")}`);
124
+ this.cycle = cycle;
125
+ }
126
+ };
127
+ var IssueFetchError = class extends DomainError {
128
+ code = "ISSUE_FETCH_ERROR";
129
+ };
130
+
131
+ export {
132
+ DomainError,
133
+ IOError,
134
+ StorageError,
135
+ LockError,
136
+ ParseError,
137
+ ValidationError,
138
+ SpawnError,
139
+ SprintNotFoundError,
140
+ TaskNotFoundError,
141
+ TicketNotFoundError,
142
+ ProjectNotFoundError,
143
+ ProjectExistsError,
144
+ SprintStatusError,
145
+ NoCurrentSprintError,
146
+ DependencyCycleError,
147
+ IssueFetchError
148
+ };
@@ -4,25 +4,33 @@ import {
4
4
  exitWithCode
5
5
  } from "./chunk-7TG3EAQ2.mjs";
6
6
  import {
7
- ProjectNotFoundError,
8
- getProject,
9
7
  listProjects,
10
8
  projectExists
11
- } from "./chunk-WGHJI3OI.mjs";
9
+ } from "./chunk-PDI6HBZ7.mjs";
12
10
  import {
13
- SprintStatusError,
14
11
  assertSprintStatus,
15
12
  generateUuid8,
16
13
  getEditor,
17
14
  resolveSprintId,
18
15
  setEditor
19
- } from "./chunk-EKMZZRWI.mjs";
16
+ } from "./chunk-LFDW6MWF.mjs";
17
+ import {
18
+ ensureError,
19
+ unwrapOrThrow,
20
+ wrapAsync
21
+ } from "./chunk-OEUJDSHY.mjs";
20
22
  import {
21
23
  SprintSchema,
22
24
  getSprintFilePath,
23
25
  readValidatedJson,
24
26
  writeValidatedJson
25
- } from "./chunk-6PYTKGB5.mjs";
27
+ } from "./chunk-W3TY22IS.mjs";
28
+ import {
29
+ IOError,
30
+ IssueFetchError,
31
+ SprintStatusError,
32
+ TicketNotFoundError
33
+ } from "./chunk-EDJX7TT6.mjs";
26
34
  import {
27
35
  createSpinner,
28
36
  emoji,
@@ -41,9 +49,11 @@ import {
41
49
 
42
50
  // src/commands/ticket/add.ts
43
51
  import { confirm, input, select as select2 } from "@inquirer/prompts";
52
+ import { Result as Result3 } from "typescript-result";
44
53
 
45
54
  // src/utils/editor-input.ts
46
55
  import { editor } from "@inquirer/prompts";
56
+ import { Result } from "typescript-result";
47
57
 
48
58
  // src/utils/editor.ts
49
59
  import { select } from "@inquirer/prompts";
@@ -77,7 +87,8 @@ async function resolveEditor() {
77
87
  async function editorInput(options) {
78
88
  if (!process.stdin.isTTY) {
79
89
  const { multilineInput } = await import("./multiline-OHSNFCRG.mjs");
80
- return multilineInput({ message: options.message, default: options.default });
90
+ const value = await multilineInput({ message: options.message, default: options.default });
91
+ return Result.ok(value);
81
92
  }
82
93
  const editorCmd = await resolveEditor();
83
94
  const prevVisual = process.env["VISUAL"];
@@ -88,7 +99,14 @@ async function editorInput(options) {
88
99
  default: options.default,
89
100
  postfix: ".md"
90
101
  });
91
- return result.trim();
102
+ return Result.ok(result.trim());
103
+ } catch (err) {
104
+ return Result.error(
105
+ new IOError(
106
+ `Editor failed: ${err instanceof Error ? err.message : String(err)}`,
107
+ err instanceof Error ? err : void 0
108
+ )
109
+ );
92
110
  } finally {
93
111
  if (prevVisual === void 0) delete process.env["VISUAL"];
94
112
  else process.env["VISUAL"] = prevVisual;
@@ -96,33 +114,21 @@ async function editorInput(options) {
96
114
  }
97
115
 
98
116
  // src/store/ticket.ts
99
- var TicketNotFoundError = class extends Error {
100
- ticketId;
101
- constructor(ticketId) {
102
- super(`Ticket not found: ${ticketId}`);
103
- this.name = "TicketNotFoundError";
104
- this.ticketId = ticketId;
105
- }
106
- };
107
117
  async function getSprintData(sprintId) {
108
118
  const id = await resolveSprintId(sprintId);
109
- return readValidatedJson(getSprintFilePath(id), SprintSchema);
119
+ const result = await readValidatedJson(getSprintFilePath(id), SprintSchema);
120
+ if (!result.ok) throw result.error;
121
+ return result.value;
110
122
  }
111
123
  async function saveSprintData(sprint) {
112
- await writeValidatedJson(getSprintFilePath(sprint.id), sprint, SprintSchema);
124
+ const result = await writeValidatedJson(getSprintFilePath(sprint.id), sprint, SprintSchema);
125
+ if (!result.ok) throw result.error;
113
126
  }
114
127
  async function addTicket(input2, sprintId) {
115
128
  const sprint = await getSprintData(sprintId);
116
129
  assertSprintStatus(sprint, ["draft"], "add tickets");
117
- try {
118
- await getProject(input2.projectName);
119
- } catch (err) {
120
- if (err instanceof ProjectNotFoundError) {
121
- throw new Error(`Project '${input2.projectName}' does not exist. Add it first with 'ralphctl project add'.`, {
122
- cause: err
123
- });
124
- }
125
- throw err;
130
+ if (!await projectExists(input2.projectName)) {
131
+ throw new Error(`Project '${input2.projectName}' does not exist. Add it first with 'ralphctl project add'.`);
126
132
  }
127
133
  const ticket = {
128
134
  id: generateUuid8(),
@@ -205,6 +211,7 @@ function formatTicketId(ticket) {
205
211
 
206
212
  // src/utils/issue-fetch.ts
207
213
  import { spawnSync } from "child_process";
214
+ import { Result as Result2 } from "typescript-result";
208
215
  var MAX_COMMENTS = 20;
209
216
  function parseIssueUrl(url) {
210
217
  let parsed;
@@ -241,7 +248,7 @@ function parseIssueUrl(url) {
241
248
  }
242
249
  return null;
243
250
  }
244
- function fetchGitHubIssue(parsed) {
251
+ function fetchGitHubIssueResult(parsed) {
245
252
  const result = spawnSync(
246
253
  "gh",
247
254
  [
@@ -257,7 +264,7 @@ function fetchGitHubIssue(parsed) {
257
264
  );
258
265
  if (result.status !== 0) {
259
266
  const stderr = result.stderr.trim();
260
- throw new IssueFetchError(`gh issue view failed: ${stderr || "unknown error"}`);
267
+ return Result2.error(new IssueFetchError(`gh issue view failed: ${stderr || "unknown error"}`));
261
268
  }
262
269
  const data = JSON.parse(result.stdout);
263
270
  const comments = (data.comments ?? []).slice(-MAX_COMMENTS).map((c) => ({
@@ -265,14 +272,14 @@ function fetchGitHubIssue(parsed) {
265
272
  createdAt: c.createdAt ?? "",
266
273
  body: c.body ?? ""
267
274
  }));
268
- return {
275
+ return Result2.ok({
269
276
  title: data.title ?? "",
270
277
  body: data.body ?? "",
271
278
  comments,
272
279
  url: `https://${parsed.hostname}/${parsed.owner}/${parsed.repo}/issues/${String(parsed.number)}`
273
- };
280
+ });
274
281
  }
275
- function fetchGitLabIssue(parsed) {
282
+ function fetchGitLabIssueResult(parsed) {
276
283
  const result = spawnSync(
277
284
  "glab",
278
285
  ["issue", "view", String(parsed.number), "--repo", `${parsed.owner}/${parsed.repo}`, "--output", "json"],
@@ -280,7 +287,7 @@ function fetchGitLabIssue(parsed) {
280
287
  );
281
288
  if (result.status !== 0) {
282
289
  const stderr = result.stderr.trim();
283
- throw new IssueFetchError(`glab issue view failed: ${stderr || "unknown error"}`);
290
+ return Result2.error(new IssueFetchError(`glab issue view failed: ${stderr || "unknown error"}`));
284
291
  }
285
292
  const data = JSON.parse(result.stdout);
286
293
  const notesResult = spawnSync(
@@ -300,18 +307,21 @@ function fetchGitLabIssue(parsed) {
300
307
  } catch {
301
308
  }
302
309
  }
303
- return {
310
+ return Result2.ok({
304
311
  title: data.title ?? "",
305
312
  body: data.description ?? "",
306
313
  comments,
307
314
  url: `https://${parsed.hostname}/${parsed.owner}/${parsed.repo}/-/issues/${String(parsed.number)}`
308
- };
315
+ });
309
316
  }
310
- function fetchIssue(parsed) {
317
+ function fetchIssueResult(parsed) {
311
318
  if (parsed.host === "github") {
312
- return fetchGitHubIssue(parsed);
319
+ return fetchGitHubIssueResult(parsed);
313
320
  }
314
- return fetchGitLabIssue(parsed);
321
+ return fetchGitLabIssueResult(parsed);
322
+ }
323
+ function fetchIssue(parsed) {
324
+ return unwrapOrThrow(fetchIssueResult(parsed));
315
325
  }
316
326
  function fetchIssueFromUrl(url) {
317
327
  const parsed = parseIssueUrl(url);
@@ -346,30 +356,19 @@ function formatIssueContext(data) {
346
356
  }
347
357
  return lines.join("\n");
348
358
  }
349
- var IssueFetchError = class extends Error {
350
- constructor(message) {
351
- super(message);
352
- this.name = "IssueFetchError";
353
- }
354
- };
355
359
 
356
360
  // src/commands/ticket/add.ts
357
361
  function tryFetchIssue(url) {
358
362
  const spinner = createSpinner("Fetching issue data...");
359
363
  spinner.start();
360
- let data;
361
- try {
362
- data = fetchIssueFromUrl(url);
363
- } catch (err) {
364
+ const fetchR = Result3.try(() => fetchIssueFromUrl(url));
365
+ if (!fetchR.ok) {
364
366
  spinner.fail("Could not fetch issue data");
365
- if (err instanceof IssueFetchError) {
366
- showWarning(err.message);
367
- } else if (err instanceof Error) {
368
- showWarning(err.message);
369
- }
367
+ showWarning(fetchR.error.message);
370
368
  log.newline();
371
369
  return void 0;
372
370
  }
371
+ const data = fetchR.value;
373
372
  if (!data) {
374
373
  spinner.stop();
375
374
  return void 0;
@@ -422,17 +421,12 @@ async function addSingleTicketNonInteractive(options) {
422
421
  const trimmedLink = options.link?.trim();
423
422
  const link = trimmedLink === "" ? void 0 : trimmedLink;
424
423
  const projectName = trimmedProject;
425
- try {
426
- const ticket = await addTicket({
427
- title,
428
- description,
429
- link,
430
- projectName
431
- });
432
- showTicketResult(ticket);
433
- } catch (err) {
434
- handleTicketError(err);
424
+ const addR = await wrapAsync(() => addTicket({ title, description, link, projectName }), ensureError);
425
+ if (!addR.ok) {
426
+ handleTicketError(addR.error);
427
+ return;
435
428
  }
429
+ showTicketResult(addR.value);
436
430
  }
437
431
  async function addSingleTicketInteractive(options) {
438
432
  const projects = await listProjects();
@@ -467,26 +461,28 @@ async function addSingleTicketInteractive(options) {
467
461
  default: prefill?.title ?? options.title?.trim(),
468
462
  validate: (v) => v.trim().length > 0 ? true : "Title is required"
469
463
  });
470
- const description = await editorInput({
464
+ const descR = await editorInput({
471
465
  message: "Description (recommended):",
472
466
  default: prefill?.body ?? options.description?.trim()
473
467
  });
468
+ if (!descR.ok) {
469
+ showError(`Editor input failed: ${descR.error.message}`);
470
+ return null;
471
+ }
472
+ const description = descR.value;
474
473
  title = title.trim();
475
474
  const trimmedDescription = description.trim();
476
475
  const normalizedDescription = trimmedDescription === "" ? void 0 : trimmedDescription;
477
- try {
478
- const ticket = await addTicket({
479
- title,
480
- description: normalizedDescription,
481
- link: normalizedLink,
482
- projectName
483
- });
484
- showTicketResult(ticket);
485
- return ticket;
486
- } catch (err) {
487
- handleTicketError(err);
476
+ const addR = await wrapAsync(
477
+ () => addTicket({ title, description: normalizedDescription, link: normalizedLink, projectName }),
478
+ ensureError
479
+ );
480
+ if (!addR.ok) {
481
+ handleTicketError(addR.error);
488
482
  return null;
489
483
  }
484
+ showTicketResult(addR.value);
485
+ return addR.value;
490
486
  }
491
487
  function showTicketResult(ticket) {
492
488
  showSuccess("Ticket added!", [
@@ -536,7 +532,6 @@ async function ticketAddCommand(options = {}) {
536
532
  }
537
533
 
538
534
  export {
539
- TicketNotFoundError,
540
535
  addTicket,
541
536
  updateTicket,
542
537
  removeTicket,
@@ -550,7 +545,6 @@ export {
550
545
  editorInput,
551
546
  fetchIssueFromUrl,
552
547
  formatIssueContext,
553
- IssueFetchError,
554
548
  addSingleTicketInteractive,
555
549
  ticketAddCommand
556
550
  };