ralphctl 0.1.1 → 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.
@@ -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
+ };
@@ -1,31 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ProjectsSchema,
4
+ expandTilde,
4
5
  fileExists,
5
6
  getProjectsFilePath,
6
7
  readValidatedJson,
7
8
  validateProjectPath,
8
9
  writeValidatedJson
9
- } from "./chunk-4C24UQ3X.mjs";
10
+ } from "./chunk-W3TY22IS.mjs";
11
+ import {
12
+ ParseError,
13
+ ProjectExistsError,
14
+ ProjectNotFoundError,
15
+ ValidationError
16
+ } from "./chunk-EDJX7TT6.mjs";
10
17
 
11
18
  // src/store/project.ts
12
19
  import { basename, resolve } from "path";
13
- var ProjectNotFoundError = class extends Error {
14
- projectName;
15
- constructor(projectName) {
16
- super(`Project not found: ${projectName}`);
17
- this.name = "ProjectNotFoundError";
18
- this.projectName = projectName;
19
- }
20
- };
21
- var ProjectExistsError = class extends Error {
22
- projectName;
23
- constructor(projectName) {
24
- super(`Project already exists: ${projectName}`);
25
- this.name = "ProjectExistsError";
26
- this.projectName = projectName;
27
- }
28
- };
29
20
  function migrateProjectIfNeeded(project) {
30
21
  if (project.repositories) {
31
22
  return project;
@@ -36,12 +27,12 @@ function migrateProjectIfNeeded(project) {
36
27
  displayName: project.displayName,
37
28
  repositories: project.paths.map((p) => ({
38
29
  name: basename(p),
39
- path: p
30
+ path: resolve(expandTilde(p))
40
31
  })),
41
32
  description: project.description
42
33
  };
43
34
  }
44
- 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}`);
45
36
  }
46
37
  async function listProjects() {
47
38
  const filePath = getProjectsFilePath();
@@ -55,10 +46,27 @@ async function listProjects() {
55
46
  if (needsMigration) {
56
47
  const migrated = rawData.map(migrateProjectIfNeeded);
57
48
  const validated = ProjectsSchema.parse(migrated);
58
- await writeValidatedJson(filePath, validated, ProjectsSchema);
49
+ const writeResult = await writeValidatedJson(filePath, validated, ProjectsSchema);
50
+ if (!writeResult.ok) throw writeResult.error;
51
+ return validated;
52
+ }
53
+ const result = await readValidatedJson(filePath, ProjectsSchema);
54
+ if (!result.ok) throw result.error;
55
+ const projects = result.value;
56
+ const hasTildePaths = projects.some((p) => p.repositories.some((r) => r.path.startsWith("~")));
57
+ if (hasTildePaths) {
58
+ const corrected = projects.map((project) => ({
59
+ ...project,
60
+ repositories: project.repositories.map(
61
+ (repo) => repo.path.startsWith("~") ? { ...repo, path: resolve(expandTilde(repo.path)) } : repo
62
+ )
63
+ }));
64
+ const validated = ProjectsSchema.parse(corrected);
65
+ const writeResult = await writeValidatedJson(filePath, validated, ProjectsSchema);
66
+ if (!writeResult.ok) throw writeResult.error;
59
67
  return validated;
60
68
  }
61
- return readValidatedJson(filePath, ProjectsSchema);
69
+ return projects;
62
70
  }
63
71
  async function getProject(name) {
64
72
  const projects = await listProjects();
@@ -79,26 +87,27 @@ async function createProject(project) {
79
87
  }
80
88
  const pathErrors = [];
81
89
  for (const repo of project.repositories) {
82
- const resolved = resolve(repo.path);
90
+ const resolved = resolve(expandTilde(repo.path));
83
91
  const validation = await validateProjectPath(resolved);
84
- if (validation !== true) {
85
- pathErrors.push(` ${repo.path}: ${validation}`);
92
+ if (!validation.ok) {
93
+ pathErrors.push(` ${repo.path}: ${validation.error.message}`);
86
94
  }
87
95
  }
88
96
  if (pathErrors.length > 0) {
89
- throw new Error(`Invalid project paths:
90
- ${pathErrors.join("\n")}`);
97
+ throw new ValidationError(`Invalid project paths:
98
+ ${pathErrors.join("\n")}`, "repositories");
91
99
  }
92
100
  const normalizedProject = {
93
101
  ...project,
94
102
  repositories: project.repositories.map((repo) => ({
95
103
  ...repo,
96
104
  name: repo.name || basename(repo.path),
97
- path: resolve(repo.path)
105
+ path: resolve(expandTilde(repo.path))
98
106
  }))
99
107
  };
100
108
  projects.push(normalizedProject);
101
- await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
109
+ const writeResult = await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
110
+ if (!writeResult.ok) throw writeResult.error;
102
111
  return normalizedProject;
103
112
  }
104
113
  async function updateProject(name, updates) {
@@ -110,20 +119,20 @@ async function updateProject(name, updates) {
110
119
  if (updates.repositories) {
111
120
  const pathErrors = [];
112
121
  for (const repo of updates.repositories) {
113
- const resolved = resolve(repo.path);
122
+ const resolved = resolve(expandTilde(repo.path));
114
123
  const validation = await validateProjectPath(resolved);
115
- if (validation !== true) {
116
- pathErrors.push(` ${repo.path}: ${validation}`);
124
+ if (!validation.ok) {
125
+ pathErrors.push(` ${repo.path}: ${validation.error.message}`);
117
126
  }
118
127
  }
119
128
  if (pathErrors.length > 0) {
120
- throw new Error(`Invalid project paths:
121
- ${pathErrors.join("\n")}`);
129
+ throw new ValidationError(`Invalid project paths:
130
+ ${pathErrors.join("\n")}`, "repositories");
122
131
  }
123
132
  updates.repositories = updates.repositories.map((repo) => ({
124
133
  ...repo,
125
134
  name: repo.name || basename(repo.path),
126
- path: resolve(repo.path)
135
+ path: resolve(expandTilde(repo.path))
127
136
  }));
128
137
  }
129
138
  const existingProject = projects[index];
@@ -137,7 +146,8 @@ ${pathErrors.join("\n")}`);
137
146
  description: updates.description ?? existingProject.description
