Skip to content

Commit 2d95108

Browse files
committed
Add robust companion resolution and runtime cache
Refactor companion resolution to handle multiple import specifiers and runtime cache locations. Adds robust fallthrough resolution: try published JS constants, probe OpenCode runtime cache candidate paths, read source TS/JS constants, inspect package.json-root candidates, and finally package entry. Handles package export-blocking errors, missing files, and non-fallthrough resolver errors; normalizes credentials and parses credential values from source files. Includes many helper functions to build presence/credential states and to try reading candidate files. Tests updated to mock runtime dirs, cover cache probing, package export blocks, package-root lookup, direct runtime candidates, and error scenarios.
1 parent 30d9af5 commit 2d95108

2 files changed

Lines changed: 517 additions & 82 deletions

File tree

src/lib/google-antigravity-companion.ts

Lines changed: 282 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
1+
import { readFile } from "fs/promises";
12
import { createRequire } from "node:module";
3+
import { dirname, join } from "node:path";
24
import { pathToFileURL } from "node:url";
35

6+
import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js";
7+
48
const require = createRequire(import.meta.url);
59

610
const COMPANION_PACKAGE_NAME = "opencode-antigravity-auth";
7-
const COMPANION_IMPORT_SPECIFIER = `${COMPANION_PACKAGE_NAME}/dist/src/constants.js`;
11+
const COMPANION_JS_IMPORT_SPECIFIERS = [
12+
`${COMPANION_PACKAGE_NAME}/dist/src/constants.js`,
13+
`${COMPANION_PACKAGE_NAME}/src/constants.js`,
14+
] as const;
15+
const COMPANION_SOURCE_IMPORT_SPECIFIER = `${COMPANION_PACKAGE_NAME}/src/constants.ts`;
16+
const COMPANION_PACKAGE_JSON_SPECIFIER = `${COMPANION_PACKAGE_NAME}/package.json`;
17+
const COMPANION_DIRECT_CANDIDATE_PATHS = [
18+
["dist", "src", "constants.js"],
19+
["src", "constants.ts"],
20+
["src", "constants.js"],
21+
["dist", "index.js"],
22+
] as const;
823
const COMPANION_MISSING_ERROR = `Install ${COMPANION_PACKAGE_NAME} separately to enable Google Antigravity quota`;
924
const COMPANION_INVALID_ERROR = `Installed ${COMPANION_PACKAGE_NAME} package is incompatible`;
1025

@@ -51,6 +66,10 @@ type CompanionModule = {
5166
ANTIGRAVITY_CLIENT_SECRET?: unknown;
5267
};
5368

69+
type CompanionResolutionContext = {
70+
packageFound: boolean;
71+
};
72+
5473
let resolvedCompanionStatePromise: Promise<ResolvedCompanionState> | null = null;
5574

