@nseng-ai/vibechk 0.1.1
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/package.json +27 -0
- package/src/cli.ts +434 -0
- package/src/exec-util.ts +37 -0
- package/src/ids.ts +12 -0
- package/src/index.ts +11 -0
- package/src/models.ts +161 -0
- package/src/reports.ts +342 -0
- package/src/repository.ts +174 -0
- package/src/runners.ts +126 -0
- package/src/store.ts +249 -0
- package/src/workflow.ts +256 -0
package/src/store.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ensurePrivateDirectory,
|
|
6
|
+
resolvePathOverride,
|
|
7
|
+
resolveXdgHome,
|
|
8
|
+
} from "@nseng-ai/capability-kit/xdg";
|
|
9
|
+
|
|
10
|
+
import type { LoadedBundle, RunBundle } from "./models.ts";
|
|
11
|
+
import { encodeRunBundle, parseRunBundle } from "./models.ts";
|
|
12
|
+
|
|
13
|
+
export const RUNS_DIR_NAME = "runs";
|
|
14
|
+
export const BUNDLE_FILE_NAME = "bundle.json";
|
|
15
|
+
export const PLAN_FILE_NAME = "plan.md";
|
|
16
|
+
export const TRANSCRIPT_FILE_NAME = "transcript.txt";
|
|
17
|
+
export const DIFF_FILE_NAME = "diff.patch";
|
|
18
|
+
export const ARTIFACTS_DIR_NAME = "artifacts";
|
|
19
|
+
|
|
20
|
+
export type VibechkErrorCode =
|
|
21
|
+
| "ambiguous_run"
|
|
22
|
+
| "bundle_missing"
|
|
23
|
+
| "invalid_bundle"
|
|
24
|
+
| "operation_failed"
|
|
25
|
+
| "run_id_allocation_failed"
|
|
26
|
+
| "run_not_found"
|
|
27
|
+
| "store_config_error";
|
|
28
|
+
|
|
29
|
+
export class VibechkError extends Error {
|
|
30
|
+
readonly code: VibechkErrorCode;
|
|
31
|
+
readonly data: Record<string, unknown>;
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
message: string,
|
|
35
|
+
code: VibechkErrorCode = "operation_failed",
|
|
36
|
+
data: Record<string, unknown> = {},
|
|
37
|
+
) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "VibechkError";
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.data = data;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resolveStoreRoot(
|
|
46
|
+
explicit: string | undefined,
|
|
47
|
+
env: Record<string, string | undefined>,
|
|
48
|
+
): string {
|
|
49
|
+
if (explicit !== undefined) {
|
|
50
|
+
return expandExplicitStoreRoot(explicit, env);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const vibechkHome = resolvePathOverride({ env, name: "VIBECHK_HOME" });
|
|
54
|
+
if (!vibechkHome.ok) throw new VibechkError(vibechkHome.error.message, "store_config_error");
|
|
55
|
+
if (vibechkHome.value !== undefined) return vibechkHome.value;
|
|
56
|
+
|
|
57
|
+
const xdgStateHome = resolveXdgHome("state", env);
|
|
58
|
+
if (!xdgStateHome.ok) throw new VibechkError(xdgStateHome.error.message, "store_config_error");
|
|
59
|
+
return join(xdgStateHome.value, "vibechk");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function expandExplicitStoreRoot(path: string, env: Record<string, string | undefined>): string {
|
|
63
|
+
if (!path.startsWith("~/")) return path;
|
|
64
|
+
const home = env["HOME"];
|
|
65
|
+
if (home === undefined) {
|
|
66
|
+
throw new VibechkError("HOME environment variable is not set", "store_config_error");
|
|
67
|
+
}
|
|
68
|
+
return join(home, path.slice(2));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function resolveRunId(storeRoot: string, idOrPrefix: string): Promise<string> {
|
|
72
|
+
const runsDir = join(storeRoot, RUNS_DIR_NAME);
|
|
73
|
+
|
|
74
|
+
let runDirs: string[];
|
|
75
|
+
try {
|
|
76
|
+
const entries = await readdir(runsDir, { withFileTypes: true });
|
|
77
|
+
runDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name.toString());
|
|
78
|
+
} catch (error: unknown) {
|
|
79
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
80
|
+
throw new VibechkError(`No run matches prefix '${idOrPrefix}'.`, "run_not_found", {
|
|
81
|
+
idOrPrefix,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check for exact match first
|
|
88
|
+
if (runDirs.includes(idOrPrefix)) {
|
|
89
|
+
return idOrPrefix;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Find prefix matches
|
|
93
|
+
const matches = runDirs.filter((name) => name.startsWith(idOrPrefix)).sort();
|
|
94
|
+
|
|
95
|
+
if (matches.length === 0) {
|
|
96
|
+
throw new VibechkError(`No run matches prefix '${idOrPrefix}'.`, "run_not_found", {
|
|
97
|
+
idOrPrefix,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (matches.length > 1) {
|
|
102
|
+
throw new VibechkError(
|
|
103
|
+
`Run prefix '${idOrPrefix}' is ambiguous: ${matches.join(", ")}.`,
|
|
104
|
+
"ambiguous_run",
|
|
105
|
+
{
|
|
106
|
+
idOrPrefix,
|
|
107
|
+
matches,
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return matches[0]!;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function readBundle(storeRoot: string, idOrPrefix: string): Promise<LoadedBundle> {
|
|
116
|
+
const runId = await resolveRunId(storeRoot, idOrPrefix);
|
|
117
|
+
const runDir = join(storeRoot, RUNS_DIR_NAME, runId);
|
|
118
|
+
return await loadBundleFromRunDir(runDir, `Run '${runId}'`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function listBundles(storeRoot: string): Promise<LoadedBundle[]> {
|
|
122
|
+
const runsDir = join(storeRoot, RUNS_DIR_NAME);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const entries = await readdir(runsDir, { withFileTypes: true });
|
|
126
|
+
|
|
127
|
+
if (entries.length === 0) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const runDirs = entries.filter((entry) => entry.isDirectory());
|
|
132
|
+
|
|
133
|
+
const loaded: LoadedBundle[] = [];
|
|
134
|
+
for (const dir of runDirs) {
|
|
135
|
+
const runDir = join(runsDir, dir.name.toString());
|
|
136
|
+
loaded.push(await loadBundleFromRunDir(runDir, `Run directory ${runDir}`));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Sort newest first, with run_id as tie-breaker
|
|
140
|
+
loaded.sort((a, b) => a.bundle.runId.localeCompare(b.bundle.runId));
|
|
141
|
+
loaded.sort((a, b) => b.bundle.startedAt.getTime() - a.bundle.startedAt.getTime());
|
|
142
|
+
|
|
143
|
+
return loaded;
|
|
144
|
+
} catch (error: unknown) {
|
|
145
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function loadBundleFromRunDir(runDir: string, context: string): Promise<LoadedBundle> {
|
|
153
|
+
const bundlePath = join(runDir, BUNDLE_FILE_NAME);
|
|
154
|
+
|
|
155
|
+
let bundleText: string;
|
|
156
|
+
try {
|
|
157
|
+
bundleText = await readFile(bundlePath, "utf-8");
|
|
158
|
+
} catch (error: unknown) {
|
|
159
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
160
|
+
throw new VibechkError(`${context} is missing bundle.json.`, "bundle_missing", {
|
|
161
|
+
context,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let data: unknown;
|
|
168
|
+
try {
|
|
169
|
+
data = JSON.parse(bundleText);
|
|
170
|
+
} catch (error: unknown) {
|
|
171
|
+
throw new VibechkError(`${context} has an invalid bundle.json: ${error}`, "invalid_bundle", {
|
|
172
|
+
context,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let bundle: RunBundle;
|
|
177
|
+
try {
|
|
178
|
+
bundle = parseRunBundle(data);
|
|
179
|
+
} catch (error: unknown) {
|
|
180
|
+
throw new VibechkError(`${context} has an invalid bundle.json: ${error}`, "invalid_bundle", {
|
|
181
|
+
context,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const planText = await readOptionalFile(join(runDir, PLAN_FILE_NAME));
|
|
186
|
+
const transcript = await readOptionalFile(join(runDir, TRANSCRIPT_FILE_NAME));
|
|
187
|
+
const diffPatch = await readOptionalFile(join(runDir, DIFF_FILE_NAME));
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
bundle,
|
|
191
|
+
runDir,
|
|
192
|
+
planText,
|
|
193
|
+
transcript,
|
|
194
|
+
diffPatch,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function readOptionalFile(path: string): Promise<string> {
|
|
199
|
+
try {
|
|
200
|
+
return await readFile(path, "utf-8");
|
|
201
|
+
} catch (error: unknown) {
|
|
202
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
203
|
+
return "";
|
|
204
|
+
}
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
210
|
+
return error instanceof Error && "code" in error;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function createRunDir(
|
|
214
|
+
storeRoot: string,
|
|
215
|
+
idGenerator: () => string,
|
|
216
|
+
): Promise<{ runId: string; runDir: string }> {
|
|
217
|
+
const runsDir = join(storeRoot, RUNS_DIR_NAME);
|
|
218
|
+
await ensurePrivateDirectory(runsDir);
|
|
219
|
+
|
|
220
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
221
|
+
const runId = idGenerator().toLowerCase();
|
|
222
|
+
const runDir = join(runsDir, runId);
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
await mkdir(runDir, { recursive: false, mode: 0o700 });
|
|
226
|
+
const artifactsDir = join(runDir, ARTIFACTS_DIR_NAME);
|
|
227
|
+
await ensurePrivateDirectory(artifactsDir);
|
|
228
|
+
return { runId, runDir };
|
|
229
|
+
} catch (error: unknown) {
|
|
230
|
+
if (isNodeError(error) && error.code === "EEXIST") {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
throw new VibechkError(
|
|
238
|
+
"Failed to allocate a unique run ID after 100 attempts.",
|
|
239
|
+
"run_id_allocation_failed",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function writeBundle(runDir: string, bundle: RunBundle): Promise<void> {
|
|
244
|
+
const bundlePath = join(runDir, BUNDLE_FILE_NAME);
|
|
245
|
+
const tempPath = join(runDir, `${BUNDLE_FILE_NAME}.tmp`);
|
|
246
|
+
|
|
247
|
+
await writeFile(tempPath, JSON.stringify(encodeRunBundle(bundle), null, 2) + "\n", "utf-8");
|
|
248
|
+
await rename(tempPath, bundlePath);
|
|
249
|
+
}
|
package/src/workflow.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { RunBundle } from "./models.ts";
|
|
5
|
+
import type { VibechkWorkdirGateway } from "./repository.ts";
|
|
6
|
+
import type { Runner, RunnerResult } from "./runners.ts";
|
|
7
|
+
import {
|
|
8
|
+
ARTIFACTS_DIR_NAME,
|
|
9
|
+
createRunDir,
|
|
10
|
+
DIFF_FILE_NAME,
|
|
11
|
+
PLAN_FILE_NAME,
|
|
12
|
+
resolveStoreRoot,
|
|
13
|
+
TRANSCRIPT_FILE_NAME,
|
|
14
|
+
VibechkError,
|
|
15
|
+
writeBundle,
|
|
16
|
+
} from "./store.ts";
|
|
17
|
+
|
|
18
|
+
const SCHEMA_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
export interface RunExecutionResult {
|
|
21
|
+
runId: string;
|
|
22
|
+
exitCode: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RunDeps {
|
|
26
|
+
runner: Runner;
|
|
27
|
+
repository: VibechkWorkdirGateway;
|
|
28
|
+
clock: () => Date;
|
|
29
|
+
idGenerator: () => string;
|
|
30
|
+
stdout: (text: string) => void;
|
|
31
|
+
stderr: (text: string) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RunExecutionOptions {
|
|
35
|
+
planPath: string;
|
|
36
|
+
workdir: string;
|
|
37
|
+
runnerName: string;
|
|
38
|
+
model: string | null;
|
|
39
|
+
store: string | undefined;
|
|
40
|
+
env: Record<string, string | undefined>;
|
|
41
|
+
deps: RunDeps;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function executeRun(options: RunExecutionOptions): Promise<RunExecutionResult> {
|
|
45
|
+
const { planPath, workdir, runnerName, model, store, env, deps } = options;
|
|
46
|
+
const resolvedPlanPath = await resolvePlanPath(planPath);
|
|
47
|
+
const resolvedWorkdir = await resolveWorkdir(workdir);
|
|
48
|
+
|
|
49
|
+
const repository = deps.repository;
|
|
50
|
+
|
|
51
|
+
const gitProvenance = await repository.readProvenance();
|
|
52
|
+
|
|
53
|
+
if (!(await repository.isClean())) {
|
|
54
|
+
throw new VibechkError(
|
|
55
|
+
`Workdir ${resolvedWorkdir} has uncommitted changes; vibechk run requires a clean workdir.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const planText = await readPlanText(resolvedPlanPath);
|
|
60
|
+
const storeRoot = resolveStoreRoot(store, env);
|
|
61
|
+
const { runId, runDir } = await createRunDir(storeRoot, deps.idGenerator);
|
|
62
|
+
const artifactsDir = join(runDir, ARTIFACTS_DIR_NAME);
|
|
63
|
+
|
|
64
|
+
await writeFile(join(runDir, PLAN_FILE_NAME), planText, "utf-8");
|
|
65
|
+
|
|
66
|
+
const startedAt = deps.clock();
|
|
67
|
+
|
|
68
|
+
let runnerResult: RunnerResult = {
|
|
69
|
+
exitCode: 1,
|
|
70
|
+
transcript: "",
|
|
71
|
+
metrics: {
|
|
72
|
+
wallTimeSeconds: null,
|
|
73
|
+
inputTokens: null,
|
|
74
|
+
outputTokens: null,
|
|
75
|
+
totalTokens: null,
|
|
76
|
+
costUsd: null,
|
|
77
|
+
},
|
|
78
|
+
artifacts: {},
|
|
79
|
+
runnerVersion: null,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let runnerError: string | null = null;
|
|
83
|
+
const transcriptPath = join(runDir, TRANSCRIPT_FILE_NAME);
|
|
84
|
+
const transcriptChunks: string[] = [];
|
|
85
|
+
|
|
86
|
+
const transcriptSink = (text: string): void => {
|
|
87
|
+
transcriptChunks.push(text);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
runnerResult = await deps.runner.run(
|
|
92
|
+
{
|
|
93
|
+
planText,
|
|
94
|
+
workdir: resolvedWorkdir,
|
|
95
|
+
model,
|
|
96
|
+
runId,
|
|
97
|
+
artifactsDir,
|
|
98
|
+
},
|
|
99
|
+
transcriptSink,
|
|
100
|
+
deps.stdout,
|
|
101
|
+
);
|
|
102
|
+
} catch (error: unknown) {
|
|
103
|
+
if (error instanceof VibechkError) {
|
|
104
|
+
runnerError = error.message;
|
|
105
|
+
const errorText = `Runner error: ${runnerError}\n`;
|
|
106
|
+
transcriptChunks.push(errorText);
|
|
107
|
+
} else {
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const transcriptContent = transcriptChunks.join("");
|
|
113
|
+
await writeFile(
|
|
114
|
+
transcriptPath,
|
|
115
|
+
transcriptContent === "" ? runnerResult.transcript : transcriptContent,
|
|
116
|
+
"utf-8",
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
let diffPatch = "";
|
|
120
|
+
let resultBranch: string | null = null;
|
|
121
|
+
let isBranchCreated = false;
|
|
122
|
+
let postRunError: string | null = null;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
diffPatch = await repository.diffPatch();
|
|
126
|
+
await writeDiffArtifact(runDir, diffPatch);
|
|
127
|
+
|
|
128
|
+
if (await repository.hasChanges()) {
|
|
129
|
+
resultBranch = `vibechk/${runId}`;
|
|
130
|
+
await repository.createResultBranchAndCommit(resultBranch, `vibechk: capture run ${runId}`);
|
|
131
|
+
isBranchCreated = true;
|
|
132
|
+
await repository.restoreBranch(gitProvenance.startingBranch);
|
|
133
|
+
|
|
134
|
+
if (!(await repository.isClean())) {
|
|
135
|
+
postRunError = `Workdir ${resolvedWorkdir} was not clean after restoring branch ${gitProvenance.startingBranch}.`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch (error: unknown) {
|
|
139
|
+
if (error instanceof VibechkError) {
|
|
140
|
+
postRunError = error.message;
|
|
141
|
+
} else {
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await writeBestEffortDiffArtifact(runDir, diffPatch, deps.stderr);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const finishedAt = deps.clock();
|
|
149
|
+
const errorMessage = runnerError ?? postRunError;
|
|
150
|
+
const status = runnerResult.exitCode === 0 && errorMessage === null ? "success" : "failed";
|
|
151
|
+
|
|
152
|
+
const bundle: RunBundle = {
|
|
153
|
+
schemaVersion: SCHEMA_VERSION,
|
|
154
|
+
runId,
|
|
155
|
+
status,
|
|
156
|
+
startedAt,
|
|
157
|
+
finishedAt,
|
|
158
|
+
runner: runnerName,
|
|
159
|
+
runnerVersion: runnerResult.runnerVersion,
|
|
160
|
+
model,
|
|
161
|
+
planSource: resolvedPlanPath,
|
|
162
|
+
workdir: resolvedWorkdir,
|
|
163
|
+
git: gitProvenance,
|
|
164
|
+
metrics: runnerResult.metrics,
|
|
165
|
+
resultBranch,
|
|
166
|
+
branchCreated: isBranchCreated,
|
|
167
|
+
runnerExitCode: runnerResult.exitCode,
|
|
168
|
+
error: errorMessage,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
await writeBundle(runDir, bundle);
|
|
172
|
+
|
|
173
|
+
if (postRunError !== null) {
|
|
174
|
+
throw new VibechkError(postRunError);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let exitCode = runnerResult.exitCode;
|
|
178
|
+
if (runnerError !== null && exitCode === 0) {
|
|
179
|
+
exitCode = 1;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
runId,
|
|
184
|
+
exitCode,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function writeDiffArtifact(runDir: string, diffPatch: string): Promise<void> {
|
|
189
|
+
await writeFile(join(runDir, DIFF_FILE_NAME), diffPatch, "utf-8");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function writeBestEffortDiffArtifact(
|
|
193
|
+
runDir: string,
|
|
194
|
+
diffPatch: string,
|
|
195
|
+
stderr: (text: string) => void,
|
|
196
|
+
): Promise<void> {
|
|
197
|
+
try {
|
|
198
|
+
await writeDiffArtifact(runDir, diffPatch);
|
|
199
|
+
} catch (error: unknown) {
|
|
200
|
+
stderr(`Warning: could not write best-effort diff artifact: ${formatUnknownError(error)}\n`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function formatUnknownError(error: unknown): string {
|
|
205
|
+
if (error instanceof Error) return error.message;
|
|
206
|
+
return String(error);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function resolvePlanPath(planPath: string): Promise<string> {
|
|
210
|
+
const resolved = resolve(planPath);
|
|
211
|
+
try {
|
|
212
|
+
const info = await stat(resolved);
|
|
213
|
+
if (!info.isFile()) {
|
|
214
|
+
throw new VibechkError(`Plan path ${planPath} is not a file.`);
|
|
215
|
+
}
|
|
216
|
+
} catch (error: unknown) {
|
|
217
|
+
if (error instanceof VibechkError) throw error;
|
|
218
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
219
|
+
throw new VibechkError(`Plan path ${planPath} does not exist.`);
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
return resolved;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function resolveWorkdir(workdir: string): Promise<string> {
|
|
227
|
+
const resolved = resolve(workdir);
|
|
228
|
+
try {
|
|
229
|
+
const info = await stat(resolved);
|
|
230
|
+
if (!info.isDirectory()) {
|
|
231
|
+
throw new VibechkError(`Workdir ${workdir} is not a directory.`);
|
|
232
|
+
}
|
|
233
|
+
} catch (error: unknown) {
|
|
234
|
+
if (error instanceof VibechkError) throw error;
|
|
235
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
236
|
+
throw new VibechkError(`Workdir ${workdir} does not exist.`);
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
return resolved;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function readPlanText(planPath: string): Promise<string> {
|
|
244
|
+
try {
|
|
245
|
+
return await readFile(planPath, "utf-8");
|
|
246
|
+
} catch (error: unknown) {
|
|
247
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
248
|
+
throw new VibechkError(`Plan file not found: ${planPath}`);
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
255
|
+
return error instanceof Error && "code" in error;
|
|
256
|
+
}
|