138
147
  };
139
148
  projects[index] = updatedProject;
140
- await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
149
+ const writeResult = await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
150
+ if (!writeResult.ok) throw writeResult.error;
141
151
  return updatedProject;
142
152
  }
143
153
  async function removeProject(name) {
@@ -147,7 +157,8 @@ async function removeProject(name) {
147
157
  throw new ProjectNotFoundError(name);
148
158
  }
149
159
  projects.splice(index, 1);
150
- await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
160
+ const writeResult = await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
161
+ if (!writeResult.ok) throw writeResult.error;
151
162
  }
152
163
  async function getProjectRepos(name) {
153
164
  const project = await getProject(name);
@@ -155,10 +166,10 @@ async function getProjectRepos(name) {
155
166
  }
156
167
  async function addProjectRepo(name, repo) {
157
168
  const project = await getProject(name);
158
- const resolvedPath = resolve(repo.path);
169
+ const resolvedPath = resolve(expandTilde(repo.path));
159
170
  const validation = await validateProjectPath(resolvedPath);
160
- if (validation !== true) {
161
- 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);
162
173
  }
163
174
  if (project.repositories.some((r) => r.path === resolvedPath)) {
164
175
  return project;
@@ -174,10 +185,10 @@ async function addProjectRepo(name, repo) {
174
185
  }
175
186
  async function removeProjectRepo(name, path) {
176
187
  const project = await getProject(name);
177
- const resolvedPath = resolve(path);
188
+ const resolvedPath = resolve(expandTilde(path));
178
189
  const newRepos = project.repositories.filter((r) => r.path !== resolvedPath);
179
190
  if (newRepos.length === 0) {
180
- throw new Error("Cannot remove the last repository from a project");
191
+ throw new ValidationError("Cannot remove the last repository from a project", "repositories");
181
192
  }
182
193
  if (newRepos.length === project.repositories.length) {
183
194
  return project;
@@ -186,8 +197,6 @@ async function removeProjectRepo(name, path) {
186
197
  }
187
198
 
188
199
  export {
189
- ProjectNotFoundError,
190
- ProjectExistsError,
191
200
  listProjects,
192
201
  getProject,
193
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() {
@@ -67,46 +73,38 @@ function assertSafeCwd(path) {
67
73
  throw new Error(`Unsafe path for cwd: must be absolute, got: ${path}`);
68
74
  }
69
75
  }
76
+ function expandTilde(path) {
77
+ if (path === "~") return homedir();
78
+ if (path.startsWith("~/")) return homedir() + path.slice(1);
79
+ return path;
80
+ }
70
81
  async function validateProjectPath(path) {
71
82
  try {
72
- const resolved = resolve(path);
83
+ const resolved = resolve(expandTilde(path));
73
84
  const lstats = await lstat(resolved);
74
85
  if (lstats.isSymbolicLink()) {
75
86
  const realPath = await realpath(resolved);
76
87
  const realStats = await stat(realPath);
77
88
  if (!realStats.isDirectory()) {
78
- return "Symlink target is not a directory";
89
+ return Result.error(new IOError("Symlink target is not a directory"));
79
90
  }
80
- return true;
91
+ return Result.ok(true);
81
92
  }
82
93
  if (!lstats.isDirectory()) {
83
- return "Path is not a directory";
94
+ return Result.error(new IOError("Path is not a directory"));
84
95
  }
85
- return true;
86
- } catch {
87
- 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));
88
101
  }
89
102
  }
90
103
 
91
104
  // src/utils/storage.ts
92
105
  import { access, appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
93
106
  import { dirname as dirname2 } from "path";
94
- var ValidationError = class extends Error {
95
- path;
96
- constructor(message, path, cause) {
97
- super(message, { cause });
98
- this.name = "ValidationError";
99
- this.path = path;
100
- }
101
- };
102
- var FileNotFoundError = class extends Error {
103
- path;
104
- constructor(message, path) {
105
- super(message);
106
- this.name = "FileNotFoundError";
107
- this.path = path;
108
- }
109
- };
107
+ import { Result as Result2 } from "typescript-result";
110
108
  async function ensureDir(dirPath) {
111
109
  await mkdir(dirPath, { recursive: true });
112
110
  }
@@ -135,46 +133,61 @@ async function readValidatedJson(filePath, schema) {
135
133
  content = await readFile(filePath, "utf-8");
136
134
  } catch (err) {
137
135
  if (err instanceof Error && "code" in err && err.code === "ENOENT") {
138
- throw new FileNotFoundError(`File not found: ${filePath}`, filePath);
136
+ return Result2.error(new StorageError(`File not found: ${filePath}`, err instanceof Error ? err : void 0));
139
137
  }
140
- throw err;
138
+ return Result2.error(new StorageError(`Failed to read ${filePath}`, err instanceof Error ? err : void 0));
141
139
  }
142
140
  let data;
143
141
  try {
144
142
  data = JSON.parse(content);
145
143
  } catch (err) {
146
- 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
+ );
147
147
  }
148
148
  const result = schema.safeParse(data);
149
149
  if (!result.success) {
150
150
  const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
151
- throw new ValidationError(`Validation failed for ${filePath}:
152
- ${issues}`, filePath, result.error);
151
+ return Result2.error(new ValidationError(`Validation failed for ${filePath}:
152
+ ${issues}`, filePath, result.error));
153
153
  }
154
- return result.data;
154
+ return Result2.ok(result.data);
155
155
  }
156
156
  async function writeValidatedJson(filePath, data, schema) {
157
157
  const result = schema.safeParse(data);
158
158
  if (!result.success) {
159
159
  const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
160
- throw new ValidationError(`Validation failed before writing to ${filePath}:
161
- ${issues}`, filePath, result.error);
160
+ return Result2.error(
161
+ new ValidationError(`Validation failed before writing to ${filePath}:
162
+ ${issues}`, filePath, result.error)
163
+ );
164
+ }
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));
162
170
  }
