ralphctl 0.1.2 → 0.1.4

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.
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/result-helpers.ts
4
+ import { Result } from "typescript-result";
5
+ async function wrapAsync(fn, mapError) {
6
+ try {
7
+ const value = await fn();
8
+ return Result.ok(value);
9
+ } catch (err) {
10
+ return Result.error(mapError(err));
11
+ }
12
+ }
13
+ function ensureError(err) {
14
+ return err instanceof Error ? err : new Error(String(err));
15
+ }
16
+ function unwrapOrThrow(result) {
17
+ if (result.ok) {
18
+ return result.value;
19
+ }
20
+ throw result.error;
21
+ }
22
+
23
+ export {
24
+ wrapAsync,
25
+ ensureError,
26
+ unwrapOrThrow
27
+ };
@@ -7,26 +7,16 @@ import {
7
7
  readValidatedJson,
8
8
  validateProjectPath,
9
9
  writeValidatedJson
10
- } from "./chunk-6PYTKGB5.mjs";
10
+ } from "./chunk-W3TY22IS.mjs";
11
+ import {
12
+ ParseError,
13
+ ProjectExistsError,
14
+ ProjectNotFoundError,
15
+ ValidationError
16
+ } from "./chunk-EDJX7TT6.mjs";
11
17
 
12
18
  // src/store/project.ts
13
19
  import { basename, resolve } from "path";
14
- var ProjectNotFoundError = class extends Error {
15
- projectName;
16
- constructor(projectName) {
17
- super(`Project not found: ${projectName}`);
18
- this.name = "ProjectNotFoundError";
19
- this.projectName = projectName;
20
- }
21
- };
22
- var ProjectExistsError = class extends Error {
23
- projectName;
24
- constructor(projectName) {
25
- super(`Project already exists: ${projectName}`);
26
- this.name = "ProjectExistsError";
27
- this.projectName = projectName;
28
- }
29
- };
30
20
  function migrateProjectIfNeeded(project) {
31
21
  if (project.repositories) {
32
22
  return project;
@@ -42,7 +32,7 @@ function migrateProjectIfNeeded(project) {
42
32
  description: project.description
43
33
  };
44
34
  }
45
- throw new Error(`Invalid project data: no paths or repositories for ${project.name}`);
35
+ throw new ParseError(`Invalid project data: no paths or repositories for ${project.name}`);
46
36
  }
