@aipoch/open-science 0.0.0-stage → 1.0.2
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/CLI.md +814 -0
- package/LICENSE +201 -0
- package/README.md +284 -2
- package/appimage-launcher.mjs +90 -0
- package/browser-import.d.ts +66 -0
- package/browser-import.mjs +389 -0
- package/cli.mjs +1845 -0
- package/codex-login.mjs +298 -0
- package/config-root.mjs +65 -0
- package/index.d.ts +677 -0
- package/index.mjs +761 -0
- package/installation-startup.cjs +205 -0
- package/locate-app.mjs +95 -0
- package/locate-backend.mjs +63 -0
- package/package.json +45 -4
- package/runtime-control.mjs +51 -0
- package/runtime-package.mjs +19 -0
package/codex-login.mjs
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { access, mkdir, readFile } from 'node:fs/promises'
|
|
5
|
+
import { isAbsolute, join, normalize } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import { resolveConfigRoot } from './config-root.mjs'
|
|
8
|
+
import { locateBackend } from './locate-backend.mjs'
|
|
9
|
+
import { connectToOpenScience } from './index.mjs'
|
|
10
|
+
|
|
11
|
+
const CODEX_CONFIG_OVERRIDE = 'cli_auth_credentials_store="file"'
|
|
12
|
+
const CODEX_ENV_KEYS = [
|
|
13
|
+
'CODEX_API_KEY',
|
|
14
|
+
'OPENAI_API_KEY',
|
|
15
|
+
'CODEX_CONFIG',
|
|
16
|
+
'CODEX_HOME',
|
|
17
|
+
'CODEX_PATH',
|
|
18
|
+
'DEFAULT_AUTH_REQUEST',
|
|
19
|
+
'HOME',
|
|
20
|
+
'MODEL_PROVIDER',
|
|
21
|
+
'NO_BROWSER',
|
|
22
|
+
'USERPROFILE'
|
|
23
|
+
]
|
|
24
|
+
const PROXY_ENV_KEYS = [
|
|
25
|
+
'HTTP_PROXY',
|
|
26
|
+
'HTTPS_PROXY',
|
|
27
|
+
'http_proxy',
|
|
28
|
+
'https_proxy',
|
|
29
|
+
'ALL_PROXY',
|
|
30
|
+
'all_proxy',
|
|
31
|
+
'NO_PROXY',
|
|
32
|
+
'no_proxy'
|
|
33
|
+
]
|
|
34
|
+
const PROXY_SERVER_ENV_KEYS = PROXY_ENV_KEYS.slice(0, 6)
|
|
35
|
+
const LOOPBACK_PROXY_BYPASS = [
|
|
36
|
+
'localhost',
|
|
37
|
+
'.localhost',
|
|
38
|
+
'127.0.0.1',
|
|
39
|
+
'127.0.0.0/8',
|
|
40
|
+
'::1',
|
|
41
|
+
'[::1]'
|
|
42
|
+
]
|
|
43
|
+
const SUPPORTED_PROXY_PROTOCOLS = new Set(['http:', 'https:', 'socks:', 'socks4:', 'socks5:'])
|
|
44
|
+
|
|
45
|
+
export class CodexLoginError extends Error {
|
|
46
|
+
constructor(message, code = 'codex_login_failed', exitCode = 1) {
|
|
47
|
+
super(message)
|
|
48
|
+
this.name = 'CodexLoginError'
|
|
49
|
+
this.code = code
|
|
50
|
+
this.exitCode = exitCode
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const normalizedProxySettings = (value) => {
|
|
55
|
+
if (!value || typeof value !== 'object') return { mode: 'system' }
|
|
56
|
+
if (value.mode === 'direct') return { mode: 'direct' }
|
|
57
|
+
if (value.mode !== 'manual' || typeof value.server !== 'string') return { mode: 'system' }
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const url = new URL(value.server.trim())
|
|
61
|
+
if (
|
|
62
|
+
!SUPPORTED_PROXY_PROTOCOLS.has(url.protocol) ||
|
|
63
|
+
!url.hostname ||
|
|
64
|
+
url.username ||
|
|
65
|
+
url.password ||
|
|
66
|
+
(url.pathname !== '' && url.pathname !== '/') ||
|
|
67
|
+
url.search ||
|
|
68
|
+
url.hash
|
|
69
|
+
) {
|
|
70
|
+
return { mode: 'system' }
|
|
71
|
+
}
|
|
72
|
+
const bypassRules =
|
|
73
|
+
typeof value.bypassRules === 'string'
|
|
74
|
+
? value.bypassRules
|
|
75
|
+
.split(/[,;\n]/)
|
|
76
|
+
.map((entry) => entry.trim())
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
: []
|
|
79
|
+
return {
|
|
80
|
+
mode: 'manual',
|
|
81
|
+
server: `${url.protocol}//${url.host}`,
|
|
82
|
+
bypassRules
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
return { mode: 'system' }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const withLoopbackProxyBypass = (env, configuredRules = []) => {
|
|
90
|
+
const inheritedRules = [env.NO_PROXY, env.no_proxy]
|
|
91
|
+
.flatMap((value) => value?.split(',') ?? [])
|
|
92
|
+
.map((entry) => entry.trim())
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
const bypass = [
|
|
95
|
+
...new Set([...configuredRules, ...inheritedRules, ...LOOPBACK_PROXY_BYPASS])
|
|
96
|
+
].join(',')
|
|
97
|
+
env.NO_PROXY = bypass
|
|
98
|
+
env.no_proxy = bypass
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const applyCodexLoginProxyPolicy = (env, value) => {
|
|
102
|
+
const settings = normalizedProxySettings(value)
|
|
103
|
+
if (settings.mode === 'system') {
|
|
104
|
+
if (PROXY_SERVER_ENV_KEYS.some((key) => Boolean(env[key]))) withLoopbackProxyBypass(env)
|
|
105
|
+
return env
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const key of PROXY_ENV_KEYS) delete env[key]
|
|
109
|
+
if (settings.mode === 'direct') return env
|
|
110
|
+
|
|
111
|
+
for (const key of PROXY_SERVER_ENV_KEYS) env[key] = settings.server
|
|
112
|
+
withLoopbackProxyBypass(env, settings.bypassRules)
|
|
113
|
+
return env
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const createCodexLoginEnvironment = (
|
|
117
|
+
codexHome,
|
|
118
|
+
sourceEnv = process.env,
|
|
119
|
+
platform = process.platform,
|
|
120
|
+
networkProxy
|
|
121
|
+
) => {
|
|
122
|
+
const env = { ...sourceEnv }
|
|
123
|
+
for (const key of CODEX_ENV_KEYS) delete env[key]
|
|
124
|
+
env.CODEX_HOME = codexHome
|
|
125
|
+
env.HOME = codexHome
|
|
126
|
+
if (platform === 'win32') env.USERPROFILE = codexHome
|
|
127
|
+
return applyCodexLoginProxyPolicy(env, networkProxy)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const resolveCodexLoginConfiguration = async (configRoot, dependencies = {}) => {
|
|
131
|
+
const deps = {
|
|
132
|
+
readFile: (path) => readFile(path, 'utf8'),
|
|
133
|
+
access: (path) => access(path),
|
|
134
|
+
...dependencies
|
|
135
|
+
}
|
|
136
|
+
let settings
|
|
137
|
+
try {
|
|
138
|
+
settings = JSON.parse(await deps.readFile(join(configRoot, 'settings.json')))
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error?.code === 'ENOENT') {
|
|
141
|
+
throw new CodexLoginError(
|
|
142
|
+
'Codex is not configured for this Open-Science profile. Configure the Codex runtime first.',
|
|
143
|
+
'codex_not_configured'
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
throw new CodexLoginError(
|
|
147
|
+
'Open-Science could not read the configured Codex runtime.',
|
|
148
|
+
'codex_configuration_invalid'
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const nativePath =
|
|
153
|
+
typeof settings?.codex?.nativePath === 'string' ? settings.codex.nativePath.trim() : ''
|
|
154
|
+
if (!nativePath || !isAbsolute(nativePath)) {
|
|
155
|
+
throw new CodexLoginError(
|
|
156
|
+
'The configured Codex runtime has no native CLI path. Repair the Codex installation first.',
|
|
157
|
+
'codex_not_configured'
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const resolvedPath = normalize(nativePath)
|
|
162
|
+
try {
|
|
163
|
+
await deps.access(resolvedPath)
|
|
164
|
+
} catch {
|
|
165
|
+
throw new CodexLoginError(
|
|
166
|
+
'The configured Codex CLI does not exist: ' + resolvedPath,
|
|
167
|
+
'codex_not_found'
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
return { codexPath: resolvedPath, networkProxy: settings.networkProxy }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const resolveConfiguredCodexNativePath = async (configRoot, dependencies = {}) =>
|
|
174
|
+
(await resolveCodexLoginConfiguration(configRoot, dependencies)).codexPath
|
|
175
|
+
|
|
176
|
+
export const runCodexProcess = (codexPath, args, options = {}) =>
|
|
177
|
+
new Promise((resolveRun, rejectRun) => {
|
|
178
|
+
const needsShell = process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(codexPath)
|
|
179
|
+
const child = spawn(needsShell ? '"' + codexPath + '"' : codexPath, args, {
|
|
180
|
+
env: options.env,
|
|
181
|
+
shell: needsShell,
|
|
182
|
+
stdio: options.inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
|
183
|
+
windowsHide: true
|
|
184
|
+
})
|
|
185
|
+
let stdout = ''
|
|
186
|
+
let stderr = ''
|
|
187
|
+
if (!options.inherit) {
|
|
188
|
+
child.stdout?.setEncoding('utf8')
|
|
189
|
+
child.stderr?.setEncoding('utf8')
|
|
190
|
+
child.stdout?.on('data', (chunk) => {
|
|
191
|
+
stdout += chunk
|
|
192
|
+
})
|
|
193
|
+
child.stderr?.on('data', (chunk) => {
|
|
194
|
+
stderr += chunk
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
child.once('error', rejectRun)
|
|
198
|
+
child.once('exit', (code, signal) => resolveRun({ code, signal, stdout, stderr }))
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const DEFAULT_DEPS = {
|
|
202
|
+
connect: connectToOpenScience,
|
|
203
|
+
locateBackend: () => locateBackend({ required: false }),
|
|
204
|
+
resolveConfigRoot: (options) => resolveConfigRoot(options),
|
|
205
|
+
resolveConfiguration: (configRoot) => resolveCodexLoginConfiguration(configRoot),
|
|
206
|
+
mkdir: (path) => mkdir(path, { recursive: true }),
|
|
207
|
+
runCodex: runCodexProcess,
|
|
208
|
+
log: (...args) => console.log(...args)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export const codexLoginCommand = async (options, dependencies = {}) => {
|
|
212
|
+
const deps = { ...DEFAULT_DEPS, ...dependencies }
|
|
213
|
+
const backend = await deps.locateBackend()
|
|
214
|
+
const configRoot = deps.resolveConfigRoot({
|
|
215
|
+
packaged: !backend.development,
|
|
216
|
+
override: options.configRoot
|
|
217
|
+
})
|
|
218
|
+
let client
|
|
219
|
+
try {
|
|
220
|
+
client = await deps.connect({ configRoot })
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (error?.code !== 'daemon_unavailable' || error?.status !== undefined) throw error
|
|
223
|
+
// Already-configured profiles retain offline terminal login. First-run writes need the owner.
|
|
224
|
+
try {
|
|
225
|
+
await deps.resolveConfiguration(configRoot)
|
|
226
|
+
} catch {
|
|
227
|
+
throw new CodexLoginError(
|
|
228
|
+
'Start Open-Science first: open-science start --no-open; then retry codex login.',
|
|
229
|
+
'daemon_unavailable'
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const bootstrap = async (action) => {
|
|
234
|
+
if (!client) return
|
|
235
|
+
const result = await client.bootstrap({ action }, { timeoutMs: 600_000 })
|
|
236
|
+
if (!result.ok) throw new CodexLoginError(`Codex setup failed: ${result.code}.`, result.code)
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
await bootstrap('codex-prepare')
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error?.status !== 404) throw error
|
|
242
|
+
// Older daemons cannot register readiness, but configured native login remains available.
|
|
243
|
+
try {
|
|
244
|
+
await deps.resolveConfiguration(configRoot)
|
|
245
|
+
} catch {
|
|
246
|
+
throw new CodexLoginError(
|
|
247
|
+
'Update and restart Open-Science before setting up Codex for this profile.',
|
|
248
|
+
'bootstrap_unavailable'
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
client = undefined
|
|
252
|
+
}
|
|
253
|
+
const complete = async () => {
|
|
254
|
+
if (client) await bootstrap('codex-complete')
|
|
255
|
+
else
|
|
256
|
+
deps.log(
|
|
257
|
+
'Sign-in saved. Use a running, updated Open-Science daemon and run codex login again to validate and activate the provider.'
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
const { codexPath, networkProxy } = await deps.resolveConfiguration(configRoot)
|
|
261
|
+
const codexHome = join(configRoot, 'codex-subscription')
|
|
262
|
+
await deps.mkdir(codexHome)
|
|
263
|
+
const env = createCodexLoginEnvironment(codexHome, process.env, process.platform, networkProxy)
|
|
264
|
+
const baseArgs = ['-c', CODEX_CONFIG_OVERRIDE, 'login']
|
|
265
|
+
|
|
266
|
+
if (!options.force) {
|
|
267
|
+
const status = await deps.runCodex(codexPath, [...baseArgs, 'status'], {
|
|
268
|
+
env,
|
|
269
|
+
inherit: false
|
|
270
|
+
})
|
|
271
|
+
if (status.code === 0) {
|
|
272
|
+
await complete()
|
|
273
|
+
deps.log(
|
|
274
|
+
'Codex is already signed in for Open-Science. Use "open-science codex login --force" to sign in again.'
|
|
275
|
+
)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
if (status.code !== 1 || status.signal) {
|
|
279
|
+
throw new CodexLoginError('Open-Science could not check the existing Codex sign-in.')
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
deps.log('Starting Codex device-code sign-in. Keep this terminal open until it completes...')
|
|
284
|
+
const login = await deps.runCodex(codexPath, [...baseArgs, '--device-auth'], {
|
|
285
|
+
env,
|
|
286
|
+
inherit: true
|
|
287
|
+
})
|
|
288
|
+
if (login.code !== 0 || login.signal) {
|
|
289
|
+
const suffix = login.signal ? ' after receiving ' + login.signal : ''
|
|
290
|
+
throw new CodexLoginError(
|
|
291
|
+
'Codex device-code sign-in did not complete' + suffix + '.',
|
|
292
|
+
'codex_login_failed',
|
|
293
|
+
login.code ?? 1
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
await complete()
|
|
297
|
+
deps.log('Codex is signed in for Open-Science.')
|
|
298
|
+
}
|
package/config-root.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
|
2
|
+
|
|
3
|
+
import { access, readFile } from 'node:fs/promises'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { isAbsolute, join, normalize } from 'node:path'
|
|
6
|
+
|
|
7
|
+
export const DEV_CONFIG_DIR = '.open-science-project'
|
|
8
|
+
export const PROD_CONFIG_DIR = '.open-science'
|
|
9
|
+
export const STATE_FILE = 'web-service.json'
|
|
10
|
+
export const TOKEN_FILE = 'web-token'
|
|
11
|
+
|
|
12
|
+
const exists = async (path) => {
|
|
13
|
+
try {
|
|
14
|
+
await access(path)
|
|
15
|
+
return true
|
|
16
|
+
} catch {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const resolveConfigRoot = ({ packaged = false, override, env = process.env } = {}) => {
|
|
22
|
+
const requested = override ?? env.OPEN_SCIENCE_CONFIG_ROOT ?? env.OPEN_SCIENCE_STORAGE_ROOT
|
|
23
|
+
if (requested) {
|
|
24
|
+
if (!isAbsolute(requested)) throw new Error('The config root must be an absolute path.')
|
|
25
|
+
return normalize(requested)
|
|
26
|
+
}
|
|
27
|
+
return join(homedir(), packaged ? PROD_CONFIG_DIR : DEV_CONFIG_DIR)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const candidateConfigRoots = ({ override, env = process.env } = {}) => {
|
|
31
|
+
if (override ?? env.OPEN_SCIENCE_CONFIG_ROOT ?? env.OPEN_SCIENCE_STORAGE_ROOT) {
|
|
32
|
+
return [resolveConfigRoot({ override, env })]
|
|
33
|
+
}
|
|
34
|
+
return [resolveConfigRoot({ packaged: false, env }), resolveConfigRoot({ packaged: true, env })]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const readStateFromRoot = async (configRoot) => {
|
|
38
|
+
try {
|
|
39
|
+
const state = JSON.parse(await readFile(join(configRoot, STATE_FILE), 'utf8'))
|
|
40
|
+
if (
|
|
41
|
+
!Number.isInteger(state.pid) ||
|
|
42
|
+
!Number.isInteger(state.port) ||
|
|
43
|
+
typeof state.startedAt !== 'string'
|
|
44
|
+
) {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
// The containing directory owns both the state and token. A stale or edited payload must not
|
|
48
|
+
// redirect credential reads or cleanup outside the enumerated candidate.
|
|
49
|
+
return { ...state, configRoot }
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const findServiceState = async (options = {}) => {
|
|
56
|
+
for (const configRoot of candidateConfigRoots(options)) {
|
|
57
|
+
if (!(await exists(join(configRoot, STATE_FILE)))) continue
|
|
58
|
+
const state = await readStateFromRoot(configRoot)
|
|
59
|
+
if (state && (!options.accept || (await options.accept(state)))) return state
|
|
60
|
+
}
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const readWebToken = async (configRoot) =>
|
|
65
|
+
(await readFile(join(configRoot, TOKEN_FILE), 'utf8')).trim()
|