163
- await ensureDir(dirname2(filePath));
164
- await writeFile(filePath, JSON.stringify(result.data, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
171
+ return Result2.ok(void 0);
165
172
  }
166
173
  async function appendToFile(filePath, content) {
167
- await ensureDir(dirname2(filePath));
168
- 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
+ }
169
181
  }
170
182
  async function readTextFile(filePath) {
171
183
  try {
172
- return await readFile(filePath, "utf-8");
184
+ const content = await readFile(filePath, "utf-8");
185
+ return Result2.ok(content);
173
186
  } catch (err) {
174
187
  if (err instanceof Error && "code" in err && err.code === "ENOENT") {
175
- throw new FileNotFoundError(`File not found: ${filePath}`, filePath);
188
+ return Result2.error(new StorageError(`File not found: ${filePath}`, err instanceof Error ? err : void 0));
176
189
  }
177
- throw err;
190
+ return Result2.error(new StorageError(`Failed to read ${filePath}`, err instanceof Error ? err : void 0));
178
191
  }
179
192
  }
180
193
 
@@ -285,9 +298,8 @@ export {
285
298
  getIdeateDir,
286
299
  getSchemaPath,
287
300
  assertSafeCwd,
301
+ expandTilde,
288
302
  validateProjectPath,
289
- ValidationError,
290
- FileNotFoundError,
291
303
  ensureDir,
292
304
  removeDir,
293
305
  fileExists,