@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
package/lib/login.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { Provider, Result, SshTarget } from './api.ts';
2
+ export interface ProxyLease {
3
+ url: string;
4
+ username: string;
5
+ password: string;
6
+ closed?: Promise<void>;
7
+ release(): Promise<void>;
8
+ }
9
+ export interface LoginBoot {
10
+ provider: Provider;
11
+ url: string;
12
+ proxy: Pick<ProxyLease, 'url' | 'username' | 'password'>;
13
+ }
14
+ export interface RemoteLogin {
15
+ ownership?: Promise<{
16
+ pid: number;
17
+ group: number;
18
+ provider: string;
19
+ }>;
20
+ url: Promise<Result<string>>;
21
+ completion: Promise<Result<void>>;
22
+ writeCode(code: string): Promise<void>;
23
+ close(): Promise<void>;
24
+ }
25
+ export interface LoginWindow {
26
+ closed: Promise<void>;
27
+ close(): Promise<void>;
28
+ }
29
+ export interface LoginPorts {
30
+ start(t: SshTarget, p: Provider, s: AbortSignal): Promise<RemoteLogin>;
31
+ acquireProxy(t: SshTarget, s: AbortSignal): Promise<ProxyLease>;
32
+ open(boot: LoginBoot, onCode: (code: string) => Promise<void>, s: AbortSignal): Promise<LoginWindow>;
33
+ }
34
+ export declare function extractLoginUrl(provider: Provider, text: string): string | undefined;
35
+ export declare function runLogin(t: SshTarget, p: Provider, ports: LoginPorts, parentSignal: AbortSignal): Promise<Result<void>>;
package/lib/login.js ADDED
@@ -0,0 +1,81 @@
1
+ import { createRequire } from 'node:module';
2
+ const { isClaudeAuthorizeUrl, validateClaudePaste } = createRequire(import.meta.url)('../helper/claude-callback.cjs');
3
+ import { fail, NeedError, need } from "./needs.js";
4
+ export function extractLoginUrl(provider, text) {
5
+ for (const match of text.matchAll(/https:\/\/[^\s<>"']+/g)) {
6
+ try {
7
+ const u = new URL(match[0]);
8
+ if (!u.username && !u.password && u.protocol === 'https:' && ((provider === 'codex' && u.origin === 'https://auth.openai.com' && u.pathname === '/oauth/authorize') || (provider === 'claude' && isClaudeAuthorizeUrl(u))))
9
+ return u.href;
10
+ }
11
+ catch { /* not a complete URL yet */ }
12
+ }
13
+ return undefined;
14
+ }
15
+ export async function runLogin(t, p, ports, parentSignal) {
16
+ const controller = new AbortController(), signal = controller.signal;
17
+ const parentAbort = () => controller.abort();
18
+ parentSignal.addEventListener('abort', parentAbort, { once: true });
19
+ if (parentSignal.aborted)
20
+ controller.abort();
21
+ let remote, lease, window, remove = () => { };
22
+ let result = fail('LOGIN_FAILED'), leaseLost = false, releasing = false;
23
+ try {
24
+ if (signal.aborted)
25
+ return result;
26
+ const cancelled = new Promise(resolve => { const abort = () => resolve(fail('LOGIN_FAILED')); signal.addEventListener('abort', abort, { once: true }); remove = () => signal.removeEventListener('abort', abort); });
27
+ remote = await ports.start(t, p, signal);
28
+ const url = await Promise.race([remote.url, cancelled]);
29
+ if (!url.ok)
30
+ return url;
31
+ if (typeof url.value !== 'string' || extractLoginUrl(p, url.value) !== url.value)
32
+ return result;
33
+ lease = await ports.acquireProxy(t, signal);
34
+ const lost = () => { if (!releasing) {
35
+ leaseLost = true;
36
+ controller.abort();
37
+ } };
38
+ void lease.closed?.then(lost, lost);
39
+ if (signal.aborted)
40
+ throw new NeedError(need(leaseLost ? 'CONNECTION_LOST' : 'LOGIN_FAILED'));
41
+ let codeSent = false, codeError = () => { };
42
+ const failedCode = new Promise(r => codeError = r);
43
+ window = await ports.open({ provider: p, url: url.value, proxy: { url: lease.url, username: lease.username, password: lease.password } }, async (code) => {
44
+ if (p !== 'claude' || codeSent || signal.aborted || !validateClaudePaste(url.value, code)) {
45
+ codeError(fail('LOGIN_FAILED'));
46
+ return;
47
+ }
48
+ codeSent = true;
49
+ try {
50
+ await remote.writeCode(code);
51
+ }
52
+ catch {
53
+ codeError(fail('LOGIN_FAILED'));
54
+ }
55
+ }, signal);
56
+ result = await Promise.race([remote.completion, window.closed.then(() => fail('LOGIN_FAILED')), failedCode, cancelled]);
57
+ }
58
+ catch (error) {
59
+ result = error instanceof NeedError ? { ok: false, error: error.need } : fail('LOGIN_FAILED');
60
+ }
61
+ finally {
62
+ remove();
63
+ parentSignal.removeEventListener('abort', parentAbort);
64
+ // Attempt every cleanup even when one resource fails; surface failure instead of success.
65
+ const cleanup = await Promise.allSettled([window?.close(), remote?.close()]);
66
+ if (cleanup.some(r => r.status === 'rejected'))
67
+ result = fail('LOGIN_FAILED');
68
+ releasing = true;
69
+ if (lease) {
70
+ try {
71
+ await lease.release();
72
+ }
73
+ catch (error) {
74
+ result = error instanceof NeedError ? { ok: false, error: error.need } : fail('LOGIN_FAILED');
75
+ }
76
+ }
77
+ if (leaseLost)
78
+ result = fail('CONNECTION_LOST');
79
+ }
80
+ return result;
81
+ }
@@ -0,0 +1,11 @@
1
+ export interface DirectoryPicker {
2
+ capability(): {
3
+ kind: string;
4
+ pick?: (signal: AbortSignal) => Promise<string | null>;
5
+ };
6
+ }
7
+ interface NativeExports {
8
+ pickNativeDirectory?: (signal: AbortSignal) => Promise<string | null>;
9
+ }
10
+ export declare function createNativeImportPicker(service: () => DirectoryPicker | undefined, loadNative?: () => Promise<NativeExports>): (signal: AbortSignal) => Promise<string | null>;
11
+ export {};
@@ -0,0 +1,49 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { NeedError, need } from "./needs.js";
10
+ const officialModule = '@deepseek-ai/dsh-host-directory-picker-native';
11
+ const loadOfficial = async () => import(__rewriteRelativeImportExtension(officialModule));
12
+ const unavailable = (requires, officialReason) => new NeedError({
13
+ ...need('DEPENDENCY_REQUIRED'), requires, ...(officialReason ? { officialReason } : {}),
14
+ });
15
+ // L的导入入口固定使用系统目录窗口,不修改DSH自动选择器或工作区流程。
16
+ // 本地原生能力直接消费;SSH启动等上下文选择browse时,调用同一官方
17
+ // 原生后端的公开函数。该分支在调用前确定,失败后绝不换后端或重试。
18
+ export function createNativeImportPicker(service, loadNative = loadOfficial) {
19
+ return async (signal) => {
20
+ const capability = service()?.capability();
21
+ let pick;
22
+ if (capability?.kind === 'native') {
23
+ if (typeof capability.pick !== 'function')
24
+ throw unavailable('官方原生目录选择服务的pick方法');
25
+ pick = signal => capability.pick(signal);
26
+ }
27
+ else {
28
+ let native;
29
+ try {
30
+ native = await loadNative();
31
+ }
32
+ catch {
33
+ throw unavailable('已安装官方原生目录选择组件 ' + officialModule);
34
+ }
35
+ if (typeof native.pickNativeDirectory !== 'function')
36
+ throw unavailable('官方原生目录选择组件的pickNativeDirectory公开导出');
37
+ pick = signal => native.pickNativeDirectory(signal);
38
+ }
39
+ try {
40
+ return await pick(signal);
41
+ }
42
+ catch (error) {
43
+ const code = error?.code;
44
+ const officialReason = typeof code === 'number' && Number.isSafeInteger(code) ? 'EXIT_' + code :
45
+ typeof code === 'string' && ['EACCES', 'EPERM', 'ENOENT', 'ENOTDIR', 'ECANCELED', 'ABORT_ERR'].includes(code) ? code : undefined;
46
+ throw unavailable('系统目录选择窗口成功返回结果;未取得结果时不会读取或提交开通资料', officialReason);
47
+ }
48
+ };
49
+ }
package/lib/needs.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { ConnectNeed, NeedCode, Result } from './api.ts';
2
+ export declare const ok: <T>(value: T) => Result<T>;
3
+ export declare const need: (code: NeedCode) => ConnectNeed;
4
+ export declare const fail: <T = never>(code: NeedCode) => Result<T>;
5
+ export declare function dependency(requires: string): Result<never>;
6
+ export declare class NeedError extends Error {
7
+ readonly need: ConnectNeed;
8
+ constructor(value: ConnectNeed);
9
+ }
package/lib/needs.js ADDED
@@ -0,0 +1,52 @@
1
+ const entries = {
2
+ ACTIVATION_CONFLICT: { layer: 'network', message: '已有另一项开通正在进行,已停止重复开通,请联系管理员。', requires: '唯一的开通请求', actor: 'administrator', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
3
+ ACTIVATION_CANCELLED: { layer: 'network', message: '本次网络开通已取消。', requires: '完成明确的开通操作', actor: 'plugin', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
4
+ ENROLLMENT_CLEANUP_UNBOUND: { layer: 'network', message: '连接资料与已保存的入网身份不匹配,已停止清理,请联系管理员。', requires: '匹配的入网身份与原始连接资料', actor: 'administrator', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
5
+ ENROLLMENT_CLEANUP_FAILED: { layer: 'network', message: '入网身份已保存;连接资料清理未完成,请联系管理员。', requires: '精确清理已消费的连接资料', actor: 'administrator', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
6
+ ENROLLMENT_MISMATCH: { layer: 'network', message: '当前连接资料与已登记的入网身份不符,请联系管理员。', requires: '一致的连接资料与入网身份', actor: 'administrator', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
7
+ LOCAL_STATE_INVALID: { layer: 'network', message: '本机网络身份记录无法核实,已停止开通,请联系管理员。', requires: '完整可信的本机网络身份记录', actor: 'administrator', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
8
+ ALREADY_RUNNING: { layer: 'network', message: '网络组件已在运行,无法确认其归属,请联系管理员。', requires: '唯一且归属明确的网络组件', actor: 'administrator', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
9
+ ENROLLMENT_RECEIPT_REQUIRED: { layer: 'network', message: '缺少可核验的入网回执,请联系管理员。', requires: '持久且匹配的入网回执', actor: 'administrator', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
10
+ CONTROL_PROTOCOL_INVALID: { layer: 'network', message: '网络组件接口不匹配,已停止操作,请联系管理员。', requires: '匹配版本的网络组件接口', actor: 'administrator', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
11
+ HELPER_HANDOFF_FAILED: { layer: 'network', message: '网络组件交接未完成,请联系管理员。', requires: '完成网络组件交接', actor: 'administrator', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
12
+ HELPER_SHUTDOWN_FORCED: { layer: 'network', message: '网络组件未能正常退出,请联系管理员核实状态。', requires: '核实网络组件退出状态', actor: 'administrator', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
13
+ ENROLLMENT_RESULT_UNKNOWN: { layer: 'enrollment', message: '上次开通结果无法确认,请联系管理员核实。已停止再次提交开通资料。', requires: '管理员核实上次开通结果', actor: 'administrator', action: { id: 'check-enrollment-result', label: '联系管理员', available: false } },
14
+ ENROLLMENT_REJECTED: { layer: 'enrollment', message: '开通资料已失效,请联系管理员处理。已停止再次提交。', requires: '管理员处理已失效的设备身份', actor: 'administrator', action: { id: 'check-enrollment-result', label: '联系管理员', available: false } },
15
+ ENROLLMENT_REQUIRED: { layer: 'enrollment', message: '我需要开通链接,请联系管理员发放。', requires: '开通文件', actor: 'administrator', action: { id: 'request-enrollment', label: '联系管理员开通', available: false } },
16
+ ENROLLMENT_INVALID: { layer: 'enrollment', message: '开通文件无效,我需要管理员重新发放。', requires: '有效开通文件', actor: 'administrator', action: { id: 'request-enrollment', label: '重新开通', available: false } },
17
+ NETWORK_KEY_INVALID: { layer: 'network', message: '员工开通已失效,请联系管理员重新发放开通链接。', requires: '新的开通链接', actor: 'administrator', action: { id: 'request-enrollment', label: '联系管理员', available: false } },
18
+ HOST_OFFLINE: { layer: 'network', message: '主机离线,请联系管理员;最后在线时间未提供时为未知。', requires: '在线的公司主机', actor: 'administrator', action: { id: 'contact-administrator', label: '联系管理员', available: false } },
19
+ EMPLOYEE_MISSING: { layer: 'ssh', message: '主机上没有你的员工账号,需要管理员运行开通。', requires: '员工主机账号', actor: 'administrator', action: { id: 'provision-employee', label: '管理员开通账号', available: false } },
20
+ SSH_FAILED: { layer: 'ssh', message: '远程主机连接暂时连不上,请联系管理员。', requires: '可用且身份匹配的员工 SSH 连接', actor: 'administrator', action: { id: 'inspect-ssh', label: '检查主机连接', available: false } },
21
+ LOGIN_PROMPT_INVALID: { layer: 'auth', message: '这个登录询问已经更新,请使用当前提示。', requires: '当前登录询问', actor: 'employee', action: { id: 'current-login-prompt', label: '查看当前提示', available: false } },
22
+ LOGIN_REQUIRED: { layer: 'auth', message: '需要登录,请到设置 → 远程主机连接 点登录', requires: 'AI 账号授权', actor: 'employee', action: { id: 'open-login', label: '前往远程主机连接设置', available: true } },
23
+ POLICY_DENIED: { layer: 'auth', message: '公司的 AI 席位设置不允许此操作,需要 AI 账号管理员开启。', requires: '允许此操作的 AI 席位设置', actor: 'ai-administrator', action: { id: 'contact-ai-administrator', label: '联系 AI 账号管理员', available: false } },
24
+ CONNECTION_LOST: { layer: 'network', message: '连接中断,正在等待网络恢复信号,恢复后将重新连接。', requires: '网络恢复', actor: 'plugin', action: { id: 'await-network', label: '等待网络恢复', available: false } },
25
+ DEPENDENCY_REQUIRED: { layer: 'integration', message: '远程主机连接的连接组件没有装好,请联系管理员。', requires: '提供方契约与适配器', actor: 'administrator', action: { id: 'supply-dependency', label: '提供集成前置条件', available: false } },
26
+ SETTINGS_REQUIRED: { layer: 'settings', message: '远程主机连接还没有设置好,请联系管理员。', requires: '已确认的连接设置', actor: 'administrator', action: { id: 'configure-settings', label: '确认具名设置', available: false } },
27
+ AUTH_STATUS_INVALID: { layer: 'auth', message: '暂时无法确认登录状态,请联系管理员。', requires: '有效的官方 CLI 登录状态', actor: 'administrator', action: { id: 'inspect-auth-status', label: '检查官方 CLI', available: false } },
28
+ LOGIN_FAILED: { layer: 'auth', message: '登录未完成,窗口已关闭或助手失败,可以重新授权。', requires: '完成 AI 授权', actor: 'employee', action: { id: 'open-login', label: '重新授权', available: true } },
29
+ HOST_TRUST_REQUIRED: { layer: 'ssh', message: '远程主机连接还缺少可信的连接信息,请联系管理员。', requires: 'host.knownHosts', actor: 'administrator', action: { id: 'supply-host-trust', label: '提供主机信任材料', available: false } },
30
+ HOST_KEY_MISMATCH: { layer: 'ssh', message: '主机身份与已开通的信任材料不符,需要管理员检查。', requires: '匹配的主机公钥', actor: 'administrator', action: { id: 'verify-host-trust', label: '检查主机身份', available: false } },
31
+ PROJECT_NAME_INVALID: { layer: 'projects', message: '工作区名必须是单个目录名称,不能是路径、点目录或控制字符。', requires: '有效工作区名', actor: 'employee', action: { id: 'rename-project', label: '修改工作区名', available: true } },
32
+ PROJECT_ROOT_INVALID: { layer: 'projects', message: '工作区暂时无法打开,请联系管理员。', requires: 'S 开通的 700 工作区根目录', actor: 'administrator', action: { id: 'check-project-root', label: '检查开通目录', available: false } },
33
+ PROJECT_EXISTS: { layer: 'projects', message: '同名工作区或文件已经存在,请换一个工作区名。', requires: '未被占用的工作区名', actor: 'employee', action: { id: 'rename-project', label: '修改工作区名', available: true } },
34
+ PROJECT_INIT_FAILED: { layer: 'projects', message: '工作区创建没有完成,已保留现有文件,请联系管理员。', requires: '可用的主机 Git 与目录检查', actor: 'administrator', action: { id: 'inspect-project-init', label: '检查 Git 初始化', available: false } },
35
+ PROJECT_CREATE_FAILED: { layer: 'projects', message: '工作区创建没有完成,请联系管理员。', requires: '可用的工作区目录与 Git', actor: 'administrator', action: { id: 'inspect-project-create', label: '检查新建工作区', available: false } },
36
+ PROJECT_LIST_FAILED: { layer: 'projects', message: '工作区列表暂时打不开,请联系管理员。', requires: '可读的工作区目录与 Git', actor: 'administrator', action: { id: 'inspect-project-list', label: '检查工作区目录', available: false } },
37
+ PROJECT_PROTOCOL_INVALID: { layer: 'projects', message: '工作区暂时无法打开,已停止操作,请联系管理员。', requires: '匹配版本的工作区服务', actor: 'administrator', action: { id: 'inspect-project-protocol', label: '检查工作区接口', available: false } },
38
+ PROJECT_BINDING_INVALID: { layer: 'projects', message: '工作区关联与当前目录或主机身份不符,已停止操作,请联系管理员。', requires: '原工作区与可信主机身份', actor: 'administrator', action: { id: 'check-workspace-binding', label: '联系管理员', available: false } },
39
+ PROJECT_BINDING_BUSY: { layer: 'projects', message: '此工作区正在关联或说明正在投递,请联系管理员核实。', requires: '完成当前关联或投递', actor: 'administrator', action: { id: 'check-workspace-binding', label: '联系管理员', available: false } },
40
+ PROJECT_CREATE_UNKNOWN: { layer: 'projects', message: '远程工作区创建结果无法确认,已保留现场,请联系管理员。', requires: '核实上次远程工作区创建结果', actor: 'administrator', action: { id: 'check-workspace-create', label: '联系管理员', available: false } },
41
+ PROJECT_NOTICE_DELIVERY_UNKNOWN: { layer: 'projects', message: '工作区说明的投递结果无法确认,已停止远程派发,请联系管理员核实。', requires: '官方会话回复的持久投递证据', actor: 'administrator', action: { id: 'check-workspace-notice', label: '联系管理员', available: false } },
42
+ PROJECT_BINDING_REQUIRED: { layer: 'projects', message: '请在会话中选择已关联的远程工作区。', requires: '当前会话的可信远程工作区关联', actor: 'employee', action: { id: 'select-workspace', label: '选择工作区', available: true } },
43
+ DISPOSED: { layer: 'integration', message: '连接插件已卸载,需要重新启用插件。', requires: '已启用的连接插件', actor: 'employee', action: { id: 'enable-plugin', label: '启用插件', available: false } },
44
+ };
45
+ export const ok = (value) => ({ ok: true, value });
46
+ export const need = (code) => structuredClone({ code, ...entries[code] });
47
+ export const fail = (code) => ({ ok: false, error: need(code) });
48
+ export function dependency(requires) { const error = need('DEPENDENCY_REQUIRED'); error.requires = requires; error.message = '远程主机连接的连接组件没有装好,请联系管理员。'; return { ok: false, error }; }
49
+ export class NeedError extends Error {
50
+ need;
51
+ constructor(value) { super(value.code); this.need = value; }
52
+ }
@@ -0,0 +1,26 @@
1
+ import type { Enrollment, NetworkEvent } from './ports.ts';
2
+ import type { Result } from './api.ts';
3
+ import type { ProxyLease } from './login.ts';
4
+ export interface NetworkSettings {
5
+ executable?: string;
6
+ runtimeDir?: string;
7
+ stateDir?: string;
8
+ proxyListen?: string;
9
+ logUpload?: boolean;
10
+ statusRefreshIntervalSeconds?: number;
11
+ dialTimeoutSeconds?: number;
12
+ headerTimeoutSeconds?: number;
13
+ enrollmentObservationSeconds?: number;
14
+ httpMaxHeaderBytes?: number;
15
+ httpMaxResponseHeaderBytes?: number;
16
+ controlMaxBytes?: number;
17
+ }
18
+ export declare function writeNetworkConfig(file: string, e: Enrollment, s: NetworkSettings): Promise<Result<void>>;
19
+ export declare function createNetworkClient(s: NetworkSettings): {
20
+ connect(e: Enrollment, signal: AbortSignal): Promise<Result<{
21
+ proxyCommand: string;
22
+ }>>;
23
+ onState(listener: (event: NetworkEvent) => void): () => void;
24
+ acquireProxy(signal: AbortSignal): Promise<ProxyLease>;
25
+ dispose(): void;
26
+ };
package/lib/network.js ADDED
@@ -0,0 +1,226 @@
1
+ import { request } from 'node:http';
2
+ import { lstat, writeFile, rename, rm } from 'node:fs/promises';
3
+ import { join, dirname, isAbsolute } from 'node:path';
4
+ import { hostname } from 'node:os';
5
+ import { StringDecoder } from 'node:string_decoder';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { need, dependency, ok, fail, NeedError } from "./needs.js";
8
+ import { privateDirectory } from "./private-files.js";
9
+ import { proxyCommand } from "./ssh.js";
10
+ function valid(s) { return [s.executable, s.runtimeDir, s.stateDir].every(p => typeof p === 'string' && isAbsolute(p) && !/[\x00-\x1f\x7f]/.test(p)) && typeof s.proxyListen === 'string' && /^127\.0\.0\.1:(?:0|[1-9][0-9]*)$/.test(s.proxyListen) && Number(s.proxyListen.split(':')[1]) <= 65535 && s.logUpload === false && [s.statusRefreshIntervalSeconds, s.dialTimeoutSeconds, s.headerTimeoutSeconds, s.enrollmentObservationSeconds].every(n => typeof n === 'number' && Number.isFinite(n) && n >= 1e-9 && n <= 9223372036) && [s.statusRefreshIntervalSeconds, s.headerTimeoutSeconds].every(n => Number.isInteger(n * 1000) && n * 1000 >= 1 && n * 1000 <= 2147483647) && [s.httpMaxHeaderBytes, s.httpMaxResponseHeaderBytes].every(n => Number.isSafeInteger(n) && n > 0) && Number.isSafeInteger(s.controlMaxBytes) && s.controlMaxBytes > 0 && s.controlMaxBytes <= 2147483647; }
11
+ function mapped(code) { return { NEED_ENROLLMENT: 'ENROLLMENT_REQUIRED', ENROLLMENT_REJECTED: 'NETWORK_KEY_INVALID', ENROLLMENT_RESULT_UNKNOWN: 'ENROLLMENT_INVALID', LOCAL_STATE_INVALID: 'ENROLLMENT_INVALID', HOST_OFFLINE: 'HOST_OFFLINE', HOST_NOT_VISIBLE: 'DEPENDENCY_REQUIRED', CONFIG_INVALID: 'SETTINGS_REQUIRED', CAPABILITY_NOT_IMPLEMENTED: 'DEPENDENCY_REQUIRED', NEED_HOST_CONTRACT: 'DEPENDENCY_REQUIRED', NETWORK_INTERRUPTED: 'CONNECTION_LOST', CONTROL_UNAVAILABLE: 'DEPENDENCY_REQUIRED' }[String(code)] ?? 'DEPENDENCY_REQUIRED'; }
12
+ export async function writeNetworkConfig(file, e, s) {
13
+ if (!valid(s) || !isAbsolute(file))
14
+ return fail('SETTINGS_REQUIRED');
15
+ let temp;
16
+ try {
17
+ await privateDirectory(dirname(file));
18
+ const config = { apiVersion: 1, stateDir: s.stateDir, runtimeDir: s.runtimeDir, nodeName: `aw-${e.username}-${hostname()}-${process.getuid()}`, ...(e.authKeyFile ? { authKeyFile: e.authKeyFile } : {}), host: { name: e.host, sshPort: e.port, employee: e.username, knownHostsFile: e.knownHostsFile }, proxyListen: s.proxyListen, statusRefreshIntervalSeconds: s.statusRefreshIntervalSeconds, dialTimeoutSeconds: s.dialTimeoutSeconds, headerTimeoutSeconds: s.headerTimeoutSeconds, enrollmentObservationSeconds: s.enrollmentObservationSeconds, logUpload: s.logUpload, httpMaxHeaderBytes: s.httpMaxHeaderBytes, httpMaxResponseHeaderBytes: s.httpMaxResponseHeaderBytes };
19
+ temp = join(dirname(file), '.network-' + randomUUID());
20
+ await writeFile(temp, JSON.stringify(config), { flag: 'wx', mode: 0o600 });
21
+ await rename(temp, file);
22
+ temp = undefined;
23
+ return ok(undefined);
24
+ }
25
+ catch {
26
+ return dependency('可写的私有 N 配置目录');
27
+ }
28
+ finally {
29
+ if (temp)
30
+ await rm(temp, { force: true });
31
+ }
32
+ }
33
+ export function createNetworkClient(s) {
34
+ const listeners = new Set();
35
+ let timer, previous, refreshing = false, disposed = false;
36
+ const pending = new Set();
37
+ let expected;
38
+ const bad = (code) => { throw new NeedError(need(code)); };
39
+ async function socket() {
40
+ if (!valid(s))
41
+ bad('SETTINGS_REQUIRED');
42
+ const dir = await lstat(s.runtimeDir), sock = await lstat(join(s.runtimeDir, 'control.sock'));
43
+ if (!dir.isDirectory() || dir.isSymbolicLink() || dir.uid !== process.getuid() || (dir.mode & 0o077) || !sock.isSocket() || sock.isSymbolicLink() || sock.uid !== process.getuid() || (sock.mode & 0o177))
44
+ bad('DEPENDENCY_REQUIRED');
45
+ }
46
+ async function send(method, path, signal) {
47
+ try {
48
+ await socket();
49
+ }
50
+ catch (e) {
51
+ if (e instanceof NeedError)
52
+ throw e;
53
+ bad('DEPENDENCY_REQUIRED');
54
+ }
55
+ if (disposed || signal.aborted)
56
+ bad('CONNECTION_LOST');
57
+ return new Promise((resolve, reject) => { const req = request({ socketPath: join(s.runtimeDir, 'control.sock'), path, method, signal, agent: false }, response => { clearTimeout(deadline); resolve(response); }); const deadline = setTimeout(() => req.destroy(new Error('CONTROL_HEADER_TIMEOUT')), s.headerTimeoutSeconds * 1000); req.on('error', () => { clearTimeout(deadline); reject(new NeedError(need('DEPENDENCY_REQUIRED'))); }); req.end(); });
58
+ }
59
+ async function json(res) { let size = 0; const chunks = []; try {
60
+ for await (const chunk of res) {
61
+ size += chunk.length;
62
+ if (size > s.controlMaxBytes)
63
+ bad('DEPENDENCY_REQUIRED');
64
+ chunks.push(chunk);
65
+ }
66
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
67
+ }
68
+ catch {
69
+ res.destroy();
70
+ bad('DEPENDENCY_REQUIRED');
71
+ } }
72
+ async function status(signal) {
73
+ try {
74
+ const res = await send('GET', '/v1/status', signal), v = await json(res);
75
+ if (v?.apiVersion !== 1)
76
+ return dependency('N v1 状态协议');
77
+ if (res.statusCode !== 200 || v.error) {
78
+ const result = fail(mapped(v.error?.code));
79
+ if (!result.ok && result.error.code === 'HOST_OFFLINE' && v.host?.name === expected?.host && typeof v.host.lastSeenAt === 'string' && !Number.isNaN(Date.parse(v.host.lastSeenAt)))
80
+ result.error.lastSeen = v.host.lastSeenAt;
81
+ return result;
82
+ }
83
+ if (v.enrollment !== 'online')
84
+ return fail(v.enrollment === 'needs_enrollment' ? 'ENROLLMENT_REQUIRED' : v.enrollment === 'rejected' ? 'NETWORK_KEY_INVALID' : 'CONNECTION_LOST');
85
+ if (v.host?.name !== expected?.host)
86
+ return dependency('与开通主机匹配的 N 状态');
87
+ if (v.host.online === false) {
88
+ const result = fail('HOST_OFFLINE');
89
+ if (!result.ok && typeof v.host.lastSeenAt === 'string' && !Number.isNaN(Date.parse(v.host.lastSeenAt)))
90
+ result.error.lastSeen = v.host.lastSeenAt;
91
+ return result;
92
+ }
93
+ if (v.host.online !== true)
94
+ return dependency('N 已核实的主机在线状态');
95
+ return ok(undefined);
96
+ }
97
+ catch (e) {
98
+ return e instanceof NeedError ? { ok: false, error: e.need } : dependency('N 私有控制 socket;未使用直连网络');
99
+ }
100
+ }
101
+ async function refresh() { if (refreshing || !expected || disposed)
102
+ return; refreshing = true; const c = new AbortController(); pending.add(c); try {
103
+ const r = await status(c.signal), online = r.ok;
104
+ if (previous !== undefined && previous !== online)
105
+ for (const listener of listeners)
106
+ listener({ kind: online ? 'restored' : 'lost' });
107
+ previous = online;
108
+ }
109
+ finally {
110
+ pending.delete(c);
111
+ refreshing = false;
112
+ } }
113
+ return {
114
+ async connect(e, signal) {
115
+ expected = e;
116
+ const result = await status(signal);
117
+ previous = result.ok;
118
+ if (!result.ok)
119
+ return result;
120
+ try {
121
+ const exe = await lstat(s.executable);
122
+ if (!exe.isFile() || exe.isSymbolicLink() || !(exe.mode & 0o111))
123
+ return dependency('N 真实 helper 可执行程序');
124
+ return ok({ proxyCommand: proxyCommand(s.executable, s.runtimeDir) });
125
+ }
126
+ catch {
127
+ return dependency('N 真实 helper 可执行程序');
128
+ }
129
+ },
130
+ onState(listener) { listeners.add(listener); if (valid(s) && !timer) {
131
+ timer = setInterval(() => { void refresh(); }, s.statusRefreshIntervalSeconds * 1000);
132
+ timer.unref();
133
+ } return () => { listeners.delete(listener); if (!listeners.size && timer) {
134
+ clearInterval(timer);
135
+ timer = undefined;
136
+ for (const c of pending)
137
+ c.abort();
138
+ } }; },
139
+ async acquireProxy(signal) {
140
+ const res = await send('POST', '/v1/login-proxy', signal);
141
+ if (res.statusCode !== 201) {
142
+ const v = await json(res);
143
+ if (v?.apiVersion !== 1)
144
+ bad('DEPENDENCY_REQUIRED');
145
+ bad(mapped(v.error?.code));
146
+ }
147
+ if (!String(res.headers['content-type']).startsWith('application/x-ndjson')) {
148
+ res.destroy();
149
+ bad('DEPENDENCY_REQUIRED');
150
+ }
151
+ let resolveLine = () => { }, rejectLine = () => { }, rejectClosed = () => { }, resolveClosed = () => { };
152
+ let text = '', size = 0, received = false, released = false;
153
+ const decoder = new StringDecoder('utf8');
154
+ const first = new Promise((yes, no) => { resolveLine = yes; rejectLine = no; });
155
+ const closed = new Promise((yes, no) => { resolveClosed = yes; rejectClosed = no; });
156
+ void closed.catch(() => { });
157
+ const lost = () => { if (!released) {
158
+ const e = new NeedError(need('CONNECTION_LOST'));
159
+ rejectLine(e);
160
+ rejectClosed(e);
161
+ } };
162
+ res.on('error', lost);
163
+ res.on('end', lost);
164
+ res.on('close', lost);
165
+ res.on('data', (chunk) => {
166
+ size += chunk.length;
167
+ if (size > s.controlMaxBytes) {
168
+ lost();
169
+ res.destroy();
170
+ return;
171
+ }
172
+ if (received) {
173
+ if (chunk.toString().trim()) {
174
+ lost();
175
+ res.destroy();
176
+ }
177
+ return;
178
+ }
179
+ text += decoder.write(chunk);
180
+ const end = text.indexOf('\n');
181
+ if (end < 0)
182
+ return;
183
+ received = true;
184
+ try {
185
+ const v = JSON.parse(text.slice(0, end)), url = new URL(v.url);
186
+ if (v.apiVersion !== 1 || typeof v.id !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(v.id) || url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port || url.username || url.password || url.pathname !== '/' || url.search || url.hash || !['username', 'password'].every(k => typeof v[k] === 'string' && v[k].length && !/[\x00-\x1f\x7f]/.test(v[k])) || text.slice(end + 1).trim())
187
+ throw Error();
188
+ resolveLine(v);
189
+ }
190
+ catch {
191
+ rejectLine(new NeedError(need('DEPENDENCY_REQUIRED')));
192
+ res.destroy();
193
+ }
194
+ finally {
195
+ text = '';
196
+ }
197
+ });
198
+ let v;
199
+ try {
200
+ v = await first;
201
+ }
202
+ catch (e) {
203
+ res.destroy();
204
+ throw e;
205
+ }
206
+ let release;
207
+ const lease = { url: v.url, username: v.username, password: v.password, closed, release: () => release ??= (async () => { released = true; res.destroy(); try {
208
+ const c = new AbortController();
209
+ const response = await send('DELETE', '/v1/login-proxy/' + encodeURIComponent(v.id), c.signal);
210
+ response.resume();
211
+ if (![204, 404].includes(response.statusCode))
212
+ bad('DEPENDENCY_REQUIRED');
213
+ }
214
+ finally {
215
+ lease.username = '';
216
+ lease.password = '';
217
+ v = null;
218
+ resolveClosed();
219
+ } })() };
220
+ return lease;
221
+ },
222
+ dispose() { disposed = true; if (timer)
223
+ clearInterval(timer); for (const c of pending)
224
+ c.abort(); listeners.clear(); },
225
+ };
226
+ }
@@ -0,0 +1,3 @@
1
+ import * as nodeFs from 'node:fs';
2
+ export declare const createWriteStream: typeof nodeFs.createWriteStream;
3
+ export declare const open: typeof nodeFs.promises.open, lstat: typeof nodeFs.promises.lstat, readFile: typeof nodeFs.promises.readFile, writeFile: typeof nodeFs.promises.writeFile, readdir: typeof nodeFs.promises.readdir, readlink: typeof nodeFs.promises.readlink, realpath: typeof nodeFs.promises.realpath, rename: typeof nodeFs.promises.rename, rm: typeof nodeFs.promises.rm, mkdir: typeof nodeFs.promises.mkdir, symlink: typeof nodeFs.promises.symlink;
@@ -0,0 +1,7 @@
1
+ import * as nodeFs from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ // 组件归档内的.asar是待校验的物理文件,不能消费Electron的虚拟目录视图。
4
+ // 仅缓存/解压边界使用;不设置全局process.noAsar,不影响宿主模块加载。
5
+ const physical = process.versions.electron ? createRequire(import.meta.url)('original-fs') : nodeFs;
6
+ export const { createWriteStream } = physical;
7
+ export const { open, lstat, readFile, writeFile, readdir, readlink, realpath, rename, rm, mkdir, symlink } = physical.promises;
package/lib/ports.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { Provider, Result, SshTarget, SessionTarget, HostContext, Project, ProjectList } from './api.ts';
2
+ export interface Enrollment {
3
+ authKeyFile?: string;
4
+ hostContext?: HostContext;
5
+ host: string;
6
+ username: string;
7
+ port: number;
8
+ projectRoot: string;
9
+ knownHostsFile: string;
10
+ session: SessionTarget;
11
+ }
12
+ export interface CommandResult {
13
+ exitCode: number | null;
14
+ stdout: string;
15
+ stderr: string;
16
+ }
17
+ export type NetworkEvent = {
18
+ kind: 'lost';
19
+ } | {
20
+ kind: 'restored';
21
+ };
22
+ export interface Ports {
23
+ enrollment?: {
24
+ read(signal: AbortSignal): Promise<Result<Enrollment>>;
25
+ };
26
+ network?: {
27
+ connect(enrollment: Enrollment, signal: AbortSignal): Promise<Result<{
28
+ proxyCommand: string;
29
+ }>>;
30
+ onState(listener: (event: NetworkEvent) => void): () => void;
31
+ };
32
+ ssh?: {
33
+ verify(target: SshTarget, signal: AbortSignal): Promise<Result<void>>;
34
+ status(target: SshTarget, provider: Provider, signal: AbortSignal): Promise<Result<CommandResult>>;
35
+ };
36
+ projects?: {
37
+ list(t: SshTarget, s: AbortSignal): Promise<Result<ProjectList>>;
38
+ create(t: SshTarget, name: string, s: AbortSignal): Promise<Result<Project>>;
39
+ };
40
+ login?: {
41
+ run(target: SshTarget, provider: Provider, signal: AbortSignal): Promise<Result<void>>;
42
+ };
43
+ }
package/lib/ports.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare function privateDirectory(path: string): Promise<void>;
2
+ export declare function privateFile(path: string): Promise<void>;
3
+ export declare function readPrivate(path: string, maxBytes?: number): Promise<string>;
@@ -0,0 +1,41 @@
1
+ import { lstat, mkdir, open } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ export async function privateDirectory(path) {
4
+ await mkdir(path, { recursive: true, mode: 0o700 });
5
+ const s = await lstat(path);
6
+ if (!s.isDirectory() || s.isSymbolicLink() || s.uid !== process.getuid() || (s.mode & 0o077) !== 0)
7
+ throw new Error('PRIVATE_DIRECTORY_REQUIRED');
8
+ }
9
+ export async function privateFile(path) {
10
+ const s = await lstat(path);
11
+ if (!s.isFile() || s.isSymbolicLink() || s.uid !== process.getuid() || (s.mode & 0o177) !== 0)
12
+ throw new Error('PRIVATE_FILE_REQUIRED');
13
+ }
14
+ export async function readPrivate(path, maxBytes) {
15
+ const h = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
16
+ try {
17
+ const s = await h.stat();
18
+ if (!s.isFile() || s.uid !== process.getuid() || (s.mode & 0o177) !== 0)
19
+ throw new Error('PRIVATE_FILE_REQUIRED');
20
+ if (maxBytes === undefined)
21
+ return await h.readFile('utf8');
22
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 2147483647 || s.size > maxBytes)
23
+ throw new Error('PRIVATE_FILE_LIMIT');
24
+ const chunks = [];
25
+ let size = 0;
26
+ for (;;) {
27
+ const buffer = Buffer.alloc(Math.min(65536, maxBytes + 1 - size));
28
+ const { bytesRead } = await h.read(buffer, 0, buffer.length, null);
29
+ if (!bytesRead)
30
+ break;
31
+ size += bytesRead;
32
+ if (size > maxBytes)
33
+ throw new Error('PRIVATE_FILE_LIMIT');
34
+ chunks.push(buffer.subarray(0, bytesRead));
35
+ }
36
+ return Buffer.concat(chunks).toString('utf8');
37
+ }
38
+ finally {
39
+ await h.close();
40
+ }
41
+ }
@@ -0,0 +1,5 @@
1
+ import type { Provider } from './api.ts';
2
+ import type { Runner } from './ssh.ts';
3
+ import type { RemoteLogin } from './login.ts';
4
+ export declare function createRunner(maxBytes: number): Runner;
5
+ export declare function createRemoteLogin(command: string, args: string[], provider: Provider, maxBytes: number, signal: AbortSignal, protocol?: 'remote-control'): Promise<RemoteLogin>;