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.
- package/dist/{add-HGJCLWED.mjs → add-7LBVENXM.mjs} +6 -4
- package/dist/{add-MRGCS3US.mjs → add-DVEYDCTR.mjs} +6 -4
- package/dist/{chunk-NTWO2LXB.mjs → chunk-7LZ6GOGN.mjs} +13 -12
- package/dist/{chunk-JON4GCLR.mjs → chunk-DZ6HHTM5.mjs} +1 -1
- package/dist/chunk-EDJX7TT6.mjs +148 -0
- package/dist/{chunk-MNMQC36F.mjs → chunk-F2MMCTB5.mjs} +71 -77
- package/dist/{chunk-EKMZZRWI.mjs → chunk-LFDW6MWF.mjs} +65 -70
- package/dist/{chunk-LOR7QBXX.mjs → chunk-M7JV6MKD.mjs} +270 -349
- package/dist/chunk-OEUJDSHY.mjs +27 -0
- package/dist/{chunk-WGHJI3OI.mjs → chunk-PDI6HBZ7.mjs} +32 -37
- package/dist/{chunk-6PYTKGB5.mjs → chunk-W3TY22IS.mjs} +45 -39
- package/dist/{chunk-MRKOFVTM.mjs → chunk-YIB7QYU4.mjs} +102 -100
- package/dist/cli.mjs +761 -739
- package/dist/create-MQ4OHZAX.mjs +12 -0
- package/dist/{handle-UG5M2OON.mjs → handle-K2AZLTKU.mjs} +1 -1
- package/dist/{project-NT3L4FTB.mjs → project-Q4LKML42.mjs} +6 -4
- package/dist/{resolver-WSFWKACM.mjs → resolver-NH34HTB6.mjs} +27 -17
- package/dist/{sprint-4VHDLGFN.mjs → sprint-UHYXSEBJ.mjs} +8 -5
- package/dist/{wizard-LRELAN2J.mjs → wizard-MCDDXLGE.mjs} +45 -48
- package/package.json +2 -1
- package/dist/create-MG7E7PLQ.mjs +0 -10
|
@@ -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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
175
|
-
throw new
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
return Result2.error(new StorageError(`File not found: ${filePath}`, err instanceof Error ? err : void 0));
|
|
144
137
|
}
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
${
|
|
160
|
+
return Result2.error(
|
|
161
|
+
new ValidationError(`Validation failed before writing to ${filePath}:
|
|
162
|
+
${issues}`, filePath, result.error)
|
|
163
|
+
);
|
|
167
164
|
}
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
+
return Result2.error(new StorageError(`File not found: ${filePath}`, err instanceof Error ? err : void 0));
|
|
181
189
|
}
|
|
182
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
|
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 {
|