pi-oracle 0.4.0 → 0.6.0
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/CHANGELOG.md +33 -0
- package/README.md +43 -12
- package/docs/ORACLE_DESIGN.md +32 -16
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +28 -1
- package/extensions/oracle/index.ts +1 -1
- package/extensions/oracle/lib/auth.ts +50 -0
- package/extensions/oracle/lib/commands.ts +57 -47
- package/extensions/oracle/lib/config.ts +53 -2
- package/extensions/oracle/lib/jobs.ts +31 -5
- package/extensions/oracle/lib/poller.ts +33 -4
- package/extensions/oracle/lib/runtime.ts +171 -7
- package/extensions/oracle/lib/tools.ts +726 -253
- package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +1 -0
- package/extensions/oracle/shared/job-lifecycle-helpers.mjs +13 -0
- package/extensions/oracle/shared/job-observability-helpers.d.mts +2 -1
- package/extensions/oracle/shared/job-observability-helpers.mjs +28 -10
- package/extensions/oracle/worker/auth-bootstrap.mjs +49 -4
- package/extensions/oracle/worker/auth-flow-helpers.mjs +1 -1
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +106 -41
- package/extensions/oracle/worker/run-job.mjs +17 -13
- package/package.json +6 -2
- package/prompts/oracle-followup.md +48 -0
- package/prompts/oracle.md +18 -11
|
@@ -34,7 +34,7 @@ import { cleanupRuntimeArtifacts, getProjectId, getSessionId, parseConversationI
|
|
|
34
34
|
export type OracleJobStatus = SharedOracleJobStatus;
|
|
35
35
|
export type OracleJobPhase = SharedOracleJobPhase;
|
|
36
36
|
|
|
37
|
-
export type OracleWakeupSettlementSource = "oracle_read" | "oracle_status";
|
|
37
|
+
export type OracleWakeupSettlementSource = "oracle_read" | "oracle_status" | "oracle_read_command";
|
|
38
38
|
|
|
39
39
|
export { ACTIVE_ORACLE_JOB_STATUSES, OPEN_ORACLE_JOB_STATUSES, TERMINAL_ORACLE_JOB_STATUSES };
|
|
40
40
|
export const ORACLE_MISSING_WORKER_GRACE_MS = 30_000;
|
|
@@ -285,6 +285,16 @@ function notificationClaimIsLive(job: Pick<OracleJob, "notifyClaimedAt" | "notif
|
|
|
285
285
|
return now - claimedAtMs < ORACLE_NOTIFICATION_CLAIM_TTL_MS;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
function getWakeupRetentionGraceDeadline(job: Pick<OracleJob, "wakeupLastRequestedAt">, now = Date.now()): { retryAt: string; remainingMs: number } | undefined {
|
|
289
|
+
const lastRequestedAtMs = parseTimestamp(job.wakeupLastRequestedAt);
|
|
290
|
+
if (lastRequestedAtMs === undefined) return undefined;
|
|
291
|
+
const retryAtMs = lastRequestedAtMs + ORACLE_WAKEUP_POST_SEND_RETENTION_MS;
|
|
292
|
+
return {
|
|
293
|
+
retryAt: new Date(retryAtMs).toISOString(),
|
|
294
|
+
remainingMs: Math.max(0, retryAtMs - now),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
288
298
|
function wakeupRetentionGraceIsActive(job: Pick<OracleJob, "wakeupLastRequestedAt">, now = Date.now()): boolean {
|
|
289
299
|
const lastRequestedAtMs = parseTimestamp(job.wakeupLastRequestedAt);
|
|
290
300
|
if (lastRequestedAtMs === undefined) return false;
|
|
@@ -464,12 +474,17 @@ export async function removeTerminalOracleJob(job: OracleJob): Promise<{ removed
|
|
|
464
474
|
},
|
|
465
475
|
};
|
|
466
476
|
}
|
|
467
|
-
|
|
477
|
+
const nowMs = Date.now();
|
|
478
|
+
if (wakeupRetentionGraceIsActive(current, nowMs)) {
|
|
479
|
+
const graceDeadline = getWakeupRetentionGraceDeadline(current, nowMs);
|
|
480
|
+
const retryHint = graceDeadline
|
|
481
|
+
? ` Retry after ${graceDeadline.retryAt} (${Math.ceil(graceDeadline.remainingMs / 1000)}s remaining).`
|
|
482
|
+
: "";
|
|
468
483
|
return {
|
|
469
484
|
removed: false,
|
|
470
485
|
cleanupReport: {
|
|
471
486
|
attempted: [],
|
|
472
|
-
warnings: [`Refusing to remove terminal oracle job ${current.id} because its wake-up delivery is still within the post-send retention grace window
|
|
487
|
+
warnings: [`Refusing to remove terminal oracle job ${current.id} because its wake-up delivery is still within the post-send retention grace window.${retryHint}`],
|
|
473
488
|
},
|
|
474
489
|
};
|
|
475
490
|
}
|
|
@@ -713,8 +728,10 @@ export async function releaseNotificationClaim(jobId: string, claimedBy: string)
|
|
|
713
728
|
export async function noteWakeupRequested(jobId: string, at = new Date().toISOString()): Promise<OracleJob | undefined> {
|
|
714
729
|
try {
|
|
715
730
|
return await updateJob(jobId, (job) => noteOracleJobWakeupRequested(job, { at, source: "oracle:poller" }));
|
|
716
|
-
} catch {
|
|
717
|
-
|
|
731
|
+
} catch (error) {
|
|
732
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
733
|
+
if (message.startsWith("Oracle job not found:")) return undefined;
|
|
734
|
+
throw error;
|
|
718
735
|
}
|
|
719
736
|
}
|
|
720
737
|
|
|
@@ -912,7 +929,16 @@ export function resolveArchiveInputs(cwd: string, files: string[]): { absolute:
|
|
|
912
929
|
|
|
913
930
|
const realCwd = realpathSync(cwd);
|
|
914
931
|
return files.map((file) => {
|
|
932
|
+
if (!file.trim()) {
|
|
933
|
+
throw new Error("Archive input must be a non-empty project-relative path");
|
|
934
|
+
}
|
|
935
|
+
if (file.trim() === "." && file !== ".") {
|
|
936
|
+
throw new Error("Archive input must use '.' exactly for a whole-repo archive");
|
|
937
|
+
}
|
|
915
938
|
const absolute = resolve(cwd, file);
|
|
939
|
+
if (absolute === cwd && file !== ".") {
|
|
940
|
+
throw new Error("Archive input must use '.' exactly for a whole-repo archive");
|
|
941
|
+
}
|
|
916
942
|
const relative = absolute.startsWith(`${cwd}/`) ? absolute.slice(cwd.length + 1) : absolute === cwd ? "." : "";
|
|
917
943
|
if (!relative) {
|
|
918
944
|
throw new Error(`Archive input must be inside the project cwd: ${file}`);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Scope: Poller/orchestration only; durable lifecycle mutations live in jobs.ts and shared observability formatting lives in extensions/oracle/shared.
|
|
4
4
|
// Usage: Imported by the oracle extension entrypoint to start or stop per-session oracle polling.
|
|
5
5
|
// Invariants/Assumptions: Poller scans are serialized per session key, wake-up delivery is best-effort, and terminal-job notifications always re-read durable job state before send.
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
6
7
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
7
8
|
import { buildOracleStatusText, buildOracleWakeupNotificationContent } from "../shared/job-observability-helpers.mjs";
|
|
8
9
|
import { isProcessAlive, readProcessStartedAt } from "../shared/process-helpers.mjs";
|
|
@@ -154,7 +155,8 @@ function requestWakeupTurn(pi: ExtensionAPI, job: OraclePollerJob): void {
|
|
|
154
155
|
customType: ORACLE_WAKEUP_REMINDER_CUSTOM_TYPE,
|
|
155
156
|
display: false,
|
|
156
157
|
content: buildOracleWakeupNotificationContent(job, {
|
|
157
|
-
responsePath: job.responsePath
|
|
158
|
+
responsePath: job.responsePath,
|
|
159
|
+
responseAvailable: Boolean(job.responsePath && existsSync(job.responsePath)),
|
|
158
160
|
artifactsPath: `${getJobDir(job.id)}/artifacts`,
|
|
159
161
|
}),
|
|
160
162
|
details: { jobId: job.id, status: job.status },
|
|
@@ -243,8 +245,14 @@ async function scan(pi: ExtensionAPI, ctx: ExtensionContext, workerPath: string,
|
|
|
243
245
|
continue;
|
|
244
246
|
}
|
|
245
247
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
+
const notedWakeup = await noteWakeupRequested(jobId);
|
|
249
|
+
const deliverableAfterNote = notedWakeup ?? readJob(jobId);
|
|
250
|
+
if (!deliverableAfterNote || shouldPruneTerminalJob(deliverableAfterNote, Date.now())) {
|
|
251
|
+
await releaseNotificationClaim(jobId, notificationClaimant).catch(() => undefined);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
requestWakeupTurn(pi, deliverableAfterNote);
|
|
248
256
|
if (ctx.hasUI) {
|
|
249
257
|
ctx.ui.notify(`Oracle job ${claimed.id} is ${claimed.status}.`, "info");
|
|
250
258
|
}
|
|
@@ -292,12 +300,33 @@ export function stopPollerForSession(sessionFile: string | undefined, cwd: strin
|
|
|
292
300
|
if (timer) {
|
|
293
301
|
clearInterval(timer);
|
|
294
302
|
activePollers.delete(sessionKey);
|
|
295
|
-
scansInFlight.delete(sessionKey);
|
|
296
303
|
}
|
|
297
304
|
const wakeupTargetLeaseKey = getWakeupTargetLeaseKey(sessionKey);
|
|
298
305
|
void releaseLease(WAKEUP_TARGET_LEASE_KIND, wakeupTargetLeaseKey).catch(() => undefined);
|
|
299
306
|
}
|
|
300
307
|
|
|
308
|
+
export async function stopAllPollers(): Promise<void> {
|
|
309
|
+
const sessionKeys = [...activePollers.keys()];
|
|
310
|
+
for (const timer of activePollers.values()) {
|
|
311
|
+
clearInterval(timer);
|
|
312
|
+
}
|
|
313
|
+
activePollers.clear();
|
|
314
|
+
await Promise.all(sessionKeys.map(async (sessionKey) => {
|
|
315
|
+
const wakeupTargetLeaseKey = getWakeupTargetLeaseKey(sessionKey);
|
|
316
|
+
await releaseLease(WAKEUP_TARGET_LEASE_KIND, wakeupTargetLeaseKey).catch(() => undefined);
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function waitForAllPollersToQuiesce(timeoutMs = 2_000): Promise<void> {
|
|
321
|
+
const startedAt = Date.now();
|
|
322
|
+
while (scansInFlight.size > 0) {
|
|
323
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
324
|
+
throw new Error(`Timed out waiting for oracle pollers to quiesce after ${timeoutMs}ms`);
|
|
325
|
+
}
|
|
326
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
301
330
|
export function stopPoller(ctx: ExtensionContext): void {
|
|
302
331
|
const sessionFile = getSessionFile(ctx);
|
|
303
332
|
if (!sessionFile) return;
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// Invariants/Assumptions: Lease metadata is the admission source of truth, tracked worker identity checks defend against PID reuse, and runtime cleanup always attempts lease release.
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
|
-
import { existsSync, realpathSync, readFileSync } from "node:fs";
|
|
9
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
10
|
-
import { dirname, join } from "node:path";
|
|
8
|
+
import { constants as fsConstants, existsSync, realpathSync, readFileSync } from "node:fs";
|
|
9
|
+
import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
10
|
+
import { delimiter, dirname, join } from "node:path";
|
|
11
11
|
import { jobBlocksAdmission } from "../shared/job-coordination-helpers.mjs";
|
|
12
12
|
import { isTrackedProcessAlive } from "../shared/process-helpers.mjs";
|
|
13
13
|
import type { OracleConfig } from "./config.js";
|
|
@@ -21,6 +21,14 @@ const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/ag
|
|
|
21
21
|
) || "agent-browser";
|
|
22
22
|
const PROFILE_CLONE_TIMEOUT_MS = 120_000;
|
|
23
23
|
const ORACLE_SUBPROCESS_KILL_GRACE_MS = 2_000;
|
|
24
|
+
const WORKSPACE_ROOT_MARKERS = [
|
|
25
|
+
".pi/extensions/oracle.json",
|
|
26
|
+
] as const;
|
|
27
|
+
const REQUIRED_ORACLE_DEPENDENCIES = [
|
|
28
|
+
{ name: "agent-browser", command: AGENT_BROWSER_BIN },
|
|
29
|
+
{ name: "tar", command: "tar" },
|
|
30
|
+
{ name: "zstd", command: "zstd" },
|
|
31
|
+
] as const;
|
|
24
32
|
|
|
25
33
|
export interface OracleRuntimeLeaseMetadata {
|
|
26
34
|
jobId: string;
|
|
@@ -51,7 +59,7 @@ export interface OracleConversationLeaseAttempt {
|
|
|
51
59
|
blocker?: OracleConversationLeaseMetadata;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
function resolveRealCwd(cwd: string): string {
|
|
55
63
|
try {
|
|
56
64
|
return realpathSync(cwd);
|
|
57
65
|
} catch {
|
|
@@ -59,6 +67,26 @@ export function getProjectId(cwd: string): string {
|
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
function hasWorkspaceRootMarker(path: string): boolean {
|
|
71
|
+
return WORKSPACE_ROOT_MARKERS.some((marker) => existsSync(join(path, marker)));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveWorkspaceRoot(realCwd: string): string {
|
|
75
|
+
let current = realCwd;
|
|
76
|
+
let nearestMarkerRoot: string | undefined;
|
|
77
|
+
while (true) {
|
|
78
|
+
if (existsSync(join(current, ".git"))) return current;
|
|
79
|
+
if (!nearestMarkerRoot && hasWorkspaceRootMarker(current)) nearestMarkerRoot = current;
|
|
80
|
+
const parent = dirname(current);
|
|
81
|
+
if (parent === current) return nearestMarkerRoot ?? realCwd;
|
|
82
|
+
current = parent;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getProjectId(cwd: string): string {
|
|
87
|
+
return resolveWorkspaceRoot(resolveRealCwd(cwd));
|
|
88
|
+
}
|
|
89
|
+
|
|
62
90
|
export function hasPersistedSessionFile(originSessionFile: string | undefined): originSessionFile is string {
|
|
63
91
|
return Boolean(originSessionFile);
|
|
64
92
|
}
|
|
@@ -98,6 +126,144 @@ export function authSessionName(config: OracleConfig): string {
|
|
|
98
126
|
return `${config.browser.sessionPrefix}-auth`;
|
|
99
127
|
}
|
|
100
128
|
|
|
129
|
+
function missingAuthSeedProfileMessage(seedDir: string): string {
|
|
130
|
+
return `Oracle auth seed profile not found: ${seedDir}. Run /oracle-auth first.`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function invalidAuthSeedProfileTypeMessage(seedDir: string): string {
|
|
134
|
+
return `Oracle auth seed profile is not a directory: ${seedDir}. Remove the invalid path or rerun /oracle-auth.`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function unreadableAuthSeedProfileMessage(seedDir: string): string {
|
|
138
|
+
return `Oracle auth seed profile is not readable: ${seedDir}. Fix its permissions or rerun /oracle-auth.`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function missingBrowserExecutableMessage(executablePath: string): string {
|
|
142
|
+
return `Configured oracle browser executable does not exist: ${executablePath}. Fix browser.executablePath or install Chrome there.`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function nonExecutableBrowserMessage(executablePath: string): string {
|
|
146
|
+
return `Configured oracle browser executable is not executable: ${executablePath}. Fix browser.executablePath permissions or point it at a runnable Chrome binary.`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function missingLocalDependencyMessage(name: string): string {
|
|
150
|
+
return `Oracle prerequisite not found on PATH: ${name}. Install ${name} and retry.`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function unwritableOracleDirectoryMessage(label: "runtime profiles" | "jobs", path: string): string {
|
|
154
|
+
return `Oracle ${label} directory is not writable: ${path}. Fix its permissions or configure a writable path, then retry.`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function resolveExecutableOnPath(command: string): Promise<string | undefined> {
|
|
158
|
+
if (!command) return undefined;
|
|
159
|
+
if (command.includes("/")) {
|
|
160
|
+
try {
|
|
161
|
+
await access(command, fsConstants.X_OK);
|
|
162
|
+
return command;
|
|
163
|
+
} catch {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const pathValue = process.env.PATH ?? "";
|
|
169
|
+
for (const segment of pathValue.split(delimiter)) {
|
|
170
|
+
if (!segment) continue;
|
|
171
|
+
const candidate = join(segment, command);
|
|
172
|
+
try {
|
|
173
|
+
await access(candidate, fsConstants.X_OK);
|
|
174
|
+
return candidate;
|
|
175
|
+
} catch {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function assertConfiguredBrowserExecutableReady(executablePath: string | undefined): Promise<void> {
|
|
183
|
+
if (!executablePath) return;
|
|
184
|
+
let executableStats;
|
|
185
|
+
try {
|
|
186
|
+
executableStats = await stat(executablePath);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
189
|
+
if (code === "ENOENT") throw new Error(missingBrowserExecutableMessage(executablePath));
|
|
190
|
+
if (code === "EACCES" || code === "EPERM") throw new Error(nonExecutableBrowserMessage(executablePath));
|
|
191
|
+
throw new Error(`Failed to inspect configured oracle browser executable ${executablePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!executableStats.isFile()) {
|
|
195
|
+
throw new Error(nonExecutableBrowserMessage(executablePath));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await access(executablePath, fsConstants.X_OK);
|
|
200
|
+
} catch {
|
|
201
|
+
throw new Error(nonExecutableBrowserMessage(executablePath));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function assertRequiredLocalDependencyReady(name: string, command: string): Promise<void> {
|
|
206
|
+
const resolved = await resolveExecutableOnPath(command);
|
|
207
|
+
if (!resolved) throw new Error(missingLocalDependencyMessage(name));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function assertWritableDirectory(path: string, label: "runtime profiles" | "jobs"): Promise<void> {
|
|
211
|
+
try {
|
|
212
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
213
|
+
} catch {
|
|
214
|
+
throw new Error(unwritableOracleDirectoryMessage(label, path));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let directoryStats;
|
|
218
|
+
try {
|
|
219
|
+
directoryStats = await stat(path);
|
|
220
|
+
} catch {
|
|
221
|
+
throw new Error(unwritableOracleDirectoryMessage(label, path));
|
|
222
|
+
}
|
|
223
|
+
if (!directoryStats.isDirectory()) {
|
|
224
|
+
throw new Error(unwritableOracleDirectoryMessage(label, path));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await access(path, fsConstants.W_OK | fsConstants.X_OK);
|
|
229
|
+
} catch {
|
|
230
|
+
throw new Error(unwritableOracleDirectoryMessage(label, path));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Promise<void> {
|
|
235
|
+
const seedDir = config.browser.authSeedProfileDir;
|
|
236
|
+
let seedStats;
|
|
237
|
+
try {
|
|
238
|
+
seedStats = await stat(seedDir);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
241
|
+
if (code === "ENOENT") throw new Error(missingAuthSeedProfileMessage(seedDir));
|
|
242
|
+
if (code === "EACCES" || code === "EPERM") throw new Error(unreadableAuthSeedProfileMessage(seedDir));
|
|
243
|
+
throw new Error(`Failed to inspect oracle auth seed profile ${seedDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!seedStats.isDirectory()) {
|
|
247
|
+
throw new Error(invalidAuthSeedProfileTypeMessage(seedDir));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await access(seedDir, fsConstants.R_OK | fsConstants.X_OK);
|
|
252
|
+
} catch {
|
|
253
|
+
throw new Error(unreadableAuthSeedProfileMessage(seedDir));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function assertOracleSubmitPrerequisites(config: OracleConfig): Promise<void> {
|
|
258
|
+
await assertOracleAuthSeedProfileReady(config);
|
|
259
|
+
await assertConfiguredBrowserExecutableReady(config.browser.executablePath);
|
|
260
|
+
for (const dependency of REQUIRED_ORACLE_DEPENDENCIES) {
|
|
261
|
+
await assertRequiredLocalDependencyReady(dependency.name, dependency.command);
|
|
262
|
+
}
|
|
263
|
+
await assertWritableDirectory(config.browser.runtimeProfilesDir, "runtime profiles");
|
|
264
|
+
await assertWritableDirectory(ORACLE_JOBS_DIR, "jobs");
|
|
265
|
+
}
|
|
266
|
+
|
|
101
267
|
export function getSeedGeneration(config: OracleConfig): string | undefined {
|
|
102
268
|
const path = join(config.browser.authSeedProfileDir, SEED_GENERATION_FILE);
|
|
103
269
|
if (!existsSync(path)) return undefined;
|
|
@@ -267,9 +433,7 @@ export async function cloneSeedProfileToRuntime(
|
|
|
267
433
|
options?: { cpTimeoutMs?: number },
|
|
268
434
|
): Promise<string | undefined> {
|
|
269
435
|
const seedDir = config.browser.authSeedProfileDir;
|
|
270
|
-
|
|
271
|
-
throw new Error(`Oracle auth seed profile not found: ${seedDir}. Run /oracle-auth first.`);
|
|
272
|
-
}
|
|
436
|
+
await assertOracleAuthSeedProfileReady(config);
|
|
273
437
|
|
|
274
438
|
await withAuthLock({ runtimeProfileDir, seedDir }, async () => {
|
|
275
439
|
await rm(runtimeProfileDir, { recursive: true, force: true }).catch(() => undefined);
|