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/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 Error('ChatGPT 模型服务返回了无效数据');
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 Error('ChatGPT 当前没有返回可用模型');
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
- let timeoutReached = false;
117
- const onAbort = () => controller.abort(parent?.reason);
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
- class ResponseTooLargeError extends Error {
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
- const cancel = () => { void reader.cancel(signal.reason).catch(() => { }); };
145
- signal.addEventListener('abort', cancel, { once: true });
163
+ let complete = false;
146
164
  try {
147
165
  while (true) {
148
- if (signal.aborted)
149
- throw signal.reason;
150
- const chunk = await reader.read();
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 ResponseTooLargeError();
179
+ throw new SubscriptionError('invalid-response');
156
180
  text += decoder.decode(chunk.value, { stream: true });
157
181
  }
158
- if (signal.aborted)
159
- throw signal.reason;
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
- signal.removeEventListener('abort', cancel);
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 Error('ChatGPT 授权不可用,请重新登录');
203
+ throw new SubscriptionError('not-connected');
175
204
  const fetcher = options.fetch ?? globalThis.fetch;
176
205
  if (typeof fetcher !== 'function')
177
- throw new Error('当前运行环境无法连接 ChatGPT 模型服务');
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 TypeError('timeoutMs must be positive');
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
- response = await fetcher(url, { method: 'GET', headers, signal: control.signal, redirect: 'error' });
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
- if (options.signal?.aborted)
200
- throw new Error('模型同步已取消');
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.status === 401 || response.status === 403) {
206
- throw new Error('ChatGPT 授权已失效,请刷新授权后重试');
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 (Number.isFinite(contentLength) && contentLength > MAX_RESPONSE_BYTES) {
214
- throw new Error('ChatGPT 模型服务返回的数据过大');
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 Error('ChatGPT 模型服务返回了无效数据');
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;
@@ -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.2",
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 --test .test-dist/tests/*.test.js && node --eval \"require('node:fs').rmSync('.test-dist', { recursive: true, force: true })\"",
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
  },