5675
function isModuleNotFoundError(error: unknown): boolean {
@@ -67,94 +86,288 @@ function isModuleNotFoundError(error: unknown): boolean {
6786
return message.includes("Cannot find module");
6887
}
6988

70-
async function resolveCompanionState(): Promise<ResolvedCompanionState> {
71-
let resolvedPath: string;
89+
function isPackagePathNotExportedError(error: unknown): boolean {
90+
if (!error || typeof error !== "object") {
91+
return false;
92+
}
93+
94+
return "code" in error && error.code === "ERR_PACKAGE_PATH_NOT_EXPORTED";
95+
}
96+
97+
function isFallthroughResolutionError(error: unknown): boolean {
98+
return isModuleNotFoundError(error) || isPackagePathNotExportedError(error);
99+
}
100+
101+
function normalizeCredential(value: unknown): string {
102+
return typeof value === "string" ? value.trim() : "";
103+
}
104+
105+
function getCompanionResolvePaths(): string[] {
106+
return getOpencodeRuntimeDirCandidates().cacheDirs;
107+
}
72108

109+
function markPackageFoundForExportBlock(
110+
error: unknown,
111+
context: CompanionResolutionContext,
112+
): void {
113+
if (isPackagePathNotExportedError(error)) {
114+
context.packageFound = true;
115+
}
116+
}
117+
118+
function resolveCompanionSpecifier(
119+
specifier: string,
120+
context: CompanionResolutionContext,
121+
): string {
73122
try {
74-
resolvedPath = require.resolve(COMPANION_IMPORT_SPECIFIER);
123+
return require.resolve(specifier);
75124
} catch (error) {
76-
if (isModuleNotFoundError(error)) {
77-
return {
78-
presence: {
79-
state: "missing",
80-
importSpecifier: COMPANION_IMPORT_SPECIFIER,
81-
error: COMPANION_MISSING_ERROR,
82-
},
83-
credentials: {
84-
state: "missing",
85-
error: COMPANION_MISSING_ERROR,
86-
},
87-
};
125+
markPackageFoundForExportBlock(error, context);
126+
if (!isFallthroughResolutionError(error)) {
127+
throw error;
88128
}
89129

90-
return {
91-
presence: {
92-
state: "invalid",
93-
importSpecifier: COMPANION_IMPORT_SPECIFIER,
94-
error: COMPANION_INVALID_ERROR,
95-
},
96-
credentials: {
97-
state: "invalid",
98-
error: COMPANION_INVALID_ERROR,
99-
},
100-
};
130+
try {
131+
return require.resolve(specifier, { paths: getCompanionResolvePaths() });
132+
} catch (resolvePathsError) {
133+
markPackageFoundForExportBlock(resolvePathsError, context);
134+
throw resolvePathsError;
135+
}
136+
}
137+
}
138+
139+
function buildConfiguredState(params: {
140+
importSpecifier: string;
141+
resolvedPath: string;
142+
clientId: string;
143+
clientSecret: string;
144+
}): ResolvedCompanionState {
145+
return {
146+
presence: {
147+
state: "present",
148+
importSpecifier: params.importSpecifier,
149+
resolvedPath: params.resolvedPath,
150+
},
151+
credentials: {
152+
state: "configured",
153+
clientId: params.clientId,
154+
clientSecret: params.clientSecret,
155+
resolvedPath: params.resolvedPath,
156+
},
157+
};
158+
}
159+
160+
function buildInvalidState(importSpecifier: string, resolvedPath?: string): ResolvedCompanionState {
161+
return {
162+
presence: {
163+
state: "invalid",
164+
importSpecifier,
165+
...(resolvedPath ? { resolvedPath } : {}),
166+
error: COMPANION_INVALID_ERROR,
167+
},
168+
credentials: {
169+
state: "invalid",
170+
...(resolvedPath ? { resolvedPath } : {}),
171+
error: COMPANION_INVALID_ERROR,
172+
},
173+
};
174+
}
175+
176+
function parseSourceCredentials(content: string): { clientId: string; clientSecret: string } | null {
177+
const clientId =
178+
content.match(/(?:export\s+const|const|var)\s+ANTIGRAVITY_CLIENT_ID\s*=\s*["']([^"']+)["']/)?.[1]?.trim() ?? "";
179+
const clientSecret =
180+
content.match(/(?:export\s+const|const|var)\s+ANTIGRAVITY_CLIENT_SECRET\s*=\s*["']([^"']+)["']/)?.[1]?.trim() ?? "";
181+
182+
return clientId && clientSecret ? { clientId, clientSecret } : null;
183+
}
184+
185+
function getPackageCredentialPaths(packageRoot: string): string[] {
186+
return COMPANION_DIRECT_CANDIDATE_PATHS.map((parts) => join(packageRoot, ...parts));
187+
}
188+
189+
function getRuntimeCredentialPaths(): string[] {
190+
return getCompanionResolvePaths().flatMap((cacheDir) =>
191+
getPackageCredentialPaths(join(cacheDir, "node_modules", COMPANION_PACKAGE_NAME)),
192+
);
193+
}
194+
195+
function isMissingFileError(error: unknown): boolean {
196+
if (!error || typeof error !== "object") {
197+
return false;
198+
}
199+
200+
const code = "code" in error ? error.code : undefined;
201+
return code === "ENOENT" || code === "ENOTDIR";
202+
}
203+
204+
async function tryReadCredentialsPath(
205+
importSpecifier: string,
206+
resolvedPath: string,
207+
): Promise<ResolvedCompanionState | null> {
208+
let content: string;
209+
try {
210+
content = await readFile(resolvedPath, "utf8");
211+
} catch (error) {
212+
return isMissingFileError(error) ? null : buildInvalidState(importSpecifier, resolvedPath);
213+
}
214+
215+
const credentials = parseSourceCredentials(content);
216+
if (!credentials) {
217+
return buildInvalidState(importSpecifier, resolvedPath);
218+
}
219+
220+
return buildConfiguredState({
221+
importSpecifier,
222+
resolvedPath,
223+
clientId: credentials.clientId,
224+
clientSecret: credentials.clientSecret,
225+
});
226+
}
227+
228+
async function tryResolveJsConstants(
229+
importSpecifier: string,
230+
context: CompanionResolutionContext,
231+
): Promise<ResolvedCompanionState | null> {
232+
let resolvedPath: string;
233+
try {
234+
resolvedPath = resolveCompanionSpecifier(importSpecifier, context);
235+
} catch (error) {
236+
if (isFallthroughResolutionError(error)) {
237+
return null;
238+
}
239+
return buildInvalidState(importSpecifier);
101240
}
102241

103242
let companionModule: CompanionModule;
104243
try {
105244
companionModule = (await import(pathToFileURL(resolvedPath).href)) as CompanionModule;
106245
} catch {
107-
return {
108-
presence: {
109-
state: "invalid",
110-
importSpecifier: COMPANION_IMPORT_SPECIFIER,
111-
resolvedPath,
112-
error: COMPANION_INVALID_ERROR,
113-
},
114-
credentials: {
115-
state: "invalid",
116-
resolvedPath,
117-
error: COMPANION_INVALID_ERROR,
118-
},
119-
};
246+
return buildInvalidState(importSpecifier, resolvedPath);
120247
}
121248

122-
const clientId =
123-
typeof companionModule.ANTIGRAVITY_CLIENT_ID === "string"
124-
? companionModule.ANTIGRAVITY_CLIENT_ID.trim()
125-
: "";
126-
const clientSecret =
127-
typeof companionModule.ANTIGRAVITY_CLIENT_SECRET === "string"
128-
? companionModule.ANTIGRAVITY_CLIENT_SECRET.trim()
129-
: "";
130-
249+
const clientId = normalizeCredential(companionModule.ANTIGRAVITY_CLIENT_ID);
250+
const clientSecret = normalizeCredential(companionModule.ANTIGRAVITY_CLIENT_SECRET);
131251
if (!clientId || !clientSecret) {
132-
return {
133-
presence: {
134-
state: "invalid",
135-
importSpecifier: COMPANION_IMPORT_SPECIFIER,
136-
resolvedPath,
137-
error: COMPANION_INVALID_ERROR,
138-
},
139-
credentials: {
140-
state: "invalid",
141-
resolvedPath,
142-
error: COMPANION_INVALID_ERROR,
143-
},
144-
};
252+
return buildInvalidState(importSpecifier, resolvedPath);
253+
}
254+
255+
return buildConfiguredState({ importSpecifier, resolvedPath, clientId, clientSecret });
256+
}
257+
258+
async function tryRuntimeCredentialPaths(): Promise<ResolvedCompanionState | null> {
259+
for (const candidatePath of getRuntimeCredentialPaths()) {
260+
const resolved = await tryReadCredentialsPath(COMPANION_SOURCE_IMPORT_SPECIFIER, candidatePath);
261+
if (resolved) {
262+
return resolved;
263+
}
264+
}
265+
266+
return null;
267+
}
268+
269+
async function tryResolveSourceConstants(
270+
context: CompanionResolutionContext,
271+
): Promise<ResolvedCompanionState | null> {
272+
let resolvedPath: string;
273+
try {
274+
resolvedPath = resolveCompanionSpecifier(COMPANION_SOURCE_IMPORT_SPECIFIER, context);
275+
} catch (error) {
276+
return isFallthroughResolutionError(error)
277+
? null
278+
: buildInvalidState(COMPANION_SOURCE_IMPORT_SPECIFIER);
279+
}
280+
281+
return (
282+
(await tryReadCredentialsPath(COMPANION_SOURCE_IMPORT_SPECIFIER, resolvedPath)) ??
283+
buildInvalidState(COMPANION_SOURCE_IMPORT_SPECIFIER, resolvedPath)
284+
);
285+
}
286+
287+
async function tryResolvePackageJson(
288+
context: CompanionResolutionContext,
289+
): Promise<ResolvedCompanionState | null> {
290+
let packageJsonPath: string;
291+
try {
292+
packageJsonPath = resolveCompanionSpecifier(COMPANION_PACKAGE_JSON_SPECIFIER, context);
293+
} catch (error) {
294+
return isFallthroughResolutionError(error)
295+
? null
296+
: buildInvalidState(COMPANION_PACKAGE_JSON_SPECIFIER);
297+
}
298+
299+
const packageRoot = dirname(packageJsonPath);
300+
for (const candidatePath of getPackageCredentialPaths(packageRoot)) {
301+
const resolved = await tryReadCredentialsPath(COMPANION_PACKAGE_JSON_SPECIFIER, candidatePath);
302+
if (resolved) {
303+
return resolved;
304+
}
305+
}
306+
307+
return buildInvalidState(COMPANION_PACKAGE_JSON_SPECIFIER, packageJsonPath);
308+
}
309+
310+
async function tryResolvePackageEntry(
311+
context: CompanionResolutionContext,
312+
): Promise<ResolvedCompanionState | null> {
313+
let packageEntryPath: string;
314+
try {
315+
packageEntryPath = resolveCompanionSpecifier(COMPANION_PACKAGE_NAME, context);
316+
} catch (error) {
317+
return isFallthroughResolutionError(error)
318+
? null
319+
: buildInvalidState(COMPANION_PACKAGE_NAME);
320+
}
321+
322+
return (
323+
(await tryReadCredentialsPath(COMPANION_PACKAGE_NAME, packageEntryPath)) ??
324+
buildInvalidState(COMPANION_PACKAGE_NAME, packageEntryPath)
325+
);
326+
}
327+
328+
async function resolveCompanionState(): Promise<ResolvedCompanionState> {
329+
const context: CompanionResolutionContext = { packageFound: false };
330+
331+
for (const importSpecifier of COMPANION_JS_IMPORT_SPECIFIERS) {
332+
const resolved = await tryResolveJsConstants(importSpecifier, context);
333+
if (resolved) {
334+
return resolved;
335+
}
336+
}
337+
338+
const runtimeResolved = await tryRuntimeCredentialPaths();
339+
if (runtimeResolved) {
340+
return runtimeResolved;
341+
}
342+
343+
const sourceResolved = await tryResolveSourceConstants(context);
344+
if (sourceResolved) {
345+
return sourceResolved;
346+
}
347+
348+
const packageJsonResolved = await tryResolvePackageJson(context);
349+
if (packageJsonResolved) {
350+
return packageJsonResolved;
351+
}
352+
353+
const packageEntryResolved = await tryResolvePackageEntry(context);
354+
if (packageEntryResolved) {
355+
return packageEntryResolved;
356+
}
357+
358+
if (context.packageFound) {
359+
return buildInvalidState(COMPANION_PACKAGE_NAME);
145360
}
146361

147362
return {
148363
presence: {
149-
state: "present",
150-
importSpecifier: COMPANION_IMPORT_SPECIFIER,
151-
resolvedPath,
364+
state: "missing",
365+
importSpecifier: COMPANION_SOURCE_IMPORT_SPECIFIER,
366+
error: COMPANION_MISSING_ERROR,
152367
},
153368
credentials: {
154-
state: "configured",
155-
clientId,
156-
clientSecret,
157-
resolvedPath,
369+
state: "missing",
370+
error: COMPANION_MISSING_ERROR,
158371
},
159372
};
160373
}

0 commit comments

Comments
 (0)