@ai-workbench/connect 0.0.0-stage → 0.7.5

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.
Files changed (129) hide show
  1. package/NOTICE.md +18 -0
  2. package/README.md +24 -2
  3. package/client/index.cjs +123 -0
  4. package/cordis.patch.yml +3 -0
  5. package/helper/auth-status.cjs +18 -0
  6. package/helper/claude-callback.cjs +29 -0
  7. package/helper/main.cjs +19 -0
  8. package/helper/projects.cjs +57 -0
  9. package/helper/remote-command.cjs +15 -0
  10. package/helper/remote-login.cjs +25 -0
  11. package/helper/window.cjs +52 -0
  12. package/lib/api.d.ts +196 -0
  13. package/lib/api.js +1 -0
  14. package/lib/archive.d.ts +1 -0
  15. package/lib/archive.js +63 -0
  16. package/lib/auth-status.d.ts +3 -0
  17. package/lib/auth-status.js +18 -0
  18. package/lib/cache.d.ts +21 -0
  19. package/lib/cache.js +156 -0
  20. package/lib/component-download.d.ts +25 -0
  21. package/lib/component-download.js +73 -0
  22. package/lib/components.d.ts +9 -0
  23. package/lib/components.js +121 -0
  24. package/lib/connect.d.ts +10 -0
  25. package/lib/connect.js +246 -0
  26. package/lib/cordis.d.ts +15 -0
  27. package/lib/cordis.js +1 -0
  28. package/lib/defaults.d.ts +29 -0
  29. package/lib/defaults.js +4 -0
  30. package/lib/electron.d.ts +8 -0
  31. package/lib/electron.js +83 -0
  32. package/lib/enrollment.d.ts +3 -0
  33. package/lib/enrollment.js +78 -0
  34. package/lib/index.d.ts +40 -0
  35. package/lib/index.js +48 -0
  36. package/lib/integration.d.ts +5 -0
  37. package/lib/integration.js +47 -0
  38. package/lib/login.d.ts +35 -0
  39. package/lib/login.js +81 -0
  40. package/lib/native-import-picker.d.ts +11 -0
  41. package/lib/native-import-picker.js +49 -0
  42. package/lib/needs.d.ts +9 -0
  43. package/lib/needs.js +52 -0
  44. package/lib/network.d.ts +26 -0
  45. package/lib/network.js +226 -0
  46. package/lib/physical-fs.d.ts +3 -0
  47. package/lib/physical-fs.js +7 -0
  48. package/lib/ports.d.ts +43 -0
  49. package/lib/ports.js +1 -0
  50. package/lib/private-files.d.ts +3 -0
  51. package/lib/private-files.js +41 -0
  52. package/lib/processes.d.ts +5 -0
  53. package/lib/processes.js +147 -0
  54. package/lib/profile-activation.d.ts +32 -0
  55. package/lib/profile-activation.js +118 -0
  56. package/lib/profile-enrollment.d.ts +21 -0
  57. package/lib/profile-enrollment.js +275 -0
  58. package/lib/profile-runtime.d.ts +6 -0
  59. package/lib/profile-runtime.js +47 -0
  60. package/lib/profile-settings.d.ts +2 -0
  61. package/lib/profile-settings.js +10 -0
  62. package/lib/projects.d.ts +4 -0
  63. package/lib/projects.js +41 -0
  64. package/lib/provider.d.ts +73 -0
  65. package/lib/provider.js +39 -0
  66. package/lib/remote-login.d.ts +2 -0
  67. package/lib/remote-login.js +9 -0
  68. package/lib/ssh.d.ts +10 -0
  69. package/lib/ssh.js +78 -0
  70. package/lib/startup.d.ts +18 -0
  71. package/lib/startup.js +211 -0
  72. package/lib/workspace-bindings.d.ts +32 -0
  73. package/lib/workspace-bindings.js +328 -0
  74. package/locale/en.json +6 -0
  75. package/locale/zh.json +6 -0
  76. package/node_modules/argparse/LICENSE +254 -0
  77. package/node_modules/argparse/README.md +84 -0
  78. package/node_modules/argparse/argparse.js +3707 -0
  79. package/node_modules/argparse/lib/sub.js +67 -0
  80. package/node_modules/argparse/lib/textwrap.js +440 -0
  81. package/node_modules/argparse/package.json +31 -0
  82. package/node_modules/js-yaml/LICENSE +21 -0
  83. package/node_modules/js-yaml/README.md +233 -0
  84. package/node_modules/js-yaml/bin/js-yaml.js +117 -0
  85. package/node_modules/js-yaml/dist/js-yaml.js +3022 -0
  86. package/node_modules/js-yaml/dist/js-yaml.js.map +1 -0
  87. package/node_modules/js-yaml/dist/js-yaml.min.js +2 -0
  88. package/node_modules/js-yaml/dist/js-yaml.min.js.map +1 -0
  89. package/node_modules/js-yaml/dist/js-yaml.mjs +3082 -0
  90. package/node_modules/js-yaml/dist/js-yaml.mjs.map +1 -0
  91. package/node_modules/js-yaml/index.js +44 -0
  92. package/node_modules/js-yaml/lib/common.js +50 -0
  93. package/node_modules/js-yaml/lib/dumper.js +937 -0
  94. package/node_modules/js-yaml/lib/exception.js +51 -0
  95. package/node_modules/js-yaml/lib/index_vite_proxy.tmp.mjs +37 -0
  96. package/node_modules/js-yaml/lib/loader.js +1803 -0
  97. package/node_modules/js-yaml/lib/schema/core.js +9 -0
  98. package/node_modules/js-yaml/lib/schema/default.js +20 -0
  99. package/node_modules/js-yaml/lib/schema/failsafe.js +14 -0
  100. package/node_modules/js-yaml/lib/schema/json.js +17 -0
  101. package/node_modules/js-yaml/lib/schema.js +109 -0
  102. package/node_modules/js-yaml/lib/snippet.js +96 -0
  103. package/node_modules/js-yaml/lib/type/binary.js +122 -0
  104. package/node_modules/js-yaml/lib/type/bool.js +35 -0
  105. package/node_modules/js-yaml/lib/type/float.js +99 -0
  106. package/node_modules/js-yaml/lib/type/int.js +142 -0
  107. package/node_modules/js-yaml/lib/type/map.js +8 -0
  108. package/node_modules/js-yaml/lib/type/merge.js +12 -0
  109. package/node_modules/js-yaml/lib/type/null.js +35 -0
  110. package/node_modules/js-yaml/lib/type/omap.js +45 -0
  111. package/node_modules/js-yaml/lib/type/pairs.js +50 -0
  112. package/node_modules/js-yaml/lib/type/seq.js +8 -0
  113. package/node_modules/js-yaml/lib/type/set.js +29 -0
  114. package/node_modules/js-yaml/lib/type/str.js +8 -0
  115. package/node_modules/js-yaml/lib/type/timestamp.js +88 -0
  116. package/node_modules/js-yaml/lib/type.js +66 -0
  117. package/node_modules/js-yaml/package.json +83 -0
  118. package/node_modules/pend/LICENSE +23 -0
  119. package/node_modules/pend/README.md +41 -0
  120. package/node_modules/pend/index.js +55 -0
  121. package/node_modules/pend/package.json +18 -0
  122. package/node_modules/pend/test.js +137 -0
  123. package/node_modules/yauzl/LICENSE +21 -0
  124. package/node_modules/yauzl/README.md +1015 -0
  125. package/node_modules/yauzl/crc32.js +45 -0
  126. package/node_modules/yauzl/fd-slicer.js +188 -0
  127. package/node_modules/yauzl/index.js +1096 -0
  128. package/node_modules/yauzl/package.json +40 -0
  129. package/package.json +89 -4
