diff --git a/scripts/project-lint.ts b/scripts/project-lint.ts index d913ba3..20fc6a9 100644 --- a/scripts/project-lint.ts +++ b/scripts/project-lint.ts @@ -247,8 +247,6 @@ const rules: DummyRuleMap = { // Collides with explicit-types `no-undefined-type`: a `let` that `try` // assigns would need `T | undefined`. "init-declarations": "off", - // Collides with `no-await-in-loop`: sequential steps chain through `reduce`. - "unicorn/no-array-reduce": "off", // Collides with literal Gitea Actions `${{ }}` expressions in workflow strings. "no-template-curly-in-string": "off", // Node code, everywhere: this action runs on Node. @@ -264,6 +262,12 @@ const rules: DummyRuleMap = { // PENDING USER: a fitting `name` needs a constructor and changes logged // error names. "unicorn/custom-error-definition": "off", + // Sequential awaits are intentional; workarounds cost more than they save. + "no-await-in-loop": "off", + // Not type-aware; TypeScript's `noImplicitReturns` covers it. + "consistent-return": "off", + // TypeScript's type checking covers exhaustive switches. + "typescript/switch-exhaustiveness-check": "off", // Off, too much churn: // Literal numbers read better than a constant per use. "no-magic-numbers": "off", diff --git a/src/profiles/profiles.ts b/src/profiles/profiles.ts index af19b04..90fd3fe 100644 --- a/src/profiles/profiles.ts +++ b/src/profiles/profiles.ts @@ -53,37 +53,26 @@ export function failureFooter(failures: ProfileFailure[]): string { .join(", ")}`; } -// Tries the profiles from `index` on, moving on only after a sign-in failure -// or exhausted usage, and records every profile it tried. -async function attemptFrom( - profiles: P[], - attempt: (profile: P) => Promise, - failures: ProfileFailure[], - index: number, -): Promise { - const profile = profiles[index]; - if (profile === undefined) { - throw new Error("profiles must contain at least one profile"); - } - try { - return await attempt(profile); - } catch (error) { - const reason = failureReason(error); - failures.push({ type: profile.type, model: profile.model, reason }); - if (reason === "failed" || index === profiles.length - 1) { - throw error; - } - console.error(`${profile.type} ${reason}; trying next profile`); - return attemptFrom(profiles, attempt, failures, index + 1); - } -} - -export function runProfiles( +// Tries the profiles in order, moving on only after a sign-in failure or +// exhausted usage, and records every profile it tried. +export async function runProfiles( profiles: P[], attempt: (profile: P) => Promise, failures: ProfileFailure[] = [], ): Promise { - return attemptFrom(profiles, attempt, failures, 0); + for (const [index, profile] of profiles.entries()) { + try { + return await attempt(profile); + } catch (error) { + const reason = failureReason(error); + failures.push({ type: profile.type, model: profile.model, reason }); + if (reason === "failed" || index === profiles.length - 1) { + throw error; + } + console.error(`${profile.type} ${reason}; trying next profile`); + } + } + throw new Error("profiles must contain at least one profile"); } // Environments are maps from variable name to value, converted to the object diff --git a/src/run/index.ts b/src/run/index.ts index 56ef719..8e8be96 100644 --- a/src/run/index.ts +++ b/src/run/index.ts @@ -283,21 +283,6 @@ async function runClaude(profile: RuntimeProfile, token: string, prompt: string) return result.result; } -// Polls every 100 ms until the login prints something, failing if it exits first. -async function pollForInstructions( - shownText: () => string, - status: Promise, -): Promise { - if (shownText().trim() !== "") { - return; - } - const exited = await Promise.race([status.then(() => true), sleep(100, false)]); - if (exited) { - throw new AgentAuthError("AI bot login produced no instructions"); - } - await pollForInstructions(shownText, status); -} - async function codexDeviceLogin( profile: RuntimeProfile, environment: ReadonlyMap, @@ -319,7 +304,12 @@ async function codexDeviceLogin( }; const drained = Promise.all([collect(login.stdout), collect(login.stderr)]); await sleep(3000); - await pollForInstructions(() => shown, status); + while (shown.trim() === "") { + const exited = await Promise.race([status.then(() => true), sleep(100, false)]); + if (exited) { + throw new AgentAuthError("AI bot login produced no instructions"); + } + } await postAuthorComment(withProfile(shown, profile)); const [code] = await Promise.all([status, drained]); if (code !== 0) { diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index fba9fc3..cf09236 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -61,32 +61,28 @@ export async function prepareRuntime( await writePrivate(reviewerFile, credentials.reviewerToken, 0, 0); // One profile at a time, in order, as each claims the next UID. - const profiles = await credentials.profiles.reduce>( - async (previous, profile, index) => { - const prepared = await previous; - const uid = FIRST_PROFILE_UID + index; - const home = join(runtimeDir, `profile-${index}`); - await mkdir(home, { mode: 0o700 }); - await chmod(home, 0o700); - await chown(home, uid, uid); - const tokenFile = - profile.token === undefined || profile.token === "" ? null : join(home, "token"); - if (tokenFile !== null && profile.token !== undefined) { - await writePrivate(tokenFile, profile.token, uid, uid); - } - prepared.push({ - index, - type: profile.type, - model: profile.model, - uid, - gid: uid, - home, - tokenFile, - }); - return prepared; - }, - Promise.resolve([]), - ); + const profiles: RuntimeProfile[] = []; + for (const [index, profile] of credentials.profiles.entries()) { + const uid = FIRST_PROFILE_UID + index; + const home = join(runtimeDir, `profile-${index}`); + await mkdir(home, { mode: 0o700 }); + await chmod(home, 0o700); + await chown(home, uid, uid); + const tokenFile = + profile.token === undefined || profile.token === "" ? null : join(home, "token"); + if (tokenFile !== null && profile.token !== undefined) { + await writePrivate(tokenFile, profile.token, uid, uid); + } + profiles.push({ + index, + type: profile.type, + model: profile.model, + uid, + gid: uid, + home, + tokenFile, + }); + } const manifestPath = join(runtimeDir, "manifest.json"); const manifest: RuntimeManifest = {