47
37
  async function listProjects() {
48
38
  const filePath = getProjectsFilePath();
@@ -56,10 +46,13 @@ async function listProjects() {
56
46
  if (needsMigration) {
57
47
  const migrated = rawData.map(migrateProjectIfNeeded);
58
48
  const validated = ProjectsSchema.parse(migrated);
59
- await writeValidatedJson(filePath, validated, ProjectsSchema);
49
+ const writeResult = await writeValidatedJson(filePath, validated, ProjectsSchema);
50
+ if (!writeResult.ok) throw writeResult.error;
60
51
  return validated;
61
52
  }
62
- const projects = await readValidatedJson(filePath, ProjectsSchema);
53
+ const result = await readValidatedJson(filePath, ProjectsSchema);
54
+ if (!result.ok) throw result.error;
55
+ const projects = result.value;
63
56
  const hasTildePaths = projects.some((p) => p.repositories.some((r) => r.path.startsWith("~")));
64
57
  if (hasTildePaths) {
65
58
  const corrected = projects.map((project) => ({
@@ -69,7 +62,8 @@ async function listProjects() {
69
62
  )
70
63
  }));
71
64
  const validated = ProjectsSchema.parse(corrected);
72
- await writeValidatedJson(filePath, validated, ProjectsSchema);
65
+ const writeResult = await writeValidatedJson(filePath, validated, ProjectsSchema);
66
+ if (!writeResult.ok) throw writeResult.error;
73
67
  return validated;
74
68
  }
75
69
  return projects;
@@ -95,13 +89,13 @@ async function createProject(project) {
95
89
  for (const repo of project.repositories) {
96
90
  const resolved = resolve(expandTilde(repo.path));
97
91
  const validation = await validateProjectPath(resolved);
98
- if (validation !== true) {
99
- pathErrors.push(` ${repo.path}: ${validation}`);
92
+ if (!validation.ok) {
93
+ pathErrors.push(` ${repo.path}: ${validation.error.message}`);
100
94
  }
101
95
  }
102
96
  if (pathErrors.length > 0) {
103
- throw new Error(`Invalid project paths:
104
- ${pathErrors.join("\n")}`);
97
+ throw new ValidationError(`Invalid project paths:
98
+ ${pathErrors.join("\n")}`, "repositories");
105
99
  }
106
100
  const normalizedProject = {
107
101
  ...project,
@@ -112,7 +106,8 @@ ${pathErrors.join("\n")}`);
112
106
  }))
113
107
  };
114
108
  projects.push(normalizedProject);
115
- await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
109
+ const writeResult = await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
110
+ if (!writeResult.ok) throw writeResult.error;
116
111
  return normalizedProject;
117
112
  }
118
113
  async function updateProject(name, updates) {
@@ -126,13 +121,13 @@ async function updateProject(name, updates) {
126
121
  for (const repo of updates.repositories) {
127
122
  const resolved = resolve(expandTilde(repo.path));
128
123
  const validation = await validateProjectPath(resolved);
129
- if (validation !== true) {
130
- pathErrors.push(` ${repo.path}: ${validation}`);
124
+ if (!validation.ok) {
125
+ pathErrors.push(` ${repo.path}: ${validation.error.message}`);
131
126
  }
132
127
  }
133
128
  if (pathErrors.length > 0) {
134
- throw new Error(`Invalid project paths:
135
- ${pathErrors.join("\n")}`);
129
+ throw new ValidationError(`Invalid project paths:
130
+ ${pathErrors.join("\n")}`, "repositories");
136
131
  }
137
132
  updates.repositories = updates.repositories.map((repo) => ({
138
133
  ...repo,
@@ -151,7 +146,8 @@ ${pathErrors.join("\n")}`);
151
146
  description: updates.description ?? existingProject.description
152
147
  };
153
148
  projects[index] = updatedProject;
154
- await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
149
+ const writeResult = await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
150
+ if (!writeResult.ok) throw writeResult.error;
155
151
  return updatedProject;
156
152
  }
157
153
  async function removeProject(name) {
@@ -161,7 +157,8 @@ async function removeProject(name) {
161
157
  throw new ProjectNotFoundError(name);
162
158
  }
163
159
  projects.splice(index, 1);
164
- await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
160
+ const writeResult = await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
161
+ if (!writeResult.ok) throw writeResult.error;
165
162
  }
166
163
  async function getProjectRepos(name) {
167
164
  const project = await getProject(name);
@@ -171,8 +168,8 @@ async function addProjectRepo(name, repo) {
171
168
  const project = await getProject(name);
172
169
  const resolvedPath = resolve(expandTilde(repo.path));
173
170
  const validation = await validateProjectPath(resolvedPath);
174
- if (validation !== true) {
175
- throw new Error(`Invalid path ${repo.path}: ${validation}`);
171
+ if (!validation.ok) {
172
+ throw new ValidationError(`Invalid path ${repo.path}: ${validation.error.message}`, repo.path);
176
173
  }
177
174
  if (project.repositories.some((r) => r.path === resolvedPath)) {
178
175
  return project;
@@ -191,7 +188,7 @@ async function removeProjectRepo(name, path) {
191
188
  const resolvedPath = resolve(expandTilde(path));
192
189
  const newRepos = project.repositories.filter((r) => r.path !== resolvedPath);
193
190
  if (newRepos.length === 0) {
194
- throw new Error("Cannot remove the last repository from a project");
191
+ throw new ValidationError("Cannot remove the last repository from a project", "repositories");
195
192
  }
196
193
  if (newRepos.length === project.repositories.length) {
197
194
  return project;
@@ -200,8 +197,6 @@ async function removeProjectRepo(name, path) {
200
197
  }
201
198
 
202
199
  export {
203
- ProjectNotFoundError,
204
- ProjectExistsError,
205
200
  listProjects,
206
201
  getProject,
207
202
  projectExists,
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ IOError,
4
+ StorageError,
5
+ ValidationError
6
+ } from "./chunk-EDJX7TT6.mjs";
2
7
 
3
8
  // src/utils/paths.ts
4
9
  import { fileURLToPath } from "url";
@@ -6,6 +11,7 @@ import { dirname, isAbsolute, join, resolve, sep } from "path";
6
11
  import { existsSync } from "fs";
7
12
  import { homedir } from "os";
8
13
  import { lstat, realpath, stat } from "fs/promises";
14
+ import { Result } from "typescript-result";
9
15
  var __filename = fileURLToPath(import.meta.url);
10
16
  var __dirname = dirname(__filename);
11
17
  function getRepoRoot() {
@@ -80,38 +86,25 @@ async function validateProjectPath(path) {
80
86
  const realPath = await realpath(resolved);
81
87
  const realStats = await stat(realPath);
82
88
  if (!realStats.isDirectory()) {
83
- return "Symlink target is not a directory";
89
+ return Result.error(new IOError("Symlink target is not a directory"));
84
90
  }
85
- return true;
91
+ return Result.ok(true);
86
92
  }
87
93
  if (!lstats.isDirectory()) {
88
- return "Path is not a directory";
94
+ return Result.error(new IOError("Path is not a directory"));
89
95
  }
90
- return true;
91
- } catch {
92
- return "Directory does not exist";
96
+ return Result.ok(true);
97
+ } catch (err) {
98
+ const code = err.code;
99
+ const message = code === "EACCES" ? "Permission denied" : "Directory does not exist";
100
+ return Result.error(new IOError(message, err instanceof Error ? err : void 0));
93
101
  }
94
102
  }
95
103
 
96
104
  // src/utils/storage.ts
97
105
  import { access, appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
98
106
  import { dirname as dirname2 } from "path";
99
- var ValidationError = class extends Error {
100
- path;
101
- constructor(message, path, cause) {
102
- super(message, { cause });
103
- this.name = "ValidationError";
104
- this.path = path;
105
- }
106
- };
107
- var FileNotFoundError = class extends Error {
108
- path;
109
- constructor(message, path) {
110
- super(message);
111
- this.name = "FileNotFoundError";
112
- this.path = path;
113
- }
114
- };
107
+ import { Result as Result2 } from "typescript-result";
115
108
  async function ensureDir(dirPath) {
116
109
  await mkdir(dirPath, { recursive: true });
117
110
  }
@@ -140,46 +133,61 @@ async function readValidatedJson(filePath, schema) {
140
133
  content = await readFile(filePath, "utf-8");
141
134
  } catch (err) {
142
135
  if (err instanceof Error && "code" in err && err.code === "ENOENT") {
143
- throw new FileNotFoundError(`File not found: ${filePath}`, filePath);
136
+ return Result2.error(new StorageError(`File not found: ${filePath}`, err instanceof Error ? err : void 0));
144
137
  }
145
- throw err;
138
+ return Result2.error(new StorageError(`Failed to read ${filePath}`, err instanceof Error ? err : void 0));
146
139
  }
147
140
  let data;
148
141
  try {
149
142
  data = JSON.parse(content);
150
143
  } catch (err) {
151
- throw new ValidationError(`Invalid JSON in ${filePath}`, filePath, err);
144
+ return Result2.error(
145
+ new ValidationError(`Invalid JSON in ${filePath}`, filePath, err instanceof Error ? err : void 0)
146
+ );
152
147
  }
153
148
  const result = schema.safeParse(data);
154
149
  if (!result.success) {
155
150
  const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
156
- throw new ValidationError(`Validation failed for ${filePath}:
157
- ${issues}`, filePath, result.error);
151
+ return Result2.error(new ValidationError(`Validation failed for ${filePath}:
152
+ ${issues}`, filePath, result.error));
158
153
  }
159
- return result.data;
154
+ return Result2.ok(result.data);
160
155
  }
161
156
  async function writeValidatedJson(filePath, data, schema) {
162
157
  const result = schema.safeParse(data);
163
158
  if (!result.success) {
164
159
  const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
165
- throw new ValidationError(`Validation failed before writing to ${filePath}:
166
- ${issues}`, filePath, result.error);
160
+ return Result2.error(
161
+ new ValidationError(`Validation failed before writing to ${filePath}:
162
+ ${issues}`, filePath, result.error)
163
+ );
167
164
  }
168
- await ensureDir(dirname2(filePath));
169
- await writeFile(filePath, JSON.stringify(result.data, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
165
+ try {
166
+ await ensureDir(dirname2(filePath));
167
+ await writeFile(filePath, JSON.stringify(result.data, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
168
+ } catch (err) {
169
+ return Result2.error(new StorageError(`Failed to write ${filePath}`, err instanceof Error ? err : void 0));
170
+ }
171
+ return Result2.ok(void 0);
170
172
  }
171
173
  async function appendToFile(filePath, content) {
172
- await ensureDir(dirname2(filePath));
173
- await appendFile(filePath, content, { encoding: "utf-8", mode: 384 });
174
+ try {
175
+ await ensureDir(dirname2(filePath));
176
+ await appendFile(filePath, content, { encoding: "utf-8", mode: 384 });
177
+ return Result2.ok(void 0);
178
+ } catch (err) {
179
+ return Result2.error(new StorageError(`Failed to append to ${filePath}`, err instanceof Error ? err : void 0));
180
+ }
174
181
  }
175
182
  async function readTextFile(filePath) {
176
183
  try {
177
- return await readFile(filePath, "utf-8");
184
+ const content = await readFile(filePath, "utf-8");
185
+ return Result2.ok(content);
178
186
  } catch (err) {
179
187
  if (err instanceof Error && "code" in err && err.code === "ENOENT") {
180
- throw new FileNotFoundError(`File not found: ${filePath}`, filePath);
188
+ return Result2.error(new StorageError(`File not found: ${filePath}`, err instanceof Error ? err : void 0));
181
189
  }
182
- throw err;
190
+ return Result2.error(new StorageError(`Failed to read ${filePath}`, err instanceof Error ? err : void 0));
183
191
  }
184
192
  }
185
193
 
@@ -292,8 +300,6 @@ export {
292
300
  assertSafeCwd,
293
301
  expandTilde,
294
302
  validateProjectPath,
295
- ValidationError,
296
- FileNotFoundError,
297
303
  ensureDir,
298
304
  removeDir,
299
305
  fileExists,
@@ -1,19 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  escapableSelect
4
- } from "./chunk-NTWO2LXB.mjs";
4
+ } from "./chunk-7LZ6GOGN.mjs";
5
5
  import {
6
6
  EXIT_ERROR,
7
7
  exitWithCode
8
8
  } from "./chunk-7TG3EAQ2.mjs";
9
9
  import {
10
- ProjectExistsError,
11
10
  createProject
12
- } from "./chunk-WGHJI3OI.mjs";
11
+ } from "./chunk-PDI6HBZ7.mjs";
12
+ import {
13
+ ensureError,
14
+ wrapAsync
15
+ } from "./chunk-OEUJDSHY.mjs";
13
16
  import {
14
17
  expandTilde,
15
18
  validateProjectPath
16
- } from "./chunk-6PYTKGB5.mjs";
19
+ } from "./chunk-W3TY22IS.mjs";
20
+ import {
21
+ IOError,
22
+ ProjectExistsError
23
+ } from "./chunk-EDJX7TT6.mjs";
17
24
  import {
18
25
  createSpinner,
19
26
  emoji,
@@ -32,34 +39,27 @@ import {
32
39
  import { existsSync as existsSync2, statSync as statSync2 } from "fs";
33
40
  import { basename, join as join3, resolve as resolve2 } from "path";
34
41
  import { input, select } from "@inquirer/prompts";
42
+ import { Result as Result3 } from "typescript-result";
35
43
 
36
44
  // src/interactive/file-browser.ts
37
45
  import { readdirSync, statSync } from "fs";
38
46
  import { homedir } from "os";
39
47
  import { dirname, join, resolve } from "path";
48
+ import { Result } from "typescript-result";
40
49
  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
- }
50
+ const r = Result.try(() => readdirSync(dirPath, { withFileTypes: true }));
51
+ if (!r.ok) return [];
52
+ return r.value.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
47
53
  }
48
54
  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
+ const r = Result.try(() => readdirSync(dirPath, { withFileTypes: true }));
56
+ if (!r.ok) return false;
57
+ return r.value.some((e) => e.isDirectory() && !e.name.startsWith("."));
55
58
  }
56
59
  function isGitRepo(dirPath) {
57
- try {
58
- const gitDir = join(dirPath, ".git");
59
- return statSync(gitDir).isDirectory();
60
- } catch {
61
- return false;
62
- }
60
+ const r = Result.try(() => statSync(join(dirPath, ".git")));
61
+ if (!r.ok) return false;
62
+ return r.value.isDirectory();
63
63
  }
64
64
  async function browseDirectory(message = "Browse to directory:", startPath) {
65
65
  let currentPath = startPath ? resolve(startPath) : homedir();
@@ -106,36 +106,37 @@ async function browseDirectory(message = "Browse to directory:", startPath) {
106
106
  name: muted("Cancel"),
107
107
  value: "__CANCEL__"
108
108
  });
109
- try {
110
- const selected = await escapableSelect({
109
+ const selectResult = await wrapAsync(
110
+ () => escapableSelect({
111
111
  message: `${emoji.donut} ${message}
112
112
  ${muted(currentPath)}`,
113
113
  choices,
114
114
  pageSize: 15,
115
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") {
116
+ }),
117
+ ensureError
118
+ );
119
+ if (!selectResult.ok) {
120
+ if (selectResult.error.name === "ExitPromptError") return null;
121
+ throw selectResult.error;
122
+ }
123
+ const selected = selectResult.value;
124
+ if (selected === null) {
125
+ return null;
126
+ }
127
+ switch (selected) {
128
+ case "__SELECT__":
129
+ return currentPath;
130
+ case "__PARENT__":
131
+ currentPath = parentDir;
132
+ break;
133
+ case "__HOME__":
134
+ currentPath = homedir();
135
+ break;
136
+ case "__CANCEL__":
136
137
  return null;
137
- }
138
- throw err;
138
+ default:
139
+ currentPath = selected;
139
140
  }
140
141
  }
141
142
  }
@@ -143,6 +144,7 @@ async function browseDirectory(message = "Browse to directory:", startPath) {
143
144
  // src/utils/detect-scripts.ts
144
145
  import { existsSync, readFileSync } from "fs";
145
146
  import { join as join2 } from "path";
147
+ import { Result as Result2 } from "typescript-result";
146
148
  function detectNodePackageManager(projectPath) {
147
149
  if (existsSync(join2(projectPath, "pnpm-lock.yaml"))) return "pnpm";
148
150
  if (existsSync(join2(projectPath, "yarn.lock"))) return "yarn";
@@ -156,15 +158,20 @@ var NODE_PRIMARY_GROUPS = [
156
158
  var NODE_FALLBACK_GROUPS = [
157
159
  { label: "build", aliases: ["build", "compile"] }
158
160
  ];
159
- function readPackageJsonScripts(projectPath) {
161
+ function safeReadPackageJsonScripts(projectPath) {
160
162
  try {
161
163
  const raw = readFileSync(join2(projectPath, "package.json"), "utf-8");
162
164
  const pkg = JSON.parse(raw);
163
- return pkg.scripts ?? {};
165
+ return Result2.ok(pkg.scripts ?? {});
164
166
  } catch {
165
- return {};
167
+ return Result2.error(new IOError("Failed to read package.json"));
166
168
  }
167
169
  }
170
+ function readPackageJsonScripts(projectPath) {
171
+ const result = safeReadPackageJsonScripts(projectPath);
172
+ if (!result.ok) return {};
173
+ return result.value;
174
+ }
168
175
  var nodeDetector = {
169
176
  type: "node",
170
177
  label: "Node.js",
@@ -285,25 +292,19 @@ function validateSlug(slug) {
285
292
  return /^[a-z0-9-]+$/.test(slug);
286
293
  }
287
294
  function isGitRepo2(path) {
288
- try {
289
- const gitDir = join3(path, ".git");
290
- return existsSync2(gitDir) && statSync2(gitDir).isDirectory();
291
- } catch {
292
- return false;
293
- }
295
+ const gitDir = join3(path, ".git");
296
+ const r = Result3.try(() => existsSync2(gitDir) && statSync2(gitDir).isDirectory());
297
+ return r.ok ? r.value : false;
294
298
  }
295
299
  function hasAiInstructions(repoPath) {
296
300
  return existsSync2(join3(repoPath, "CLAUDE.md")) || existsSync2(join3(repoPath, ".github", "copilot-instructions.md"));
297
301
  }
298
302
  async function addCheckScriptToRepository(repo) {
299
303
  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 {
304
+ const detectR = Result3.try(() => detectCheckScriptCandidates(repo.path));
305
+ if (detectR.ok && detectR.value) {
306
+ log.success(` Detected: ${detectR.value.typeLabel}`);
307
+ suggested = suggestCheckScript(repo.path);
307
308
  }
308
309
  const checkInput = await input({
309
310
  message: " Check script (optional):",
@@ -340,8 +341,8 @@ async function projectAddCommand(options = {}) {
340
341
  for (const path of options.paths) {
341
342
  const resolved = resolve2(expandTilde(path.trim()));
342
343
  const validation = await validateProjectPath(resolved);
343
- if (validation !== true) {
344
- errors.push(`--path ${path}: ${validation}`);
344
+ if (!validation.ok) {
345
+ errors.push(`--path ${path}: ${validation.error.message}`);
345
346
  }
346
347
  }
347
348
  spinner?.succeed("Paths validated");
@@ -387,7 +388,7 @@ async function projectAddCommand(options = {}) {
387
388
  for (const p of options.paths) {
388
389
  const resolved = resolve2(expandTilde(p.trim()));
389
390
  const validation = await validateProjectPath(resolved);
390
- if (validation === true) {
391
+ if (validation.ok) {
391
392
  repositories.push({ name: basename(resolved), path: resolved });
392
393
  }
393
394
  }
@@ -417,15 +418,15 @@ async function projectAddCommand(options = {}) {
417
418
  default: process.cwd(),
418
419
  validate: async (v) => {
419
420
  const result = await validateProjectPath(v.trim());
420
- return result;
421
+ return result.ok ? true : result.error.message;
421
422
  }
422
423
  });
423
424
  firstPath = firstPath.trim();
424
425
  }
425
426
  const resolved = resolve2(expandTilde(firstPath));
426
427
  const validation = await validateProjectPath(resolved);
427
- if (validation !== true) {
428
- showError(`Invalid path: ${validation}`);
428
+ if (!validation.ok) {
429
+ showError(`Invalid path: ${validation.error.message}`);
429
430
  exitWithCode(EXIT_ERROR);
430
431
  }
431
432
  repositories.push({ name: basename(resolved), path: resolved });
@@ -459,13 +460,13 @@ Configuring: ${firstRepo.name}`);
459
460
  if (browsed) {
460
461
  const resolved = resolve2(expandTilde(browsed));
461
462
  const validation = await validateProjectPath(resolved);
462
- if (validation === true) {
463
+ if (validation.ok) {
463
464
  const newRepo = { name: basename(resolved), path: resolved };
464
465
  log.success(`Added: ${newRepo.name}`);
465
466
  const repoWithScripts = await addCheckScriptToRepository(newRepo);
466
467
  repositories.push(repoWithScripts);
467
468
  } else {
468
- log.error(`Invalid path: ${validation}`);
469
+ log.error(`Invalid path: ${validation.error.message}`);
469
470
  }
470
471
  }
471
472
  } else {
@@ -477,13 +478,13 @@ Configuring: ${firstRepo.name}`);
477
478
  } else {
478
479
  const resolved = resolve2(expandTilde(additionalPath.trim()));
479
480
  const validation = await validateProjectPath(resolved);
480
- if (validation === true) {
481
+ if (validation.ok) {
481
482
  const newRepo = { name: basename(resolved), path: resolved };
482
483
  log.success(`Added: ${newRepo.name}`);
483
484
  const repoWithScripts = await addCheckScriptToRepository(newRepo);
484
485
  repositories.push(repoWithScripts);
485
486
  } else {
486
- log.error(`Invalid path: ${validation}`);
487
+ log.error(`Invalid path: ${validation.error.message}`);
487
488
  }
488
489
  }
489
490
  }
@@ -495,40 +496,41 @@ Configuring: ${firstRepo.name}`);
495
496
  const trimmedDescInteractive = description.trim();
496
497
  description = trimmedDescInteractive === "" ? void 0 : trimmedDescInteractive;
497
498
  }
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) {
499
+ const project = {
500
+ name,
501
+ displayName,
502
+ repositories,
503
+ description
504
+ };
505
+ const createR = await wrapAsync(() => createProject(project), ensureError);
506
+ if (!createR.ok) {
507
+ if (createR.error instanceof ProjectExistsError) {
525
508
  showError(`Project "${name}" already exists.`);
526
509
  showNextStep(`ralphctl project remove ${name}`, "remove existing project first");
527
510
  log.newline();
528
511
  } else {
529
- throw err;
512
+ throw createR.error;
513
+ }
514
+ return;
515
+ }
516
+ const created = createR.value;
517
+ showSuccess("Project added!", [
518
+ ["Name", created.name],
519
+ ["Display Name", created.displayName]
520
+ ]);
521
+ if (created.description) {
522
+ console.log(field("Description", created.description));
523
+ }
524
+ console.log(field("Repositories", ""));
525
+ for (const repo of created.repositories) {
526
+ log.item(`${repo.name} \u2192 ${repo.path}`);
527
+ if (repo.checkScript) {
528
+ console.log(` Check: ${repo.checkScript}`);
529
+ } else {
530
+ console.log(` Check: ${muted("(not configured)")}`);
530
531
  }
531
532
  }
533
+ console.log("");
532
534
  }
533
535
 
534
536
  export {