dsh-openai-subscription 0.1.2 → 0.1.4
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/README.md +36 -12
- package/README.zh.md +36 -12
- package/dist/client.d.ts +8 -0
- package/dist/client.js +507 -170
- package/dist/driver.d.ts +12 -0
- package/dist/driver.js +136 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +58 -0
- package/dist/host.d.ts +32 -14
- package/dist/host.js +242 -312
- package/dist/models.js +91 -55
- package/dist/oauth.js +2 -2
- package/dist/platform.d.ts +17 -0
- package/dist/platform.js +279 -0
- package/package.json +2 -2
package/dist/models.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Dynamic ChatGPT Codex model discovery and catalog ownership helpers. */
|
|
2
|
+
import { SubscriptionError } from './errors.js';
|
|
2
3
|
const MODELS_ENDPOINT = 'https://chatgpt.com/backend-api/codex/models';
|
|
3
4
|
/**
|
|
4
5
|
* Codex releases replace their workspace's 0.0.0 version at build time, while
|
|
@@ -71,7 +72,7 @@ function normalizeInputs(value) {
|
|
|
71
72
|
export function parseOpenAIModelCatalog(value) {
|
|
72
73
|
const root = recordOf(value);
|
|
73
74
|
if (root === null || !Array.isArray(root.models))
|
|
74
|
-
throw new
|
|
75
|
+
throw new SubscriptionError('invalid-response');
|
|
75
76
|
const candidates = [];
|
|
76
77
|
const seenIds = new Set();
|
|
77
78
|
const adoptedIds = new Set();
|
|
@@ -108,77 +109,106 @@ export function parseOpenAIModelCatalog(value) {
|
|
|
108
109
|
candidates.sort((left, right) => left.priority - right.priority || left.index - right.index);
|
|
109
110
|
const models = candidates.map((candidate) => candidate.model);
|
|
110
111
|
if (models.length === 0)
|
|
111
|
-
throw new
|
|
112
|
+
throw new SubscriptionError('models-empty');
|
|
112
113
|
return { models, seenIds: [...seenIds] };
|
|
113
114
|
}
|
|
114
115
|
function abortableSignal(parent, timeoutMs) {
|
|
115
116
|
const controller = new AbortController();
|
|
116
|
-
|
|
117
|
-
const onAbort = () => controller.abort(
|
|
117
|
+
// Never propagate the caller's reason: it can contain tokens or remote text.
|
|
118
|
+
const onAbort = () => controller.abort(new SubscriptionError('cancelled'));
|
|
118
119
|
if (parent?.aborted)
|
|
119
120
|
onAbort();
|
|
120
121
|
else
|
|
121
122
|
parent?.addEventListener('abort', onAbort, { once: true });
|
|
122
|
-
const timeout = setTimeout(() =>
|
|
123
|
-
timeoutReached = true;
|
|
124
|
-
controller.abort(new Error('model discovery timed out'));
|
|
125
|
-
}, timeoutMs);
|
|
123
|
+
const timeout = setTimeout(() => controller.abort(new SubscriptionError('timeout')), timeoutMs);
|
|
126
124
|
return {
|
|
127
125
|
signal: controller.signal,
|
|
128
|
-
timedOut: () => timeoutReached,
|
|
129
126
|
dispose: () => {
|
|
130
127
|
clearTimeout(timeout);
|
|
131
128
|
parent?.removeEventListener('abort', onAbort);
|
|
132
129
|
},
|
|
133
130
|
};
|
|
134
131
|
}
|
|
135
|
-
|
|
132
|
+
/** Bound even implementations whose fetch/read promises ignore the signal. */
|
|
133
|
+
async function awaitWithAbort(promise, signal) {
|
|
134
|
+
let onAbort = () => { };
|
|
135
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
136
|
+
onAbort = () => reject(signal.reason);
|
|
137
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
138
|
+
if (signal.aborted)
|
|
139
|
+
onAbort();
|
|
140
|
+
});
|
|
141
|
+
try {
|
|
142
|
+
return await Promise.race([aborted, promise]);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
signal.removeEventListener('abort', onAbort);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function cancelBody(response) {
|
|
149
|
+
// Cancellation is best-effort, not another unbounded part of the deadline.
|
|
150
|
+
try {
|
|
151
|
+
void response.body?.cancel().catch(() => { });
|
|
152
|
+
}
|
|
153
|
+
catch { /* Already unavailable. */ }
|
|
136
154
|
}
|
|
137
155
|
async function readBoundedResponse(response, signal) {
|
|
156
|
+
signal.throwIfAborted();
|
|
138
157
|
if (response.body === null)
|
|
139
158
|
return '';
|
|
140
159
|
const reader = response.body.getReader();
|
|
141
160
|
const decoder = new TextDecoder();
|
|
142
161
|
let total = 0;
|
|
143
162
|
let text = '';
|
|
144
|
-
|
|
145
|
-
signal.addEventListener('abort', cancel, { once: true });
|
|
163
|
+
let complete = false;
|
|
146
164
|
try {
|
|
147
165
|
while (true) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
signal.throwIfAborted();
|
|
167
|
+
let chunk;
|
|
168
|
+
try {
|
|
169
|
+
chunk = await awaitWithAbort(reader.read(), signal);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
signal.throwIfAborted();
|
|
173
|
+
throw new SubscriptionError('network');
|
|
174
|
+
}
|
|
151
175
|
if (chunk.done)
|
|
152
176
|
break;
|
|
153
177
|
total += chunk.value.byteLength;
|
|
154
178
|
if (total > MAX_RESPONSE_BYTES)
|
|
155
|
-
throw new
|
|
179
|
+
throw new SubscriptionError('invalid-response');
|
|
156
180
|
text += decoder.decode(chunk.value, { stream: true });
|
|
157
181
|
}
|
|
158
|
-
|
|
159
|
-
|
|
182
|
+
signal.throwIfAborted();
|
|
183
|
+
complete = true;
|
|
160
184
|
return text + decoder.decode();
|
|
161
185
|
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
await reader.cancel().catch(() => { });
|
|
164
|
-
throw error;
|
|
165
|
-
}
|
|
166
186
|
finally {
|
|
167
|
-
|
|
187
|
+
// Do not await cancel(): an underlying source can stall or reject it. The
|
|
188
|
+
// pending read is handled by awaitWithAbort and its lock is always released.
|
|
189
|
+
if (!complete) {
|
|
190
|
+
try {
|
|
191
|
+
void reader.cancel().catch(() => { });
|
|
192
|
+
}
|
|
193
|
+
catch { /* Already unavailable. */ }
|
|
194
|
+
}
|
|
168
195
|
reader.releaseLock();
|
|
169
196
|
}
|
|
170
197
|
}
|
|
171
198
|
/** Fetch the live model catalog available to this exact ChatGPT account. */
|
|
172
199
|
export async function discoverOpenAIModels(credential, options = {}) {
|
|
200
|
+
if (options.signal?.aborted)
|
|
201
|
+
throw new SubscriptionError('cancelled');
|
|
173
202
|
if (nonEmptyString(credential.access) === undefined)
|
|
174
|
-
throw new
|
|
203
|
+
throw new SubscriptionError('not-connected');
|
|
175
204
|
const fetcher = options.fetch ?? globalThis.fetch;
|
|
176
205
|
if (typeof fetcher !== 'function')
|
|
177
|
-
throw new
|
|
206
|
+
throw new SubscriptionError('network');
|
|
178
207
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
179
208
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
180
|
-
throw new
|
|
209
|
+
throw new SubscriptionError('invalid-response');
|
|
181
210
|
const control = abortableSignal(options.signal, timeoutMs);
|
|
211
|
+
let response;
|
|
182
212
|
try {
|
|
183
213
|
const url = new URL(MODELS_ENDPOINT);
|
|
184
214
|
url.searchParams.set('client_version', CLIENT_VERSION);
|
|
@@ -191,51 +221,57 @@ export async function discoverOpenAIModels(credential, options = {}) {
|
|
|
191
221
|
const accountId = nonEmptyString(credential.accountId);
|
|
192
222
|
if (accountId !== undefined)
|
|
193
223
|
headers.set('chatgpt-account-id', accountId);
|
|
194
|
-
let response;
|
|
195
224
|
try {
|
|
196
|
-
|
|
225
|
+
control.signal.throwIfAborted();
|
|
226
|
+
const pending = fetcher(url, { method: 'GET', headers, signal: control.signal, redirect: 'error' })
|
|
227
|
+
.then((result) => {
|
|
228
|
+
// A non-cooperative fetch may finish after discovery already stopped.
|
|
229
|
+
if (control.signal.aborted) {
|
|
230
|
+
cancelBody(result);
|
|
231
|
+
control.signal.throwIfAborted();
|
|
232
|
+
}
|
|
233
|
+
response = result;
|
|
234
|
+
return result;
|
|
235
|
+
});
|
|
236
|
+
response = await awaitWithAbort(pending, control.signal);
|
|
197
237
|
}
|
|
198
238
|
catch {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (control.timedOut())
|
|
202
|
-
throw new Error('获取 ChatGPT 模型超时,请稍后重试');
|
|
203
|
-
throw new Error('无法连接 ChatGPT 模型服务,请检查网络后重试');
|
|
239
|
+
control.signal.throwIfAborted();
|
|
240
|
+
throw new SubscriptionError('network');
|
|
204
241
|
}
|
|
205
|
-
if (response.
|
|
206
|
-
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
if (response.status === 401)
|
|
244
|
+
throw new SubscriptionError('authorization-expired');
|
|
245
|
+
if (response.status === 403)
|
|
246
|
+
throw new SubscriptionError('access-denied');
|
|
247
|
+
if (response.status === 429)
|
|
248
|
+
throw new SubscriptionError('rate-limited');
|
|
249
|
+
throw new SubscriptionError('models-unavailable');
|
|
207
250
|
}
|
|
208
|
-
if (response.status === 429)
|
|
209
|
-
throw new Error('模型同步请求过于频繁,请稍后重试');
|
|
210
|
-
if (!response.ok)
|
|
211
|
-
throw new Error(`ChatGPT 模型服务暂时不可用(HTTP ${response.status})`);
|
|
212
251
|
const contentLength = Number(response.headers.get('content-length'));
|
|
213
|
-
if (
|
|
214
|
-
throw new
|
|
215
|
-
}
|
|
216
|
-
let text;
|
|
217
|
-
try {
|
|
218
|
-
text = await readBoundedResponse(response, control.signal);
|
|
219
|
-
}
|
|
220
|
-
catch (error) {
|
|
221
|
-
if (error instanceof ResponseTooLargeError)
|
|
222
|
-
throw new Error('ChatGPT 模型服务返回的数据过大');
|
|
223
|
-
if (options.signal?.aborted)
|
|
224
|
-
throw new Error('模型同步已取消');
|
|
225
|
-
if (control.timedOut())
|
|
226
|
-
throw new Error('获取 ChatGPT 模型超时,请稍后重试');
|
|
227
|
-
throw new Error('读取 ChatGPT 模型数据失败,请稍后重试');
|
|
252
|
+
if (contentLength > MAX_RESPONSE_BYTES) {
|
|
253
|
+
throw new SubscriptionError('invalid-response');
|
|
228
254
|
}
|
|
255
|
+
const text = await readBoundedResponse(response, control.signal);
|
|
229
256
|
let body;
|
|
230
257
|
try {
|
|
231
258
|
body = JSON.parse(text);
|
|
232
259
|
}
|
|
233
260
|
catch {
|
|
234
|
-
throw new
|
|
261
|
+
throw new SubscriptionError('invalid-response');
|
|
235
262
|
}
|
|
236
263
|
return parseOpenAIModelCatalog(body);
|
|
237
264
|
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
control.signal.throwIfAborted();
|
|
267
|
+
if (error instanceof SubscriptionError)
|
|
268
|
+
throw error;
|
|
269
|
+
throw new SubscriptionError('network');
|
|
270
|
+
}
|
|
238
271
|
finally {
|
|
272
|
+
// Includes non-OK responses, oversized headers, and failures before reading.
|
|
273
|
+
if (response !== undefined && !response.bodyUsed)
|
|
274
|
+
cancelBody(response);
|
|
239
275
|
control.dispose();
|
|
240
276
|
}
|
|
241
277
|
}
|
package/dist/oauth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
function nonEmptyString(value) {
|
|
2
|
-
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
2
|
+
return typeof value === 'string' && value.trim().length > 0 && !/[\r\n\0]/.test(value) ? value : undefined;
|
|
3
3
|
}
|
|
4
4
|
function positiveFiniteNumber(value) {
|
|
5
5
|
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
@@ -9,7 +9,7 @@ function positiveFiniteNumber(value) {
|
|
|
9
9
|
* previously normalized credential.
|
|
10
10
|
*/
|
|
11
11
|
export function normalizeOAuthCredential(value, fallback = {}) {
|
|
12
|
-
if (typeof value !== 'object' || value === null)
|
|
12
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
13
13
|
return null;
|
|
14
14
|
const raw = value;
|
|
15
15
|
const access = nonEmptyString(raw.access);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered filesystem fallbacks: running DSH first, then Node/npm/nvm,
|
|
3
|
+
* NODE_PATH and user pnpm/bun roots. The fourth argument permits portable
|
|
4
|
+
* win32 fixtures; omitted, it uses the current OS and preserves the old API.
|
|
5
|
+
* This list is not a trust decision and does not import any candidate.
|
|
6
|
+
*/
|
|
7
|
+
export declare function authModuleCandidates(entryScript: string | undefined, nodeBinary: string | undefined, env: NodeJS.ProcessEnv | undefined, platform?: NodeJS.Platform): string[];
|
|
8
|
+
/** Find a real file, without loading OAuth code or launching external commands. */
|
|
9
|
+
export declare function resolveAuthModule(entryScript?: string | undefined, nodeBinary?: string | undefined, env?: NodeJS.ProcessEnv | undefined): string;
|
|
10
|
+
/** pi-ai's supported Node floor; reject malformed version reports conservatively. */
|
|
11
|
+
export declare function runtimeSupported(version?: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Build only a command; callers MUST keep execution through ctx.shell.
|
|
14
|
+
* Default DSH providers use POSIX Bash on Unix and PowerShell on Windows.
|
|
15
|
+
* Custom providers using a different dialect need an explicit platform override.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildNodeCommand(script: string, modulePath: string, platform?: NodeJS.Platform, nodeBinary?: string): string;
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join, posix, win32 } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
const AUTH_PACKAGE = '@earendil-works/pi-ai';
|
|
6
|
+
const AUTH_RELATIVE = 'dist/auth/oauth/openai-codex.js';
|
|
7
|
+
const ADAPTER_PACKAGE = '@deepseek-ai/dsh-llm-pi-ai';
|
|
8
|
+
const DSH_PACKAGE = '@deepseek-ai/dsh';
|
|
9
|
+
const PI_CLI_PACKAGE = '@earendil-works/pi-coding-agent';
|
|
10
|
+
function paths(platform) {
|
|
11
|
+
return platform === 'win32' ? win32 : posix;
|
|
12
|
+
}
|
|
13
|
+
/** Canonical targets precede shims/symlinks, without invoking readlink or a shell. */
|
|
14
|
+
function entryPaths(value, platform) {
|
|
15
|
+
if (!value)
|
|
16
|
+
return [];
|
|
17
|
+
const normalized = paths(platform).resolve(value);
|
|
18
|
+
if (platform === process.platform) {
|
|
19
|
+
try {
|
|
20
|
+
const actual = realpathSync(normalized);
|
|
21
|
+
if (actual !== normalized)
|
|
22
|
+
return [actual, normalized];
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Missing/inaccessible paths still provide useful lexical fallback roots.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return [normalized];
|
|
29
|
+
}
|
|
30
|
+
function moduleRoots(entry, platform) {
|
|
31
|
+
const path = paths(platform);
|
|
32
|
+
const roots = [];
|
|
33
|
+
let directory = path.dirname(entry);
|
|
34
|
+
for (let depth = 0; depth < 40; depth++) {
|
|
35
|
+
if (path.basename(directory) !== 'node_modules')
|
|
36
|
+
roots.push(path.join(directory, 'node_modules'));
|
|
37
|
+
const parent = path.dirname(directory);
|
|
38
|
+
if (parent === directory)
|
|
39
|
+
break;
|
|
40
|
+
directory = parent;
|
|
41
|
+
}
|
|
42
|
+
return roots;
|
|
43
|
+
}
|
|
44
|
+
function globalRoots(nodeBinary, env, platform) {
|
|
45
|
+
const path = paths(platform);
|
|
46
|
+
const roots = [];
|
|
47
|
+
const addPrefix = (prefix) => {
|
|
48
|
+
if (!prefix)
|
|
49
|
+
return;
|
|
50
|
+
roots.push(path.join(prefix, 'node_modules'), path.join(prefix, 'lib/node_modules'));
|
|
51
|
+
};
|
|
52
|
+
const addPnpm = (home) => {
|
|
53
|
+
if (!home)
|
|
54
|
+
return;
|
|
55
|
+
roots.push(path.join(home, 'global/5/node_modules'), path.join(home, 'global/node_modules'));
|
|
56
|
+
};
|
|
57
|
+
for (const binary of entryPaths(nodeBinary, platform)) {
|
|
58
|
+
const directory = path.dirname(binary);
|
|
59
|
+
roots.push(path.resolve(directory, '../lib/node_modules'), path.join(directory, 'node_modules'));
|
|
60
|
+
}
|
|
61
|
+
for (const root of env?.NODE_PATH?.split(path.delimiter) ?? []) {
|
|
62
|
+
if (root)
|
|
63
|
+
roots.push(root, path.join(root, 'node_modules'));
|
|
64
|
+
}
|
|
65
|
+
addPrefix(env?.npm_config_prefix ?? env?.NPM_CONFIG_PREFIX ?? env?.PREFIX);
|
|
66
|
+
if (env?.NVM_BIN)
|
|
67
|
+
roots.push(path.resolve(env.NVM_BIN, '../lib/node_modules'));
|
|
68
|
+
addPrefix(env?.NVM_SYMLINK);
|
|
69
|
+
addPnpm(env?.PNPM_HOME);
|
|
70
|
+
if (env?.BUN_INSTALL)
|
|
71
|
+
roots.push(path.join(env.BUN_INSTALL, 'install/global/node_modules'));
|
|
72
|
+
if (platform === 'win32') {
|
|
73
|
+
if (env?.APPDATA)
|
|
74
|
+
roots.push(path.join(env.APPDATA, 'npm/node_modules'));
|
|
75
|
+
if (env?.LOCALAPPDATA)
|
|
76
|
+
addPnpm(path.join(env.LOCALAPPDATA, 'pnpm'));
|
|
77
|
+
}
|
|
78
|
+
if (env?.XDG_DATA_HOME)
|
|
79
|
+
addPnpm(path.join(env.XDG_DATA_HOME, 'pnpm'));
|
|
80
|
+
const home = platform === 'win32'
|
|
81
|
+
? env?.USERPROFILE || (env?.HOMEDRIVE && env.HOMEPATH ? env.HOMEDRIVE + env.HOMEPATH : env?.HOME)
|
|
82
|
+
: env?.HOME;
|
|
83
|
+
if (home) {
|
|
84
|
+
roots.push(path.join(home, '.bun/install/global/node_modules'));
|
|
85
|
+
addPrefix(path.join(home, '.npm-global'));
|
|
86
|
+
addPrefix(path.join(home, '.local'));
|
|
87
|
+
if (platform === 'win32') {
|
|
88
|
+
roots.push(path.join(home, 'AppData/Roaming/npm/node_modules'));
|
|
89
|
+
addPnpm(path.join(home, 'AppData/Local/pnpm'));
|
|
90
|
+
}
|
|
91
|
+
else if (platform === 'darwin') {
|
|
92
|
+
addPnpm(path.join(home, 'Library/pnpm'));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
addPnpm(path.join(home, '.local/share/pnpm'));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return roots;
|
|
99
|
+
}
|
|
100
|
+
function pathDirectories(env, platform) {
|
|
101
|
+
return ((platform === 'win32' ? env?.PATH ?? env?.Path : env?.PATH) ?? '')
|
|
102
|
+
.split(paths(platform).delimiter).filter(Boolean);
|
|
103
|
+
}
|
|
104
|
+
function pathEntries(env, platform) {
|
|
105
|
+
if (platform !== process.platform)
|
|
106
|
+
return [];
|
|
107
|
+
const entries = [];
|
|
108
|
+
for (const name of ['dsh', 'pi', 'pi-ai']) {
|
|
109
|
+
for (const directory of pathDirectories(env, platform)) {
|
|
110
|
+
for (const suffix of platform === 'win32' ? ['.cmd', '.exe', '.js', ''] : ['']) {
|
|
111
|
+
const entry = file(join(directory, name + suffix));
|
|
112
|
+
if (entry)
|
|
113
|
+
entries.push(entry);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return entries;
|
|
118
|
+
}
|
|
119
|
+
function pathRoots(env, platform) {
|
|
120
|
+
const path = paths(platform);
|
|
121
|
+
return pathDirectories(env, platform).flatMap((directory) => [
|
|
122
|
+
path.join(directory, 'node_modules'), path.resolve(directory, '../lib/node_modules'),
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
function lookupRoots(entryScript, nodeBinary, env, platform) {
|
|
126
|
+
return [
|
|
127
|
+
...entryPaths(entryScript, platform).flatMap((entry) => moduleRoots(entry, platform)),
|
|
128
|
+
...globalRoots(nodeBinary, env, platform),
|
|
129
|
+
...pathEntries(env, platform).flatMap((entry) => moduleRoots(entry, platform)),
|
|
130
|
+
...pathRoots(env, platform),
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Ordered filesystem fallbacks: running DSH first, then Node/npm/nvm,
|
|
135
|
+
* NODE_PATH and user pnpm/bun roots. The fourth argument permits portable
|
|
136
|
+
* win32 fixtures; omitted, it uses the current OS and preserves the old API.
|
|
137
|
+
* This list is not a trust decision and does not import any candidate.
|
|
138
|
+
*/
|
|
139
|
+
export function authModuleCandidates(entryScript, nodeBinary, env, platform = process.platform) {
|
|
140
|
+
const path = paths(platform);
|
|
141
|
+
const candidates = [];
|
|
142
|
+
const seen = new Set();
|
|
143
|
+
for (const root of lookupRoots(entryScript, nodeBinary, env, platform)) {
|
|
144
|
+
for (const base of [
|
|
145
|
+
path.join(root, DSH_PACKAGE, 'node_modules', ADAPTER_PACKAGE, 'node_modules'),
|
|
146
|
+
path.join(root, DSH_PACKAGE, 'node_modules'),
|
|
147
|
+
path.join(root, ADAPTER_PACKAGE, 'node_modules'),
|
|
148
|
+
root,
|
|
149
|
+
path.join(root, PI_CLI_PACKAGE, 'node_modules'),
|
|
150
|
+
]) {
|
|
151
|
+
const candidate = path.resolve(base, AUTH_PACKAGE, AUTH_RELATIVE);
|
|
152
|
+
const key = platform === 'win32' ? candidate.toLowerCase() : candidate;
|
|
153
|
+
if (seen.has(key))
|
|
154
|
+
continue;
|
|
155
|
+
seen.add(key);
|
|
156
|
+
candidates.push(candidate);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return candidates;
|
|
160
|
+
}
|
|
161
|
+
function file(value) {
|
|
162
|
+
try {
|
|
163
|
+
return statSync(value).isFile() ? realpathSync(value) : '';
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return '';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function packageEntry(directory) {
|
|
170
|
+
try {
|
|
171
|
+
return statSync(directory).isDirectory() ? join(realpathSync(directory), 'package.json') : '';
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return '';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Only local ancestry, not createRequire's ambient NODE_PATH/globalPaths. */
|
|
178
|
+
function dependencyRoots(entry, name) {
|
|
179
|
+
const allowed = new Set(moduleRoots(entry, process.platform));
|
|
180
|
+
try {
|
|
181
|
+
return (createRequire(pathToFileURL(entry)).resolve.paths(name) ?? []).filter((root) => allowed.has(root));
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return [...allowed];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function probePiAi(entry) {
|
|
188
|
+
// Walk require's roots rather than requiring/importing the module. pi-ai's
|
|
189
|
+
// exports are import-only and deliberately hide package.json and this file.
|
|
190
|
+
for (const root of dependencyRoots(entry, AUTH_PACKAGE)) {
|
|
191
|
+
const candidate = file(join(root, AUTH_PACKAGE, AUTH_RELATIVE));
|
|
192
|
+
if (candidate)
|
|
193
|
+
return candidate;
|
|
194
|
+
}
|
|
195
|
+
return '';
|
|
196
|
+
}
|
|
197
|
+
function probeInstallation(entry) {
|
|
198
|
+
// Canonicalizing the adapter follows pnpm's symlink into its virtual store,
|
|
199
|
+
// where its own pi-ai dependency can differ from a top-level hoisted copy.
|
|
200
|
+
for (const root of dependencyRoots(entry, ADAPTER_PACKAGE)) {
|
|
201
|
+
const adapter = packageEntry(join(root, ADAPTER_PACKAGE));
|
|
202
|
+
if (adapter) {
|
|
203
|
+
const candidate = probePiAi(adapter);
|
|
204
|
+
if (candidate)
|
|
205
|
+
return candidate;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return probePiAi(entry);
|
|
209
|
+
}
|
|
210
|
+
function probeRoot(root) {
|
|
211
|
+
const dsh = packageEntry(join(root, DSH_PACKAGE));
|
|
212
|
+
if (dsh) {
|
|
213
|
+
const candidate = probeInstallation(dsh);
|
|
214
|
+
if (candidate)
|
|
215
|
+
return candidate;
|
|
216
|
+
}
|
|
217
|
+
const adapter = packageEntry(join(root, ADAPTER_PACKAGE));
|
|
218
|
+
const candidate = (adapter && probePiAi(adapter)) || file(join(root, AUTH_PACKAGE, AUTH_RELATIVE));
|
|
219
|
+
if (candidate)
|
|
220
|
+
return candidate;
|
|
221
|
+
const pi = packageEntry(join(root, PI_CLI_PACKAGE));
|
|
222
|
+
return pi ? probePiAi(pi) : '';
|
|
223
|
+
}
|
|
224
|
+
/** Find a real file, without loading OAuth code or launching external commands. */
|
|
225
|
+
export function resolveAuthModule(entryScript = process.argv[1], nodeBinary = process.execPath, env = process.env) {
|
|
226
|
+
for (const entry of entryPaths(entryScript, process.platform)) {
|
|
227
|
+
const candidate = probeInstallation(entry);
|
|
228
|
+
if (candidate)
|
|
229
|
+
return candidate;
|
|
230
|
+
}
|
|
231
|
+
for (const root of globalRoots(nodeBinary, env, process.platform)) {
|
|
232
|
+
const candidate = probeRoot(root);
|
|
233
|
+
if (candidate)
|
|
234
|
+
return candidate;
|
|
235
|
+
}
|
|
236
|
+
// Last-resort replacement for `command -v pi`: inspect PATH entries with
|
|
237
|
+
// Node fs only. Canonical targets cover Unix symlinks; adjacent package
|
|
238
|
+
// roots cover npm's Windows .cmd shims without parsing or executing them.
|
|
239
|
+
for (const entry of pathEntries(env, process.platform)) {
|
|
240
|
+
const candidate = probeInstallation(entry);
|
|
241
|
+
if (candidate)
|
|
242
|
+
return candidate;
|
|
243
|
+
}
|
|
244
|
+
for (const root of pathRoots(env, process.platform)) {
|
|
245
|
+
const candidate = probeRoot(root);
|
|
246
|
+
if (candidate)
|
|
247
|
+
return candidate;
|
|
248
|
+
}
|
|
249
|
+
return '';
|
|
250
|
+
}
|
|
251
|
+
/** pi-ai's supported Node floor; reject malformed version reports conservatively. */
|
|
252
|
+
export function runtimeSupported(version = process.versions.node) {
|
|
253
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
|
|
254
|
+
if (!match)
|
|
255
|
+
return false;
|
|
256
|
+
const major = Number(match[1]);
|
|
257
|
+
return major > 22 || (major === 22 && Number(match[2]) >= 19);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Build only a command; callers MUST keep execution through ctx.shell.
|
|
261
|
+
* Default DSH providers use POSIX Bash on Unix and PowerShell on Windows.
|
|
262
|
+
* Custom providers using a different dialect need an explicit platform override.
|
|
263
|
+
*/
|
|
264
|
+
export function buildNodeCommand(script, modulePath, platform = process.platform, nodeBinary = process.execPath) {
|
|
265
|
+
if ([script, modulePath, nodeBinary].some((value) => value.includes('\0'))) {
|
|
266
|
+
throw new TypeError('Node command arguments must not contain NUL');
|
|
267
|
+
}
|
|
268
|
+
if (platform === 'win32') {
|
|
269
|
+
const quote = (value) => "'" + value.replace(/'/g, "''") + "'";
|
|
270
|
+
// No embedded double quotes in the native eval argument: this also works
|
|
271
|
+
// with Windows PowerShell's legacy native argument marshalling. Data URLs
|
|
272
|
+
// retain ESM syntax without temporary files; drivers use node: imports and
|
|
273
|
+
// the absolute modulePath argument, not relative imports from this module.
|
|
274
|
+
const bootstrap = "await import('data:text/javascript;base64," + Buffer.from(script).toString('base64') + "')";
|
|
275
|
+
return '& ' + quote(nodeBinary) + ' --input-type=module --eval ' + quote(bootstrap) + ' -- ' + quote(modulePath);
|
|
276
|
+
}
|
|
277
|
+
const quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
|
|
278
|
+
return quote(nodeBinary) + ' --input-type=module --eval ' + quote(script) + ' -- ' + quote(modulePath);
|
|
279
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-openai-subscription",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "ChatGPT subscription sign-in for DeepSeek Harness via OpenAI device authorization.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.6.3",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"clean": "node --eval \"for (const path of ['dist', '.test-dist']) require('node:fs').rmSync(path, { recursive: true, force: true })\"",
|
|
28
28
|
"build": "node --eval \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
29
29
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
30
|
-
"test": "node --eval \"require('node:fs').rmSync('.test-dist', { recursive: true, force: true })\" && tsc -p tsconfig.test.json && node
|
|
30
|
+
"test": "node --eval \"require('node:fs').rmSync('.test-dist', { recursive: true, force: true })\" && tsc -p tsconfig.test.json && node scripts/run-tests.mjs && node --eval \"require('node:fs').rmSync('.test-dist', { recursive: true, force: true })\"",
|
|
31
31
|
"check": "node scripts/run-scripts.mjs typecheck build test",
|
|
32
32
|
"prepack": "node scripts/run-scripts.mjs check"
|
|
33
33
|
},
|