@@ -0,0 +1,10 @@
1
+ import { isAbsolute, join } from 'node:path';
2
+ import { L_DEFAULTS, ELECTRON_PIN } from "./defaults.js";
3
+ // Owner approved technical values, 2026-10-20 settings decision, part two.
4
+ // 组件直接来自自己的官方地址;OAuth网页仍通过N主机出口。
5
+ export function profileSettings(config, profileDir, platform = process.platform, arch = process.arch) {
6
+ if (typeof profileDir !== 'string' || !isAbsolute(profileDir))
7
+ return config;
8
+ const runtime = platform === 'darwin' && arch === 'arm64' ? ELECTRON_PIN : {};
9
+ return { ...config, commandOutputMaxBytes: config.commandOutputMaxBytes === undefined ? L_DEFAULTS.commandOutputMaxBytes : config.commandOutputMaxBytes, helper: { ...runtime, componentDownloadMaxRedirects: L_DEFAULTS.componentDownloadMaxRedirects, maxDownloadBytes: L_DEFAULTS.maxDownloadBytes, maxExtractedBytes: L_DEFAULTS.maxExtractedBytes, loginOutputMaxBytes: L_DEFAULTS.loginOutputMaxBytes, ipcMaxBytes: L_DEFAULTS.ipcMaxBytes, remoteKillGraceMs: L_DEFAULTS.remoteKillGraceMs, ...config.helper, cacheDir: config.helper?.cacheDir === undefined ? join(profileDir, 'ai-workbench-connect', 'login-component-cache') : config.helper.cacheDir } };
10
+ }
@@ -0,0 +1,4 @@
1
+ import type { Ports } from './ports.ts';
2
+ import type { Runner } from './ssh.ts';
3
+ export declare function isProjectName(name: string): boolean;
4
+ export declare function createProjects(runner: Runner): NonNullable<Ports['projects']>;
@@ -0,0 +1,41 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { runRemote } from "./ssh.js";
3
+ import { fail, ok } from "./needs.js";
4
+ export function isProjectName(name) { return typeof name === 'string' && name !== '' && name !== '.' && name !== '..' && !/[\\/\x00-\x1f\x7f]/.test(name); }
5
+ const codes = ['PROJECT_ROOT_INVALID', 'PROJECT_EXISTS', 'PROJECT_INIT_FAILED', 'PROJECT_CREATE_FAILED', 'PROJECT_LIST_FAILED', 'PROJECT_NAME_INVALID'];
6
+ const reasons = new Set(['invalid-name', 'symlink', 'not-directory', 'permissions', 'not-git', 'invalid-git', 'outside-root']);
7
+ function validProject(value, root) {
8
+ return typeof value === 'object' && value !== null && 'name' in value && typeof value.name === 'string' && isProjectName(value.name) && 'path' in value && value.path === root + '/' + value.name;
9
+ }
10
+ export function createProjects(runner) {
11
+ const request = async (t, operation, signal, name) => {
12
+ if (operation === 'create' && !isProjectName(name))
13
+ return fail('PROJECT_NAME_INVALID');
14
+ try {
15
+ const source = await readFile(new URL('../helper/projects.cjs', import.meta.url), 'utf8');
16
+ const command = 'node -e ' + "'" + (source + '\nmain();').replaceAll("'", "'\\''") + "'";
17
+ const raw = await runRemote(runner, t, command, signal, JSON.stringify({ protocolVersion: 1, operation, root: t.projectRoot, ...(operation === 'create' ? { name } : {}) }));
18
+ if (signal.aborted)
19
+ return fail('CONNECTION_LOST');
20
+ if (raw.exitCode !== 0)
21
+ return fail(operation === 'list' ? 'PROJECT_LIST_FAILED' : 'PROJECT_CREATE_FAILED');
22
+ const result = JSON.parse(raw.stdout);
23
+ if (result.protocolVersion !== 1 || typeof result.ok !== 'boolean')
24
+ return fail('PROJECT_PROTOCOL_INVALID');
25
+ if (!result.ok) {
26
+ const code = codes.find(code => code === result.error?.code);
27
+ return fail(code ?? 'PROJECT_PROTOCOL_INVALID');
28
+ }
29
+ if (operation === 'create')
30
+ return validProject(result.value, t.projectRoot) && result.value.name === name ? ok(result.value) : fail('PROJECT_PROTOCOL_INVALID');
31
+ const list = result.value;
32
+ if (!list || !Array.isArray(list.projects) || !Array.isArray(list.unavailable) || !list.projects.every((p) => validProject(p, t.projectRoot)) || !list.unavailable.every((p) => p && typeof p.name === 'string' && reasons.has(p.reason)))
33
+ return fail('PROJECT_PROTOCOL_INVALID');
34
+ return ok(list);
35
+ }
36
+ catch {
37
+ return fail(operation === 'list' ? 'PROJECT_LIST_FAILED' : 'PROJECT_CREATE_FAILED');
38
+ }
39
+ };
40
+ return { list: (t, s) => request(t, 'list', s), create: (t, n, s) => request(t, 'create', s, n) };
41
+ }
@@ -0,0 +1,73 @@
1
+ import type { Ports } from './ports.ts';
2
+ import type { ProxyLease } from './login.ts';
3
+ interface NNeed {
4
+ code: string;
5
+ }
6
+ type NResult<T> = {
7
+ ok: true;
8
+ value: T;
9
+ } | {
10
+ ok: false;
11
+ error: NNeed;
12
+ };
13
+ export interface ActivationSettings {
14
+ configFile: string;
15
+ helperLogFile: string;
16
+ startupObservationSeconds: number;
17
+ shutdownObservationSeconds: number;
18
+ controlResponseMaxBytes: number;
19
+ downloadMaxRedirects: number;
20
+ }
21
+ export interface ActivationState {
22
+ instanceId: string;
23
+ revision: number;
24
+ phase: 'unconfigured' | 'activating' | 'active' | 'blocked' | 'stopped';
25
+ receipt: {
26
+ apiVersion: 1;
27
+ session: 'key' | 'identity';
28
+ key: 'not-required' | 'unattempted' | 'attempted' | 'confirmed' | 'unknown';
29
+ identity: 'unconfirmed' | 'resumable';
30
+ cleanup: 'not-required' | 'pending' | 'complete';
31
+ } | null;
32
+ error: NNeed | null;
33
+ }
34
+ export interface NetworkProvider {
35
+ activate?(settings: ActivationSettings, signal: AbortSignal): Promise<NResult<ActivationState>>;
36
+ getActivationState?(): ActivationState;
37
+ onActivationState?(listener: (state: ActivationState) => void): () => void;
38
+ getState(): {
39
+ phase: string;
40
+ error?: NNeed;
41
+ status?: {
42
+ host: {
43
+ lastSeenAt: string | null;
44
+ };
45
+ };
46
+ };
47
+ connect(target: {
48
+ host: string;
49
+ username: string;
50
+ port: number;
51
+ knownHostsFile: string;
52
+ }, signal: AbortSignal): Promise<NResult<{
53
+ proxyCommand: string;
54
+ }>>;
55
+ onState(listener: (event: {
56
+ kind: 'lost' | 'restored';
57
+ }) => void): () => void;
58
+ openLoginProxy(signal: AbortSignal): Promise<NResult<{
59
+ credentials: {
60
+ url: string;
61
+ username: string;
62
+ password: string;
63
+ };
64
+ release(): Promise<void>;
65
+ }>>;
66
+ download(source: string, signal: AbortSignal): Promise<NResult<Response>>;
67
+ }
68
+ export declare function mapNetworkNeed(value: NNeed): import("./api.ts").ConnectNeed;
69
+ export declare function adaptNetwork(provider: NetworkProvider): NonNullable<Ports['network']> & {
70
+ acquireProxy(signal: AbortSignal): Promise<ProxyLease>;
71
+ download(source: string, signal: AbortSignal): Promise<Response>;
72
+ };
73
+ export {};
@@ -0,0 +1,39 @@
1
+ import { NeedError, need } from "./needs.js";
2
+ export function mapNetworkNeed(value) {
3
+ const code = { NEED_ENROLLMENT: 'ENROLLMENT_REQUIRED', ENROLLMENT_REJECTED: 'ENROLLMENT_REJECTED', ENROLLMENT_RESULT_UNKNOWN: 'ENROLLMENT_RESULT_UNKNOWN', LOCAL_STATE_INVALID: 'ENROLLMENT_INVALID', HOST_OFFLINE: 'HOST_OFFLINE', HOST_TRUST_REQUIRED: 'HOST_TRUST_REQUIRED', HOST_KEY_MISMATCH: 'HOST_KEY_MISMATCH', ENROLLMENT_MISMATCH: 'ENROLLMENT_INVALID', CONFIG_INVALID: 'SETTINGS_REQUIRED', SETTINGS_REQUIRED: 'SETTINGS_REQUIRED', SSH_FAILED: 'SSH_FAILED', NETWORK_INTERRUPTED: 'CONNECTION_LOST', CONTROL_UNAVAILABLE: 'CONNECTION_LOST', NETWORK_NOT_READY: 'CONNECTION_LOST', DISPOSED: 'DISPOSED' }[value.code] ?? 'DEPENDENCY_REQUIRED';
4
+ const direct = ['ACTIVATION_CONFLICT', 'ACTIVATION_CANCELLED', 'ENROLLMENT_CLEANUP_UNBOUND', 'ENROLLMENT_CLEANUP_FAILED', 'ENROLLMENT_MISMATCH', 'LOCAL_STATE_INVALID', 'ALREADY_RUNNING', 'ENROLLMENT_RECEIPT_REQUIRED', 'CONTROL_PROTOCOL_INVALID', 'HELPER_HANDOFF_FAILED', 'HELPER_SHUTDOWN_FORCED'];
5
+ const result = need(direct.includes(value.code) ? value.code : code);
6
+ result.officialReason = /^[A-Z][A-Z0-9_]*$/.test(value.code) ? value.code : 'NETWORK_NEED_INVALID';
7
+ return result;
8
+ }
9
+ export function adaptNetwork(provider) {
10
+ return {
11
+ async connect(e, signal) {
12
+ const result = await provider.connect({ host: e.host, username: e.username, port: e.port, knownHostsFile: e.knownHostsFile }, signal);
13
+ if (result.ok)
14
+ return result;
15
+ const error = mapNetworkNeed(result.error), lastSeen = provider.getState().status?.host.lastSeenAt;
16
+ if (error.code === 'HOST_OFFLINE' && typeof lastSeen === 'string' && !Number.isNaN(Date.parse(lastSeen)))
17
+ error.lastSeen = lastSeen;
18
+ return { ok: false, error };
19
+ },
20
+ onState(listener) { return provider.onState(event => { if (event.kind === 'lost' || event.kind === 'restored')
21
+ listener({ kind: event.kind }); }); },
22
+ async acquireProxy(signal) {
23
+ const result = await provider.openLoginProxy(signal);
24
+ if (!result.ok)
25
+ throw new NeedError(mapNetworkNeed(result.error));
26
+ const source = result.value, credentials = source.credentials;
27
+ let release;
28
+ return { url: credentials.url, get username() { return credentials.username; }, get password() { return credentials.password; }, release: () => release ??= (async () => { try {
29
+ await source.release();
30
+ }
31
+ finally {
32
+ credentials.username = '';
33
+ credentials.password = '';
34
+ } })() };
35
+ },
36
+ async download(source, signal) { const result = await provider.download(source, signal); if (!result.ok)
37
+ throw new NeedError(mapNetworkNeed(result.error)); return result.value; },
38
+ };
39
+ }
@@ -0,0 +1,2 @@
1
+ import type { Provider } from './api.ts';
2
+ export declare function remoteLoginCommand(provider: Provider, killGraceMs: number, probeIsolate?: boolean): string;
@@ -0,0 +1,9 @@
1
+ import { readFileSync } from 'node:fs';
2
+ export function remoteLoginCommand(provider, killGraceMs, probeIsolate = false) {
3
+ if (provider !== 'claude' && provider !== 'codex')
4
+ throw Error('LOGIN_PROVIDER_INVALID');
5
+ if (!Number.isSafeInteger(killGraceMs) || killGraceMs < 0 || killGraceMs > 2147483647)
6
+ throw Error('REMOTE_KILL_GRACE_REQUIRED');
7
+ const source = readFileSync(new URL('../helper/remote-login.cjs', import.meta.url), 'utf8');
8
+ return "node -e '" + source.replaceAll("'", "'\\''") + "' -- " + provider + ' ' + killGraceMs + (probeIsolate ? ' --probe-isolate' : '');
9
+ }
package/lib/ssh.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { SshTarget } from './api.ts';
2
+ import type { CommandResult, Ports } from './ports.ts';
3
+ export declare function shellQuote(s: string): string;
4
+ export declare function proxyCommand(binary: string, runtimeDir: string): string;
5
+ export declare function sshArgs(t: SshTarget): string[];
6
+ export interface Runner {
7
+ run(cmd: string, args: string[], signal: AbortSignal, input?: string): Promise<CommandResult>;
8
+ }
9
+ export declare function runRemote(runner: Runner, t: SshTarget, command: string, signal: AbortSignal, input?: string): Promise<CommandResult>;
10
+ export declare function createSsh(runner: Runner, maxOutputBytes?: number): NonNullable<Ports['ssh']>;
package/lib/ssh.js ADDED
@@ -0,0 +1,78 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { fail, ok } from "./needs.js";
4
+ export function shellQuote(s) {
5
+ if (/[\x00-\x1f\x7f%]/.test(s))
6
+ throw new Error('INVALID_PROXY_PATH');
7
+ return "'" + s.replaceAll("'", "'\\''") + "'";
8
+ }
9
+ export function proxyCommand(binary, runtimeDir) {
10
+ if (!binary.startsWith('/') || !runtimeDir.startsWith('/'))
11
+ throw new Error('ABSOLUTE_PROXY_PATH_REQUIRED');
12
+ return `${shellQuote(binary)} ssh-proxy --runtime-dir ${shellQuote(runtimeDir)} %h %p`;
13
+ }
14
+ export function sshArgs(t) {
15
+ return ['-F', '/dev/null', '-T', '-o', 'IdentityAgent=none', '-o', 'PubkeyAuthentication=no', '-o', 'PasswordAuthentication=no', '-o', 'KbdInteractiveAuthentication=no', '-o', 'PreferredAuthentications=none', '-o', 'UpdateHostKeys=no', '-o', 'CanonicalizeHostname=no', '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=yes', '-o', 'GlobalKnownHostsFile=/dev/null', '-o', 'UserKnownHostsFile=' + JSON.stringify(t.knownHostsFile.replaceAll('%', '%%')), '-o', 'ProxyCommand=' + t.proxyCommand, '-p', String(t.port), '-l', t.username, t.host];
16
+ }
17
+ export async function runRemote(runner, t, command, signal, input) {
18
+ const nonce = randomUUID(), source = await readFile(new URL('../helper/remote-command.cjs', import.meta.url), 'utf8');
19
+ const remote = 'node -e ' + "'" + source.replaceAll("'", "'\\''") + "'";
20
+ const raw = await runner.run('ssh', [...sshArgs(t), remote], signal, JSON.stringify({ protocolVersion: 1, nonce, command, ...input === undefined ? {} : { input } }));
21
+ if (signal.aborted || raw.exitCode !== 0 || raw.stderr !== '')
22
+ throw Error('REMOTE_RESULT_INVALID');
23
+ const lines = raw.stdout.split('\n');
24
+ if (lines.pop() !== '')
25
+ throw Error('REMOTE_RESULT_INVALID');
26
+ let started = false, ended = false, exitCode = null;
27
+ const out = [], err = [];
28
+ for (const line of lines) {
29
+ const v = JSON.parse(line);
30
+ if (!v || typeof v !== 'object' || v.protocolVersion !== 1 || v.nonce !== nonce || ended)
31
+ throw Error('REMOTE_RESULT_INVALID');
32
+ const keys = Object.keys(v).sort().join(',');
33
+ if (v.type === 'start' && !started && keys === 'nonce,protocolVersion,type') {
34
+ started = true;
35
+ continue;
36
+ }
37
+ if (!started)
38
+ throw Error('REMOTE_RESULT_INVALID');
39
+ if ((v.type === 'stdout' || v.type === 'stderr') && keys === 'data,nonce,protocolVersion,type' && typeof v.data === 'string') {
40
+ const bytes = Buffer.from(v.data, 'base64');
41
+ if (bytes.toString('base64') !== v.data)
42
+ throw Error('REMOTE_RESULT_INVALID');
43
+ (v.type === 'stdout' ? out : err).push(bytes);
44
+ continue;
45
+ }
46
+ if (v.type === 'end' && keys === 'exitCode,nonce,protocolVersion,signal,type' && Number.isInteger(v.exitCode) && v.exitCode >= 0 && v.exitCode <= 255 && v.signal === null) {
47
+ ended = true;
48
+ exitCode = v.exitCode;
49
+ continue;
50
+ }
51
+ throw Error('REMOTE_RESULT_INVALID');
52
+ }
53
+ if (!ended || exitCode === null)
54
+ throw Error('REMOTE_RESULT_INVALID');
55
+ return { exitCode, stdout: Buffer.concat(out).toString('utf8'), stderr: Buffer.concat(err).toString('utf8') };
56
+ }
57
+ export function createSsh(runner, maxOutputBytes) {
58
+ return {
59
+ verify: async (t, signal) => { try {
60
+ const r = await runRemote(runner, t, 'id -un', signal);
61
+ return r.exitCode === 0 && r.stdout.trim() === t.username ? ok(undefined) : fail('SSH_FAILED');
62
+ }
63
+ catch {
64
+ return fail('SSH_FAILED');
65
+ } },
66
+ status: async (t, p, signal) => {
67
+ try {
68
+ if (!['codex', 'claude'].includes(p) || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1)
69
+ return fail('AUTH_STATUS_INVALID');
70
+ const source = await readFile(new URL('../helper/auth-status.cjs', import.meta.url), 'utf8');
71
+ return ok(await runRemote(runner, t, 'node -e ' + "'" + source.replaceAll("'", "'\\''") + "' -- " + p + ' ' + maxOutputBytes, signal));
72
+ }
73
+ catch {
74
+ return fail('AUTH_STATUS_INVALID');
75
+ }
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,18 @@
1
+ import type { ConnectApi, ConnectionState, LoginPromptState, LoginPromptResult, Provider, NeedCode, Result } from './api.ts';
2
+ export type StartupState = LoginPromptState;
3
+ export declare function employeeMessage(code?: NeedCode): string;
4
+ type Operations = Pick<ConnectApi, 'connect' | 'checkLogin' | 'login' | 'getState'>;
5
+ export declare function createStartup(api: Operations, providers: Provider[], emit?: (s: LoginPromptState) => void, result?: (r: LoginPromptResult) => void, instanceId?: string): {
6
+ getState: () => LoginPromptState;
7
+ check: () => Promise<void>;
8
+ subscribe(listener: (s: LoginPromptState) => void): () => void;
9
+ request(provider: Provider, intent: "automatic" | "user"): Promise<Result<LoginPromptState>>;
10
+ respond: (requestId: string, action: "authorize" | "defer") => Promise<Result<LoginPromptState>>;
11
+ defer(provider: Provider): void;
12
+ authorize(provider: Provider): Promise<void>;
13
+ connectionChanged(next: ConnectionState): void;
14
+ stop(): void;
15
+ dispose(): Promise<void>;
16
+ };
17
+ export type Startup = ReturnType<typeof createStartup>;
18
+ export {};
package/lib/startup.js ADDED
@@ -0,0 +1,211 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { fail, need, ok } from "./needs.js";
3
+ export function employeeMessage(code) {
4
+ if (code === 'ENROLLMENT_RESULT_UNKNOWN' || code === 'ENROLLMENT_REJECTED')
5
+ return need(code).message;
6
+ if (code === 'DEPENDENCY_REQUIRED')
7
+ return '远程主机连接尚未准备好,请联系管理员。';
8
+ if (code === 'ENROLLMENT_REQUIRED' || code === 'ENROLLMENT_INVALID' || code === 'NETWORK_KEY_INVALID')
9
+ return '我需要连接资料,请联系管理员发放。';
10
+ if (code === 'SETTINGS_REQUIRED')
11
+ return '远程主机连接还没有设置好,请联系管理员。';
12
+ if (code === 'LOGIN_FAILED')
13
+ return '登录没有完成。你可以再次请求登录,或稍后再试。';
14
+ if (code === 'POLICY_DENIED')
15
+ return '这个账号暂时不能使用,请联系公司账号管理员。';
16
+ return '远程主机暂时连不上,请联系管理员。';
17
+ }
18
+ export function createStartup(api, providers, emit = () => { }, result = () => { }, instanceId = randomUUID()) {
19
+ let disposed = false, revision = 0, serial = 0, generation = 0, checkSerial = 0, connectionPhase = 'unavailable', observedConnected = false;
20
+ let flight, authorization, authorizationId;
21
+ const pending = new Set(), listeners = new Set(), deferred = new Set();
22
+ let state = { instanceId, revision: 0, phase: 'checking', message: '正在连接远程主机…', logins: {}, checkingProviders: [], providerNeeds: {}, technicalDetails: {} };
23
+ const publish = (next) => { state = { ...next, instanceId, revision: ++revision }; emit(structuredClone(state)); for (const listener of listeners)
24
+ listener(structuredClone(state)); };
25
+ const track = (p) => { pending.add(p); void p.then(() => pending.delete(p), () => pending.delete(p)); return p; };
26
+ const close = (outcome, code) => { if (state.requestId && state.provider)
27
+ result({ requestId: state.requestId, provider: state.provider, outcome, ...(code ? { need: need(code) } : {}) }); };
28
+ const blocked = (code) => { close('invalidated', code); publish({ phase: 'blocked', message: employeeMessage(code), logins: {}, checkingProviders: [], providerNeeds: {}, technicalDetails: { ...state.technicalDetails, code } }); };
29
+ const snapshot = () => state.phase === 'disposed' ? fail('DISPOSED') : state.phase === 'blocked' ? fail(state.technicalDetails.code ?? 'AUTH_STATUS_INVALID') : ok(structuredClone(state));
30
+ const ask = () => {
31
+ const provider = state.requestId && state.provider && state.logins[state.provider] === false && !deferred.has(state.provider) ? state.provider : providers.find(p => state.logins[p] === false && !deferred.has(p));
32
+ const same = provider && state.provider === provider && state.requestId && state.phase === 'login-required';
33
+ publish({ ...state, phase: provider ? 'login-required' : 'idle', provider, requestId: provider ? (same ? state.requestId : 'login-' + instanceId + '-' + (++serial)) : undefined, message: provider ? '需要登录,请到设置 → 远程主机连接 点登录' : Object.keys(state.providerNeeds).length ? Object.keys(state.providerNeeds).map(p => p === 'claude' ? 'Claude' : 'Codex').join('、') + ' 登录状态无法确认,请联系管理员。' : '远程主机连接检查完成。' });
34
+ };
35
+ const check = () => {
36
+ if (disposed)
37
+ return Promise.resolve();
38
+ if (authorization)
39
+ return Promise.resolve();
40
+ if (flight)
41
+ return flight;
42
+ const mine = generation, checkMine = ++checkSerial;
43
+ const run = async () => {
44
+ try {
45
+ // Preserve an existing question while gathering fresh evidence, so duplicate requests keep its ticket.
46
+ publish({ ...state, ...(!state.requestId ? { phase: 'checking', message: '正在检查登录状态…' } : {}), checkingProviders: [] });
47
+ const connected = await api.connect();
48
+ if (disposed || mine !== generation || checkMine !== checkSerial)
49
+ return;
50
+ if (!connected.ok) {
51
+ blocked(connected.error.code);
52
+ return;
53
+ }
54
+ const logins = { ...state.logins }, providerNeeds = { ...state.providerNeeds };
55
+ for (const provider of providers) {
56
+ publish({ ...state, checkingProviders: [provider] });
57
+ const status = await api.checkLogin(provider);
58
+ if (disposed || mine !== generation || checkMine !== checkSerial)
59
+ return;
60
+ if (!status.ok) {
61
+ if (status.error.layer !== 'auth' && !['AUTH_STATUS_INVALID', 'LOGIN_FAILED', 'POLICY_DENIED'].includes(status.error.code)) {
62
+ blocked(status.error.code);
63
+ return;
64
+ }
65
+ delete logins[provider];
66
+ providerNeeds[provider] = status.error;
67
+ }
68
+ else {
69
+ logins[provider] = status.value.loggedIn;
70
+ delete providerNeeds[provider];
71
+ }
72
+ if (state.requestId && state.provider === provider && logins[provider] !== false) {
73
+ close(logins[provider] === true ? 'logged-in' : 'invalidated', providerNeeds[provider]?.code);
74
+ publish({ ...state, requestId: undefined, provider: undefined, phase: 'checking', message: '正在检查登录状态…' });
75
+ }
76
+ publish({ ...state, logins: { ...logins }, providerNeeds: { ...providerNeeds }, checkingProviders: state.checkingProviders.filter(p => p !== provider) });
77
+ }
78
+ if (state.requestId && state.provider && logins[state.provider] === true) {
79
+ close('logged-in');
80
+ publish({ ...state, requestId: undefined, provider: undefined, phase: 'checking', message: '正在检查登录状态…' });
81
+ }
82
+ publish({ ...state, logins, providerNeeds, checkingProviders: [], technicalDetails: connected.value.hostContext ? { hostContext: structuredClone(connected.value.hostContext) } : {} });
83
+ ask();
84
+ }
85
+ catch {
86
+ if (!disposed && mine === generation && checkMine === checkSerial)
87
+ blocked('DEPENDENCY_REQUIRED');
88
+ }
89
+ };
90
+ const promise = track(run());
91
+ flight = promise;
92
+ void promise.then(() => { if (flight === promise)
93
+ flight = undefined; });
94
+ return promise;
95
+ };
96
+ const providerFailed = (provider, code, sourceNeed = need(code)) => {
97
+ const logins = { ...state.logins };
98
+ delete logins[provider];
99
+ publish({ ...state, phase: 'idle', requestId: undefined, provider: undefined, logins, checkingProviders: [], providerNeeds: { ...state.providerNeeds, [provider]: structuredClone(sourceNeed) } });
100
+ ask();
101
+ };
102
+ const respond = (requestId, action) => {
103
+ if (disposed)
104
+ return Promise.resolve(fail('DISPOSED'));
105
+ if (action === 'authorize' && authorization && authorizationId === requestId)
106
+ return authorization;
107
+ if (!requestId || state.requestId !== requestId || state.phase !== 'login-required' || !state.provider || !['authorize', 'defer'].includes(action))
108
+ return Promise.resolve(fail('LOGIN_PROMPT_INVALID'));
109
+ const provider = state.provider;
110
+ if (action === 'defer') {
111
+ close('deferred');
112
+ deferred.add(provider);
113
+ publish({ ...state, requestId: undefined, provider: undefined });
114
+ ask();
115
+ return Promise.resolve(snapshot());
116
+ }
117
+ if (authorization)
118
+ return Promise.resolve(fail('LOGIN_PROMPT_INVALID'));
119
+ const mine = generation;
120
+ checkSerial++;
121
+ authorizationId = requestId;
122
+ publish({ ...state, phase: 'authorizing', checkingProviders: [provider], message: '请在授权窗口完成本人登录。' });
123
+ const run = async () => {
124
+ try {
125
+ await flight;
126
+ if (disposed || mine !== generation)
127
+ return fail(disposed ? 'DISPOSED' : 'CONNECTION_LOST');
128
+ const current = await api.checkLogin(provider);
129
+ if (disposed || mine !== generation)
130
+ return fail(disposed ? 'DISPOSED' : 'CONNECTION_LOST');
131
+ const verified = current.ok && current.value.loggedIn ? current : current.ok ? await api.login(provider) : current;
132
+ if (disposed || mine !== generation)
133
+ return fail(disposed ? 'DISPOSED' : 'CONNECTION_LOST');
134
+ if (!verified.ok || !verified.value.loggedIn) {
135
+ const code = verified.ok ? 'LOGIN_FAILED' : verified.error.code;
136
+ result({ requestId, provider, outcome: 'failed', need: need(code) });
137
+ providerFailed(provider, code, verified.ok ? need(code) : verified.error);
138
+ return verified.ok ? fail(code) : verified;
139
+ }
140
+ result({ requestId, provider, outcome: 'logged-in' });
141
+ deferred.delete(provider);
142
+ const providerNeeds = { ...state.providerNeeds };
143
+ delete providerNeeds[provider];
144
+ publish({ ...state, requestId: undefined, provider: undefined, logins: { ...state.logins, [provider]: true }, providerNeeds, checkingProviders: [], phase: 'idle' });
145
+ ask();
146
+ return snapshot();
147
+ }
148
+ catch {
149
+ if (disposed || mine !== generation)
150
+ return fail(disposed ? 'DISPOSED' : 'CONNECTION_LOST');
151
+ result({ requestId, provider, outcome: 'failed', need: need('LOGIN_FAILED') });
152
+ providerFailed(provider, 'LOGIN_FAILED');
153
+ return fail('LOGIN_FAILED');
154
+ }
155
+ };
156
+ const promise = track(run());
157
+ authorization = promise;
158
+ void promise.then(() => { if (authorization === promise) {
159
+ authorization = undefined;
160
+ authorizationId = undefined;
161
+ } });
162
+ return promise;
163
+ };
164
+ return { getState: () => structuredClone(state), check,
165
+ subscribe(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; },
166
+ async request(provider, intent) {
167
+ if (!providers.includes(provider) || !['automatic', 'user'].includes(intent))
168
+ return fail('AUTH_STATUS_INVALID');
169
+ const mine = generation;
170
+ if (intent === 'user')
171
+ deferred.delete(provider);
172
+ if (authorization)
173
+ return state.providerNeeds[provider] ? { ok: false, error: structuredClone(state.providerNeeds[provider]) } : snapshot();
174
+ await check();
175
+ if (disposed || mine !== generation)
176
+ return fail(disposed ? 'DISPOSED' : 'CONNECTION_LOST');
177
+ if (intent === 'user' && !authorization && state.logins[provider] === false && !state.providerNeeds[provider] && !state.checkingProviders.includes(provider) && state.phase !== 'blocked' && state.phase !== 'disposed' && state.provider !== provider) {
178
+ close('invalidated', 'LOGIN_PROMPT_INVALID');
179
+ publish({ ...state, phase: 'login-required', provider, requestId: 'login-' + instanceId + '-' + (++serial), message: '需要登录,请到设置 → 远程主机连接 点登录' });
180
+ }
181
+ return state.providerNeeds[provider] ? { ok: false, error: structuredClone(state.providerNeeds[provider]) } : snapshot();
182
+ }, respond,
183
+ // Internal compatibility facade. HTTP and public API must use ticketed responses.
184
+ defer(provider) { if (state.provider === provider && state.requestId)
185
+ void respond(state.requestId, 'defer'); },
186
+ async authorize(provider) { if (state.provider === provider && state.requestId)
187
+ await respond(state.requestId, 'authorize'); },
188
+ connectionChanged(next) {
189
+ if (disposed)
190
+ return;
191
+ const prior = connectionPhase;
192
+ connectionPhase = next.phase;
193
+ if (next.phase === 'unavailable' || next.phase === 'disposed') {
194
+ generation++;
195
+ flight = undefined;
196
+ blocked(next.need?.code ?? (next.phase === 'disposed' ? 'DISPOSED' : 'CONNECTION_LOST'));
197
+ }
198
+ else if (next.phase === 'connected' && prior !== 'connected') {
199
+ const restored = observedConnected;
200
+ observedConnected = true;
201
+ if (restored) {
202
+ const mine = generation;
203
+ queueMicrotask(() => { void Promise.resolve(authorization).then(() => { if (!disposed && mine === generation && connectionPhase === 'connected')
204
+ void check(); }); });
205
+ }
206
+ }
207
+ },
208
+ stop() { if (disposed)
209
+ return; disposed = true; generation++; close('invalidated', 'DISPOSED'); publish({ phase: 'disposed', message: '远程主机连接已关闭。', logins: {}, checkingProviders: [], providerNeeds: {}, technicalDetails: { code: 'DISPOSED' } }); listeners.clear(); },
210
+ async dispose() { this.stop(); await Promise.allSettled([...pending]); }, };
211
+ }
@@ -0,0 +1,32 @@
1
+ import type { ConnectApi, SessionProjectTarget, Result, ConnectionState, WorkspaceBindingState, WorkspaceNoticeReceipt, WorkspaceUse } from './api.ts';
2
+ export interface NativeWorkspace {
3
+ id: string;
4
+ path: string;
5
+ title: string;
6
+ sessionIds: string[];
7
+ }
8
+ export interface NativeWorkspaceRegistry {
9
+ get(id: string): NativeWorkspace | undefined;
10
+ list(): NativeWorkspace[];
11
+ }
12
+ export interface WorkspaceBindingOptions {
13
+ profileDir: string;
14
+ metadataMaxBytes: number;
15
+ api: Pick<ConnectApi, 'getConnectionState' | 'getSshTarget' | 'listProjects' | 'createProject' | 'ensureReady'>;
16
+ registry: NativeWorkspaceRegistry;
17
+ readHeader: (sessionId: string) => Promise<{
18
+ id: string;
19
+ cwd?: string;
20
+ } | undefined>;
21
+ nextRevision?: () => number;
22
+ onState?: (state: WorkspaceBindingState) => void;
23
+ diagnostic?: (error: unknown) => void;
24
+ }
25
+ export declare function createWorkspaceBindings(options: WorkspaceBindingOptions): {
26
+ observeConnection(state: ConnectionState): void;
27
+ dispose(): Promise<unknown>;
28
+ getWorkspaceBindingState: () => WorkspaceBindingState;
29
+ onWorkspaceBindingState(listener: (state: WorkspaceBindingState) => void): () => void;
30
+ acknowledgeWorkspaceNotice: (receipt: WorkspaceNoticeReceipt) => Promise<Result<void>>;
31
+ resolveSessionProject: (sessionId: string, use: WorkspaceUse) => Promise<Result<SessionProjectTarget>>;
32
+ };