@koreanhole/opencodex 2.99.0 → 3.0.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.
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env bun
2
+ import { randomBytes } from "node:crypto";
3
+ import { rmSync } from "node:fs";
4
+ import {
5
+ ADAPTER_DEFAULT_PORT,
6
+ ADAPTER_PROVIDER_IDS,
7
+ ADAPTER_TOKEN_ENV,
8
+ AdapterConfigError,
9
+ adapterRuntimePath,
10
+ discoverManagedProviderMetadata,
11
+ readAdapterRuntime,
12
+ readUpstreamConfig,
13
+ removeAdapterRuntimeIfPid,
14
+ restoreManagedProviderRows,
15
+ setAdapterDefaultProvider,
16
+ syncManagedProviderRows,
17
+ writeAdapterRuntime,
18
+ type AdapterProviderId,
19
+ } from "../adapter-runtime/config";
20
+ import { startAdapterGateway, type AdapterGateway } from "../adapter-runtime/gateway";
21
+ import { ConfigError } from "../config";
22
+ import { UpstreamPackageError, resolveUpstreamPackage } from "../upstream/package";
23
+ import { loadUpstreamCli } from "../upstream/cli";
24
+ import { runAdapterCli } from "./index";
25
+
26
+ type AdapterCli = (argv: string[]) => Promise<number>;
27
+ type UpstreamLoader = () => Promise<void>;
28
+ type StartPreparer = (env: NodeJS.ProcessEnv) => Promise<unknown>;
29
+
30
+ export interface EntryDeps {
31
+ adapterCli?: AdapterCli;
32
+ loadUpstream?: UpstreamLoader;
33
+ prepareStart?: StartPreparer;
34
+ }
35
+
36
+ const DISTRIBUTION_PACKAGE = "@koreanhole/opencodex";
37
+
38
+ function isAdapterId(value: string | undefined): value is AdapterProviderId {
39
+ return value === "gemini-cli" || value === "cline-cli";
40
+ }
41
+
42
+ export function isAdapterOwnedCommand(argv: readonly string[]): boolean {
43
+ const command = argv[0];
44
+ if (command === "__cli-agent-mcp") return true;
45
+ if (command === "update") return true;
46
+ if (command === "system" && argv.slice(1).includes("update")) return true;
47
+ if (command === "login" && (argv[1] === "cline-cli" || argv[1] === "gemini-cli")) return true;
48
+ if (command === "adapter" && argv[1] === "detach") return true;
49
+ if (command !== "provider") return false;
50
+ if (argv[1] === "add" && isAdapterId(argv[2])) return true;
51
+ if (argv[1] === "use" && isAdapterId(argv[2])) return true;
52
+ return false;
53
+ }
54
+
55
+ function takeFlag(args: string[], flag: string): boolean {
56
+ const index = args.indexOf(flag);
57
+ if (index < 0) return false;
58
+ args.splice(index, 1);
59
+ return true;
60
+ }
61
+
62
+ async function adapterProviderCommand(args: string[], env: NodeJS.ProcessEnv): Promise<number> {
63
+ const sub = args.shift();
64
+ if (sub !== "add" && sub !== "use") throw new ConfigError("Unknown provider command: " + String(sub ?? "(missing)"));
65
+ const provider = args.shift();
66
+ if (!isAdapterId(provider)) throw new ConfigError("Only gemini-cli and cline-cli are handled for provider " + sub + ".");
67
+ if (sub === "add") {
68
+ const setDefault = takeFlag(args, "--set-default");
69
+ if (args.length > 0) throw new ConfigError("Unknown provider add option: " + args[0]);
70
+ const runtime = await readAdapterRuntime(env);
71
+ await syncManagedProviderRows({
72
+ env,
73
+ gatewayPort: runtime?.port ?? ADAPTER_DEFAULT_PORT,
74
+ ...(setDefault ? { defaultProvider: provider } : {}),
75
+ });
76
+ console.log("Configured " + provider + ".");
77
+ return 0;
78
+ }
79
+ if (args.length > 0) throw new ConfigError("Unknown provider use option: " + args[0]);
80
+ await setAdapterDefaultProvider(provider, env);
81
+ console.log("Default provider: " + provider);
82
+ return 0;
83
+ }
84
+
85
+ async function detachAdapter(env: NodeJS.ProcessEnv): Promise<number> {
86
+ const result = await restoreManagedProviderRows(env);
87
+ if (!result.restored) {
88
+ console.log("CLI adapters are not attached.");
89
+ return 0;
90
+ }
91
+ if (result.preservedUserRows.length > 0) {
92
+ console.log("Preserved user-owned provider rows: " + result.preservedUserRows.join(", "));
93
+ }
94
+ console.log("CLI adapter provider rows detached.");
95
+ return 0;
96
+ }
97
+
98
+ function printDistributionUpdate(argv: readonly string[]): number {
99
+ const command = "npm install -g " + DISTRIBUTION_PACKAGE + "@latest";
100
+ if (argv.includes("--json")) {
101
+ console.log(JSON.stringify({
102
+ ok: false,
103
+ reason: "distribution_managed",
104
+ package: DISTRIBUTION_PACKAGE,
105
+ command,
106
+ }));
107
+ } else {
108
+ console.error("ocx: updates are managed by " + DISTRIBUTION_PACKAGE + ".");
109
+ console.error("Run: " + command);
110
+ }
111
+ return 2;
112
+ }
113
+
114
+ async function runAdapterCommand(argv: string[], env: NodeJS.ProcessEnv, adapterCli: AdapterCli): Promise<number> {
115
+ if (argv[0] === "__cli-agent-mcp") return adapterCli(argv);
116
+ if (argv[0] === "login") return adapterCli(argv);
117
+ if (argv[0] === "update" || (argv[0] === "system" && argv.slice(1).includes("update"))) {
118
+ return printDistributionUpdate(argv);
119
+ }
120
+ if (argv[0] === "adapter" && argv[1] === "detach") return detachAdapter(env);
121
+ if (argv[0] === "provider") return adapterProviderCommand(argv.slice(1), env);
122
+ throw new ConfigError("Internal adapter dispatch error.");
123
+ }
124
+
125
+ function pidAlive(pid: number): boolean {
126
+ try {
127
+ process.kill(pid, 0);
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+
134
+ /** Attach the two managed rows and keep the gateway in the serving process. */
135
+ export async function prepareAdapterStart(
136
+ env: NodeJS.ProcessEnv = process.env,
137
+ deps: {
138
+ discoverMetadata?: typeof discoverManagedProviderMetadata;
139
+ startGateway?: typeof startAdapterGateway;
140
+ syncRows?: typeof syncManagedProviderRows;
141
+ } = {},
142
+ ): Promise<AdapterGateway | undefined> {
143
+ const existing = await readAdapterRuntime(env);
144
+ const inheritedToken = env[ADAPTER_TOKEN_ENV]?.trim();
145
+ if (existing && existing.pid !== process.pid && pidAlive(existing.pid) && !inheritedToken) {
146
+ throw new AdapterConfigError(
147
+ "The CLI adapter gateway is already running in another process; restart it from that process or stop it first.",
148
+ );
149
+ }
150
+ const token = inheritedToken || randomBytes(32).toString("base64url");
151
+ env[ADAPTER_TOKEN_ENV] = token;
152
+ let gateway: AdapterGateway | undefined;
153
+ let gatewayPort: number;
154
+ if (existing && existing.pid !== process.pid && pidAlive(existing.pid)) {
155
+ gatewayPort = existing.port;
156
+ } else {
157
+ if (existing) await removeAdapterRuntimeIfPid(existing.pid, env);
158
+ gateway = (deps.startGateway ?? startAdapterGateway)({ token, env });
159
+ gatewayPort = gateway.port;
160
+ await writeAdapterRuntime({
161
+ version: 1,
162
+ pid: process.pid,
163
+ host: "127.0.0.1",
164
+ port: gatewayPort,
165
+ tokenEnv: ADAPTER_TOKEN_ENV,
166
+ providers: [...ADAPTER_PROVIDER_IDS],
167
+ startedAt: new Date().toISOString(),
168
+ }, env);
169
+ process.once("exit", () => {
170
+ try {
171
+ rmSync(adapterRuntimePath(env), { force: true });
172
+ } catch {
173
+ // best effort
174
+ }
175
+ });
176
+ }
177
+
178
+ try {
179
+ const metadata = await (deps.discoverMetadata ?? discoverManagedProviderMetadata)(env).catch(() => undefined);
180
+ const synced = await (deps.syncRows ?? syncManagedProviderRows)({
181
+ env,
182
+ gatewayPort,
183
+ ...(metadata ? { metadata } : {}),
184
+ });
185
+ if (synced.providerEnvToken) env.OPENCODEX_API_AUTH_TOKEN ??= synced.providerEnvToken;
186
+ return gateway;
187
+ } catch (error) {
188
+ await gateway?.stop().catch(() => undefined);
189
+ await removeAdapterRuntimeIfPid(process.pid, env).catch(() => undefined);
190
+ throw error;
191
+ }
192
+ }
193
+
194
+ export async function runEntry(argv: string[], deps: EntryDeps = {}): Promise<void> {
195
+ const env = process.env;
196
+ env.OCX_DISABLE_UPDATE_CHECK ??= "1";
197
+ if (isAdapterOwnedCommand(argv)) {
198
+ process.exitCode = await runAdapterCommand(argv, env, deps.adapterCli ?? runAdapterCli);
199
+ return;
200
+ }
201
+ if (argv[0] === "start") {
202
+ await (deps.prepareStart ?? prepareAdapterStart)(env);
203
+ }
204
+ const entry = process.argv[1] ?? "ocx";
205
+ const executable = process.argv[0] ?? "bun";
206
+ process.argv = [executable, entry, ...argv];
207
+ await (deps.loadUpstream ?? (() => loadUpstreamCli()))();
208
+ }
209
+
210
+ function printError(error: unknown): number {
211
+ const message = error instanceof Error ? error.message : String(error);
212
+ console.error("ocx: " + message);
213
+ return error instanceof ConfigError || error instanceof AdapterConfigError || error instanceof UpstreamPackageError ? 2 : 1;
214
+ }
215
+
216
+ if (import.meta.main) {
217
+ try {
218
+ await runEntry(process.argv.slice(2));
219
+ } catch (error) {
220
+ process.exitCode = printError(error);
221
+ }
222
+ }
package/src/cli/index.ts CHANGED
@@ -348,7 +348,7 @@ async function stopCommand(args: string[]): Promise<number> {
348
348
  return restored.success ? 0 : 1;
349
349
  }
350
350
 
351
- async function main(argv: string[]): Promise<number> {
351
+ export async function runAdapterCli(argv: string[]): Promise<number> {
352
352
  if (argv[0] === "__cli-agent-mcp") {
353
353
  await runCodingAgentMcpServer(argv[1] ?? "", argv[2]);
354
354
  return 0;
@@ -372,8 +372,10 @@ async function main(argv: string[]): Promise<number> {
372
372
  throw new ConfigError(`Unknown command: ${command}. Expected login, provider, codex, start, status, stop, restore, eject, version, or help.`);
373
373
  }
374
374
 
375
- try {
376
- process.exitCode = await main(process.argv.slice(2));
377
- } catch (error) {
378
- process.exitCode = printError(error);
375
+ if (import.meta.main) {
376
+ try {
377
+ process.exitCode = await runAdapterCli(process.argv.slice(2));
378
+ } catch (error) {
379
+ process.exitCode = printError(error);
380
+ }
379
381
  }
@@ -5,6 +5,7 @@ import {
5
5
  assertSafeListener,
6
6
  defaultConfig,
7
7
  normalizeConfig,
8
+ type ProviderId,
8
9
  type RuntimeConfig,
9
10
  } from "../config";
10
11
  import type { IncomingMeta } from "../types";
@@ -29,6 +30,7 @@ export interface ServerOptions {
29
30
  clineDeps?: ClineTurnDeps;
30
31
  geminiDeps?: GeminiTurnDeps;
31
32
  geminiPackageRoot?: string;
33
+ providerScope?: ProviderId;
32
34
  onWebSocketRequest?: (observation: WebSocketRequestObservation) => void;
33
35
  }
34
36
 
@@ -92,23 +94,32 @@ async function discoverModels(
92
94
  clineDeps: ClineTurnDeps | undefined,
93
95
  geminiDeps: GeminiTurnDeps | undefined,
94
96
  geminiPackageRoot: string | undefined,
97
+ providerScope?: ProviderId,
95
98
  ): Promise<ProviderModels> {
96
99
  const [cline, gemini] = await Promise.all([
97
- discoverClineLiveModels({ env, ...(clineDeps ? { deps: clineDeps } : {}) }),
98
- discoverGeminiLiveModels({ env, ...(geminiDeps ? { deps: geminiDeps } : {}), ...(geminiPackageRoot ? { packageRoot: geminiPackageRoot } : {}) }),
100
+ !providerScope || providerScope === "cline-cli"
101
+ ? discoverClineLiveModels({ env, ...(clineDeps ? { deps: clineDeps } : {}) })
102
+ : undefined,
103
+ !providerScope || providerScope === "gemini-cli"
104
+ ? discoverGeminiLiveModels({ env, ...(geminiDeps ? { deps: geminiDeps } : {}), ...(geminiPackageRoot ? { packageRoot: geminiPackageRoot } : {}) })
105
+ : undefined,
99
106
  ]);
100
- if (!cline.ok && cline.reason !== "not_found" && env.OPENCODEX_CLINE_ACP?.trim() !== "0") {
107
+ if (cline && !cline.ok && cline.reason !== "not_found" && env.OPENCODEX_CLINE_ACP?.trim() !== "0") {
101
108
  console.warn(`[opencodex] Cline model discovery degraded [${cline.reason}]: ${cline.detail}`);
102
109
  }
103
- if (!gemini.ok && gemini.reason !== "not_found" && env.OPENCODEX_GEMINI_ACP?.trim() !== "0") {
110
+ if (gemini && !gemini.ok && gemini.reason !== "not_found" && env.OPENCODEX_GEMINI_ACP?.trim() !== "0") {
104
111
  console.warn(`[opencodex] Gemini model discovery degraded [${gemini.reason}]: ${gemini.detail}`);
105
112
  }
106
- return {
107
- "cline-cli": cline.ok ? ["default", ...cline.models.filter(id => id !== "default")] : ["default"],
108
- "gemini-cli": gemini.ok
113
+ const models: ProviderModels = {};
114
+ if (cline) {
115
+ models["cline-cli"] = cline.ok ? ["default", ...cline.models.filter(id => id !== "default")] : ["default"];
116
+ }
117
+ if (gemini) {
118
+ models["gemini-cli"] = gemini.ok
109
119
  ? ["default", ...gemini.modelInfo.filter(model => model.id !== "default")]
110
- : ["default"],
111
- };
120
+ : ["default"];
121
+ }
122
+ return models;
112
123
  }
113
124
 
114
125
  export function startServer(config: RuntimeConfig | number, options: ServerOptions = {}): RunningServer {
@@ -138,8 +149,8 @@ export function startServer(config: RuntimeConfig | number, options: ServerOptio
138
149
  }
139
150
  try {
140
151
  if (req.method === "GET" && url.pathname === "/v1/models") {
141
- const known = await discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot);
142
- return Response.json({ object: "list", data: modelList(known) });
152
+ const known = await discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot, options.providerScope);
153
+ return Response.json({ object: "list", data: modelList(known, options.providerScope) });
143
154
  }
144
155
  if (req.method === "GET" && url.pathname === "/v1/model-catalog") {
145
156
  const discovered = await discoverCodexCatalogModels({
@@ -152,16 +163,16 @@ export function startServer(config: RuntimeConfig | number, options: ServerOptio
152
163
  }
153
164
  if (req.method === "POST" && url.pathname === "/v1/responses") {
154
165
  const body = await boundedJson(req);
155
- const known = await discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot);
166
+ const known = await discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot, options.providerScope);
156
167
  const signal = AbortSignal.any([req.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]);
157
168
  const incoming: IncomingMeta = { headers: req.headers, abortSignal: signal };
158
- return await handleResponses(body, incoming, known, turnDeps, config.defaultProvider);
169
+ return await handleResponses(body, incoming, known, turnDeps, options.providerScope ?? config.defaultProvider);
159
170
  }
160
171
  if (req.method === "POST" && url.pathname === "/v1/messages") {
161
172
  const body = await boundedJson(req);
162
- const known = await discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot);
173
+ const known = await discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot, options.providerScope);
163
174
  const signal = AbortSignal.any([req.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]);
164
- return await handleMessages(body, { headers: req.headers, abortSignal: signal }, known, turnDeps, config.defaultProvider);
175
+ return await handleMessages(body, { headers: req.headers, abortSignal: signal }, known, turnDeps, options.providerScope ?? config.defaultProvider);
165
176
  }
166
177
  if (req.method === "POST" && url.pathname === "/v1/messages/count_tokens") {
167
178
  return handleCountTokens(await boundedJson(req));
@@ -173,9 +184,9 @@ export function startServer(config: RuntimeConfig | number, options: ServerOptio
173
184
  };
174
185
 
175
186
  const websocketContext = {
176
- knownModels: () => discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot),
187
+ knownModels: () => discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot, options.providerScope),
177
188
  deps: turnDeps,
178
- defaultProvider: config.defaultProvider,
189
+ defaultProvider: options.providerScope ?? config.defaultProvider,
179
190
  timeoutMs: REQUEST_TIMEOUT_MS,
180
191
  ...(options.onWebSocketRequest ? { onRequest: options.onWebSocketRequest } : {}),
181
192
  };
@@ -379,7 +379,10 @@ const PROVIDER_LABELS: Record<ProviderId, string> = {
379
379
  [GEMINI_PROVIDER_ID]: "Gemini CLI",
380
380
  };
381
381
 
382
- export function modelList(known: string[] | ProviderModels): Array<Record<string, unknown>> {
382
+ export function modelList(
383
+ known: string[] | ProviderModels,
384
+ scope?: ProviderId,
385
+ ): Array<Record<string, unknown>> {
383
386
  if (Array.isArray(known)) {
384
387
  const ids = ["default", ...known.filter(id => id !== "default")];
385
388
  return [...new Set(ids)].map(id => ({
@@ -390,6 +393,44 @@ export function modelList(known: string[] | ProviderModels): Array<Record<string
390
393
  display_name: id === "default" ? "Cline default" : id,
391
394
  }));
392
395
  }
396
+ if (scope) {
397
+ const entries = ["default", ...(known[scope] ?? []).filter(entry => modelIdOf(entry) !== "default")];
398
+ const rows: Array<Record<string, unknown>> = [];
399
+ const seen = new Set<string>();
400
+ for (const entry of entries) {
401
+ const id = modelIdOf(entry);
402
+ if (seen.has(id)) continue;
403
+ seen.add(id);
404
+ const info = typeof entry === "string" ? { id: entry } as OcxProviderModel : entry;
405
+ const reasoning = info.supportedReasoningLevels ?? [];
406
+ rows.push({
407
+ id: encodeRoutedModelId(id),
408
+ object: "model",
409
+ created: 0,
410
+ owned_by: scope,
411
+ display_name: info.name ?? (id === "default" ? PROVIDER_LABELS[scope] + " default" : id),
412
+ ...(info.description ? { description: info.description } : {}),
413
+ ...(info.contextWindowTokens !== undefined
414
+ ? {
415
+ context_window: info.contextWindowTokens,
416
+ max_context_window: info.maxContextWindowTokens ?? info.contextWindowTokens,
417
+ }
418
+ : {}),
419
+ ...(info.inputModalities ? { input_modalities: [...info.inputModalities] } : {}),
420
+ ...(info.loadSession !== undefined ? { load_session: info.loadSession } : {}),
421
+ ...(reasoning.length > 0
422
+ ? {
423
+ supported_reasoning_levels: reasoning.map(level => ({
424
+ effort: level.effort,
425
+ description: level.description,
426
+ })),
427
+ default_reasoning_level: info.defaultReasoningLevel ?? reasoning[0]!.effort,
428
+ }
429
+ : { supported_reasoning_levels: [] }),
430
+ });
431
+ }
432
+ return rows;
433
+ }
393
434
  const rows: Array<Record<string, unknown>> = [{
394
435
  id: "default",
395
436
  object: "model",
@@ -0,0 +1,12 @@
1
+ import { pathToFileURL } from "node:url";
2
+ import { resolveUpstreamPackage, type UpstreamPackage } from "./package";
3
+
4
+ export type UpstreamModuleImporter = (url: string) => Promise<unknown>;
5
+
6
+ /** Load the immutable upstream CLI into this process so service ownership stays ours. */
7
+ export async function loadUpstreamCli(
8
+ pkg: UpstreamPackage = resolveUpstreamPackage(),
9
+ importer: UpstreamModuleImporter = url => import(url),
10
+ ): Promise<void> {
11
+ await importer(pathToFileURL(pkg.cliEntry).href);
12
+ }
@@ -0,0 +1,75 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+
5
+ export const UPSTREAM_PACKAGE_NAME = "@bitkyc08/opencodex";
6
+ export const UPSTREAM_PACKAGE_VERSION = "2.94.0";
7
+ export const UPSTREAM_CLI_RELATIVE_PATH = "src/cli/index.ts";
8
+
9
+ export interface UpstreamPackage {
10
+ name: string;
11
+ version: string;
12
+ root: string;
13
+ cliEntry: string;
14
+ }
15
+
16
+ export class UpstreamPackageError extends Error {
17
+ constructor(message: string) {
18
+ super(message);
19
+ this.name = "UpstreamPackageError";
20
+ }
21
+ }
22
+
23
+ export interface ResolveUpstreamPackageDeps {
24
+ resolvePackageJson?: () => string;
25
+ readJson?: (path: string) => unknown;
26
+ exists?: (path: string) => boolean;
27
+ }
28
+
29
+ /** Resolve the exact immutable upstream package without importing its runtime. */
30
+ export function resolveUpstreamPackage(
31
+ deps: ResolveUpstreamPackageDeps = {},
32
+ ): UpstreamPackage {
33
+ const resolvePackageJson = deps.resolvePackageJson
34
+ ?? (() => createRequire(import.meta.url).resolve(UPSTREAM_PACKAGE_NAME + "/package.json"));
35
+ const readJson = deps.readJson
36
+ ?? (path => JSON.parse(readFileSync(path, "utf8")) as unknown);
37
+ const exists = deps.exists ?? existsSync;
38
+
39
+ let packageJsonPath: string;
40
+ try {
41
+ packageJsonPath = resolvePackageJson();
42
+ } catch {
43
+ throw new UpstreamPackageError(
44
+ UPSTREAM_PACKAGE_NAME + " is not installed. Reinstall this package with its dependencies.",
45
+ );
46
+ }
47
+ let parsed: unknown;
48
+ try {
49
+ parsed = readJson(packageJsonPath);
50
+ } catch {
51
+ throw new UpstreamPackageError("The installed opencodex package metadata is unreadable.");
52
+ }
53
+ const packageJson = parsed && typeof parsed === "object" && !Array.isArray(parsed)
54
+ ? parsed as Record<string, unknown>
55
+ : undefined;
56
+ if (packageJson?.name !== UPSTREAM_PACKAGE_NAME || packageJson.version !== UPSTREAM_PACKAGE_VERSION) {
57
+ throw new UpstreamPackageError(
58
+ "Expected " + UPSTREAM_PACKAGE_NAME + "@" + UPSTREAM_PACKAGE_VERSION
59
+ + ", found " + String(packageJson?.name ?? "unknown") + "@" + String(packageJson?.version ?? "unknown") + ".",
60
+ );
61
+ }
62
+ const root = dirname(packageJsonPath);
63
+ const cliEntry = join(root, UPSTREAM_CLI_RELATIVE_PATH);
64
+ if (!exists(cliEntry)) {
65
+ throw new UpstreamPackageError(
66
+ UPSTREAM_PACKAGE_NAME + "@" + UPSTREAM_PACKAGE_VERSION + " does not contain " + UPSTREAM_CLI_RELATIVE_PATH + ".",
67
+ );
68
+ }
69
+ return {
70
+ name: UPSTREAM_PACKAGE_NAME,
71
+ version: UPSTREAM_PACKAGE_VERSION,
72
+ root,
73
+ cliEntry,
74
+ };
75
+ }