@zhin.js/gateway 0.0.1-bootstrap.0 → 1.1.1

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 (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/lib/config/ai-config-loader.d.ts +22 -0
  4. package/lib/config/ai-config-loader.js +96 -0
  5. package/lib/config/configuration-chat-policy.d.ts +57 -0
  6. package/lib/config/configuration-chat-policy.js +423 -0
  7. package/lib/config/configuration-patch.d.ts +16 -0
  8. package/lib/config/configuration-patch.js +105 -0
  9. package/lib/config/configuration-path.d.ts +6 -0
  10. package/lib/config/configuration-path.js +40 -0
  11. package/lib/config/configuration-projection.d.ts +6 -0
  12. package/lib/config/configuration-projection.js +28 -0
  13. package/lib/config/configuration-redaction.d.ts +24 -0
  14. package/lib/config/configuration-redaction.js +128 -0
  15. package/lib/config/configuration.d.ts +81 -0
  16. package/lib/config/configuration.js +686 -0
  17. package/lib/config/console-schema.d.ts +11 -0
  18. package/lib/config/console-schema.js +222 -0
  19. package/lib/config/environment-files.d.ts +2 -0
  20. package/lib/config/environment-files.js +21 -0
  21. package/lib/config/plugin-package-map.d.ts +2 -0
  22. package/lib/config/plugin-package-map.js +14 -0
  23. package/lib/config/plugin-schema-catalog.d.ts +22 -0
  24. package/lib/config/plugin-schema-catalog.js +262 -0
  25. package/lib/config.d.ts +12 -0
  26. package/lib/config.js +12 -0
  27. package/lib/database/config.d.ts +22 -0
  28. package/lib/database/config.js +159 -0
  29. package/lib/database/index.d.ts +2 -0
  30. package/lib/database/index.js +2 -0
  31. package/lib/database/project-database.d.ts +31 -0
  32. package/lib/database/project-database.js +130 -0
  33. package/lib/database.d.ts +2 -0
  34. package/lib/database.js +2 -0
  35. package/lib/http/console-rpc.d.ts +19 -0
  36. package/lib/http/console-rpc.js +51 -0
  37. package/lib/http/gateway-console.d.ts +63 -0
  38. package/lib/http/gateway-console.js +309 -0
  39. package/lib/http/gateway-events.d.ts +35 -0
  40. package/lib/http/gateway-events.js +172 -0
  41. package/lib/http/gateway-http-server.d.ts +95 -0
  42. package/lib/http/gateway-http-server.js +518 -0
  43. package/lib/http/local-principal.d.ts +2 -0
  44. package/lib/http/local-principal.js +16 -0
  45. package/lib/http/run-gateway.d.ts +43 -0
  46. package/lib/http/run-gateway.js +268 -0
  47. package/lib/http/start-gateway.d.ts +114 -0
  48. package/lib/http/start-gateway.js +220 -0
  49. package/lib/http-response.d.ts +6 -0
  50. package/lib/http-response.js +25 -0
  51. package/lib/http.d.ts +6 -0
  52. package/lib/http.js +6 -0
  53. package/lib/iam/crypto.d.ts +12 -0
  54. package/lib/iam/crypto.js +58 -0
  55. package/lib/iam/index.d.ts +8 -0
  56. package/lib/iam/index.js +8 -0
  57. package/lib/iam/install.d.ts +36 -0
  58. package/lib/iam/install.js +74 -0
  59. package/lib/iam/local-admin.d.ts +27 -0
  60. package/lib/iam/local-admin.js +102 -0
  61. package/lib/iam/rate-limit.d.ts +15 -0
  62. package/lib/iam/rate-limit.js +52 -0
  63. package/lib/iam/routes.d.ts +23 -0
  64. package/lib/iam/routes.js +157 -0
  65. package/lib/iam/rpc.d.ts +14 -0
  66. package/lib/iam/rpc.js +100 -0
  67. package/lib/iam/service.d.ts +203 -0
  68. package/lib/iam/service.js +784 -0
  69. package/lib/iam/setup-service.d.ts +83 -0
  70. package/lib/iam/setup-service.js +206 -0
  71. package/lib/iam/tables.d.ts +20 -0
  72. package/lib/iam/tables.js +82 -0
  73. package/lib/iam.d.ts +2 -0
  74. package/lib/iam.js +2 -0
  75. package/lib/index.d.ts +22 -0
  76. package/lib/index.js +22 -0
  77. package/lib/local/client.d.ts +71 -0
  78. package/lib/local/client.js +296 -0
  79. package/lib/local/handler.d.ts +30 -0
  80. package/lib/local/handler.js +86 -0
  81. package/lib/local/local-socket.d.ts +55 -0
  82. package/lib/local/local-socket.js +232 -0
  83. package/lib/local/paths.d.ts +34 -0
  84. package/lib/local/paths.js +58 -0
  85. package/lib/local/protocol.d.ts +140 -0
  86. package/lib/local/protocol.js +90 -0
  87. package/lib/local/state-file.d.ts +8 -0
  88. package/lib/local/state-file.js +45 -0
  89. package/lib/local.d.ts +10 -0
  90. package/lib/local.js +10 -0
  91. package/lib/onboarding/allowlist.d.ts +54 -0
  92. package/lib/onboarding/allowlist.js +106 -0
  93. package/lib/onboarding/errors.d.ts +7 -0
  94. package/lib/onboarding/errors.js +17 -0
  95. package/lib/onboarding/index.d.ts +6 -0
  96. package/lib/onboarding/index.js +6 -0
  97. package/lib/onboarding/install.d.ts +99 -0
  98. package/lib/onboarding/install.js +132 -0
  99. package/lib/onboarding/package-manager.d.ts +47 -0
  100. package/lib/onboarding/package-manager.js +179 -0
  101. package/lib/onboarding/service.d.ts +53 -0
  102. package/lib/onboarding/service.js +144 -0
  103. package/lib/onboarding/state-store.d.ts +26 -0
  104. package/lib/onboarding/state-store.js +97 -0
  105. package/lib/onboarding.d.ts +2 -0
  106. package/lib/onboarding.js +2 -0
  107. package/lib/packages/authorization.d.ts +23 -0
  108. package/lib/packages/authorization.js +109 -0
  109. package/lib/packages/errors.d.ts +11 -0
  110. package/lib/packages/errors.js +26 -0
  111. package/lib/packages/index.d.ts +5 -0
  112. package/lib/packages/index.js +5 -0
  113. package/lib/packages/rpc.d.ts +30 -0
  114. package/lib/packages/rpc.js +155 -0
  115. package/lib/packages/service.d.ts +121 -0
  116. package/lib/packages/service.js +441 -0
  117. package/lib/packages/snapshot.d.ts +14 -0
  118. package/lib/packages/snapshot.js +73 -0
  119. package/lib/packages.d.ts +2 -0
  120. package/lib/packages.js +2 -0
  121. package/lib/plugin-management/activity.d.ts +6 -0
  122. package/lib/plugin-management/activity.js +14 -0
  123. package/lib/plugin-management/config-explanation.d.ts +6 -0
  124. package/lib/plugin-management/config-explanation.js +113 -0
  125. package/lib/plugin-management/use-cases.d.ts +15 -0
  126. package/lib/plugin-management/use-cases.js +120 -0
  127. package/lib/plugin-management/workflow.d.ts +92 -0
  128. package/lib/plugin-management/workflow.js +772 -0
  129. package/lib/plugin-management.d.ts +5 -0
  130. package/lib/plugin-management.js +5 -0
  131. package/lib/project/config-file.d.ts +6 -0
  132. package/lib/project/config-file.js +58 -0
  133. package/lib/project/environment.d.ts +13 -0
  134. package/lib/project/environment.js +52 -0
  135. package/lib/project/instance-declarations.d.ts +55 -0
  136. package/lib/project/instance-declarations.js +542 -0
  137. package/lib/project/legacy-config.d.ts +25 -0
  138. package/lib/project/legacy-config.js +70 -0
  139. package/lib/project/node-requirements.d.ts +7 -0
  140. package/lib/project/node-requirements.js +20 -0
  141. package/lib/project/tsx-module-loader.d.ts +7 -0
  142. package/lib/project/tsx-module-loader.js +29 -0
  143. package/lib/project.d.ts +7 -0
  144. package/lib/project.js +7 -0
  145. package/lib/server.d.ts +11 -0
  146. package/lib/server.js +11 -0
  147. package/lib/supervisor/instance-logs.d.ts +39 -0
  148. package/lib/supervisor/instance-logs.js +152 -0
  149. package/lib/supervisor/instance-supervisor.d.ts +140 -0
  150. package/lib/supervisor/instance-supervisor.js +408 -0
  151. package/lib/supervisor/run-instance.d.ts +35 -0
  152. package/lib/supervisor/run-instance.js +161 -0
  153. package/lib/supervisor/terminal-state.d.ts +30 -0
  154. package/lib/supervisor/terminal-state.js +57 -0
  155. package/lib/supervisor.d.ts +4 -0
  156. package/lib/supervisor.js +4 -0
  157. package/lib/terminal/backends.d.ts +38 -0
  158. package/lib/terminal/backends.js +193 -0
  159. package/lib/terminal/config.d.ts +63 -0
  160. package/lib/terminal/config.js +237 -0
  161. package/lib/terminal/host.d.ts +26 -0
  162. package/lib/terminal/host.js +76 -0
  163. package/lib/terminal/index.d.ts +60 -0
  164. package/lib/terminal/index.js +135 -0
  165. package/lib/terminal/modules.d.ts +87 -0
  166. package/lib/terminal/modules.js +44 -0
  167. package/lib/terminal/service.d.ts +101 -0
  168. package/lib/terminal/service.js +455 -0
  169. package/lib/terminal/tickets.d.ts +46 -0
  170. package/lib/terminal/tickets.js +76 -0
  171. package/lib/terminal.d.ts +3 -0
  172. package/lib/terminal.js +3 -0
  173. package/lib/usage/aggregate.d.ts +87 -0
  174. package/lib/usage/aggregate.js +212 -0
  175. package/lib/usage.d.ts +2 -0
  176. package/lib/usage.js +2 -0
  177. package/package.json +117 -4
@@ -0,0 +1,54 @@
1
+ import type { ConsolePrincipal } from '@zhin.js/host-http';
2
+ /** Where an allowlisted package comes from (ADR 0053 §3). */
3
+ export type InstallPackageKind = 'database-driver' | 'adapter' | 'ai' | 'market';
4
+ export interface InstallAllowlistEntry {
5
+ readonly name: string;
6
+ readonly kind: InstallPackageKind;
7
+ /** Range / dist-tag used when a request names no version (`latest` → bare name). */
8
+ readonly defaultVersion?: string;
9
+ }
10
+ /** One plugin-market entry; the catalog source (plugins.json, registry) is the caller's. */
11
+ export interface MarketCatalogEntry {
12
+ readonly name: string;
13
+ readonly version?: string;
14
+ }
15
+ export interface InstallAllowlist {
16
+ get(name: string): InstallAllowlistEntry | undefined;
17
+ entries(): readonly InstallAllowlistEntry[];
18
+ }
19
+ /** A request that passed parsing and the allowlist. `spec` is what the package manager receives. */
20
+ export interface AuthorizedPackage {
21
+ readonly name: string;
22
+ readonly kind: InstallPackageKind;
23
+ readonly version?: string;
24
+ readonly spec: string;
25
+ }
26
+ export declare function isSafeVersionSpec(version: string): boolean;
27
+ /**
28
+ * Parses `name` or `name@version` strictly. Anything else (URLs, paths, git / file / npm: aliases,
29
+ * whitespace, shell syntax) is rejected; there is no fallback interpretation.
30
+ */
31
+ export declare function parseInstallRequest(request: unknown): {
32
+ readonly name: string;
33
+ readonly version?: string;
34
+ };
35
+ export interface InstallAllowlistOptions {
36
+ /** Plugin-market entries; names that fail parsing are dropped. */
37
+ readonly market?: readonly MarketCatalogEntry[];
38
+ }
39
+ /** Official catalog: database drivers, adapter packages (+ their extra deps), AI stack, market entries. */
40
+ export declare function createInstallAllowlist(options?: InstallAllowlistOptions): InstallAllowlist;
41
+ /**
42
+ * A Console principal, or `'local'` for the CLI: the operator already has shell access to the
43
+ * project, so only the allowlist and spec rules apply.
44
+ */
45
+ export type InstallPrincipal = ConsolePrincipal | 'local';
46
+ export interface AuthorizeInstallOptions {
47
+ /** True once an owner exists; the bootstrap token is refused afterwards. */
48
+ readonly ownerExists?: boolean;
49
+ }
50
+ /**
51
+ * ADR 0053 §3: only allowlisted packages; the bootstrap token may install database drivers only
52
+ * (and only before the first owner exists); everything else needs `plugin.manage`. Deny by default.
53
+ */
54
+ export declare function authorizeInstall(packages: readonly unknown[], principal: InstallPrincipal | undefined, allowlist: InstallAllowlist, options?: AuthorizeInstallOptions): readonly AuthorizedPackage[];
@@ -0,0 +1,106 @@
1
+ import { CONSOLE_PERMISSIONS, grantsAllow } from '@zhin.js/console-protocol';
2
+ import { ADAPTERS, AI_STACK_VERSIONS, DATABASE_DIALECT_DEFINITIONS, ZHIN_STACK_VERSIONS, } from '@zhin.js/scaffold-wizard';
3
+ import { OnboardingError } from './errors.js';
4
+ const NAME = /^(?:@[a-z0-9][a-z0-9._-]{0,213}\/)?[a-z0-9][a-z0-9._-]{0,213}$/u;
5
+ // One comparator without whitespace: 1.2.3, ^1.2.3, ~1.2, >=1.0.0, 1.x, with an optional prerelease.
6
+ const SEMVER_RANGE = /^(?:[~^]|[<>]=?|=)?\d{1,9}(?:\.(?:\d{1,9}|x|\*)){0,2}(?:-[0-9A-Za-z][0-9A-Za-z.-]{0,63})?$/u;
7
+ // npm dist-tags must not look like a version or a path.
8
+ const DIST_TAG = /^[a-z][a-z0-9-]{0,31}$/u;
9
+ export function isSafeVersionSpec(version) {
10
+ return SEMVER_RANGE.test(version) || DIST_TAG.test(version) || version === '*';
11
+ }
12
+ /**
13
+ * Parses `name` or `name@version` strictly. Anything else (URLs, paths, git / file / npm: aliases,
14
+ * whitespace, shell syntax) is rejected; there is no fallback interpretation.
15
+ */
16
+ export function parseInstallRequest(request) {
17
+ if (typeof request !== 'string' || request.length === 0 || request.length > 256) {
18
+ throw new OnboardingError('invalid_input', '包名无效');
19
+ }
20
+ const at = request.indexOf('@', request.startsWith('@') ? 1 : 0);
21
+ const name = at < 0 ? request : request.slice(0, at);
22
+ const version = at < 0 ? undefined : request.slice(at + 1);
23
+ if (!NAME.test(name) || name.includes('..'))
24
+ throw new OnboardingError('invalid_input', `包名无效:${name.slice(0, 64)}`);
25
+ if (version !== undefined && !isSafeVersionSpec(version)) {
26
+ throw new OnboardingError('invalid_input', `${name} 的版本只能是 semver 范围或 dist-tag`);
27
+ }
28
+ return Object.freeze({ name, ...(version === undefined ? {} : { version }) });
29
+ }
30
+ /** Official catalog: database drivers, adapter packages (+ their extra deps), AI stack, market entries. */
31
+ export function createInstallAllowlist(options = {}) {
32
+ const map = new Map();
33
+ const add = (name, kind, version) => {
34
+ if (map.has(name) || !NAME.test(name))
35
+ return;
36
+ const defaultVersion = version && version !== 'latest' && isSafeVersionSpec(version) ? version : undefined;
37
+ map.set(name, Object.freeze({ name, kind, ...(defaultVersion ? { defaultVersion } : {}) }));
38
+ };
39
+ // Order matters: a driver stays a driver even if a later catalog also lists it.
40
+ for (const definition of DATABASE_DIALECT_DEFINITIONS) {
41
+ if (definition.driver)
42
+ add(definition.driver.package, 'database-driver', definition.driver.version);
43
+ }
44
+ const stack = ZHIN_STACK_VERSIONS;
45
+ for (const adapter of ADAPTERS) {
46
+ add(adapter.package, 'adapter', stack[adapter.package]);
47
+ for (const [name, version] of Object.entries(adapter.extraDeps ?? {}))
48
+ add(name, 'adapter', version);
49
+ }
50
+ for (const [name, version] of Object.entries(AI_STACK_VERSIONS))
51
+ add(name, 'ai', version);
52
+ // AI authoring conventions the wizard adds with the agent (collectWizardDependencies).
53
+ for (const name of ['@zhin.js/tool', '@zhin.js/prompt-section'])
54
+ add(name, 'ai', stack[name]);
55
+ for (const entry of options.market ?? []) {
56
+ if (entry && typeof entry.name === 'string')
57
+ add(entry.name, 'market', entry.version);
58
+ }
59
+ const entries = Object.freeze([...map.values()]);
60
+ return Object.freeze({
61
+ get: (name) => map.get(name),
62
+ entries: () => entries,
63
+ });
64
+ }
65
+ /**
66
+ * ADR 0053 §3: only allowlisted packages; the bootstrap token may install database drivers only
67
+ * (and only before the first owner exists); everything else needs `plugin.manage`. Deny by default.
68
+ */
69
+ export function authorizeInstall(packages, principal, allowlist, options = {}) {
70
+ if (!principal)
71
+ throw new OnboardingError('forbidden', '未登录');
72
+ if (!Array.isArray(packages) || packages.length === 0 || packages.length > 32) {
73
+ throw new OnboardingError('invalid_input', '请提供 1–32 个包');
74
+ }
75
+ const seen = new Set();
76
+ const authorized = [];
77
+ for (const request of packages) {
78
+ const { name, version } = parseInstallRequest(request);
79
+ const entry = allowlist.get(name);
80
+ if (!entry)
81
+ throw new OnboardingError('forbidden', `${name} 不在可安装目录中`);
82
+ if (seen.has(name))
83
+ throw new OnboardingError('invalid_input', `重复的包:${name}`);
84
+ seen.add(name);
85
+ if (principal === 'local') {
86
+ // Allowlist and spec checks above still apply.
87
+ }
88
+ else if (principal.kind === 'bootstrap') {
89
+ if (options.ownerExists)
90
+ throw new OnboardingError('forbidden', '已创建所有者:初始化令牌已失效');
91
+ if (entry.kind !== 'database-driver')
92
+ throw new OnboardingError('forbidden', '初始化令牌只能安装数据库驱动');
93
+ }
94
+ else if (!grantsAllow(principal.grants, CONSOLE_PERMISSIONS.PLUGIN_MANAGE)) {
95
+ throw new OnboardingError('forbidden', `安装 ${name} 需要 ${CONSOLE_PERMISSIONS.PLUGIN_MANAGE} 权限`);
96
+ }
97
+ const resolved = version ?? entry.defaultVersion;
98
+ authorized.push(Object.freeze({
99
+ name,
100
+ kind: entry.kind,
101
+ ...(resolved === undefined ? {} : { version: resolved }),
102
+ spec: resolved === undefined ? name : `${name}@${resolved}`,
103
+ }));
104
+ }
105
+ return Object.freeze(authorized);
106
+ }
@@ -0,0 +1,7 @@
1
+ export type OnboardingErrorCode = 'invalid_input' | 'forbidden' | 'conflict' | 'unavailable';
2
+ /** Transport-independent onboarding error; adapters map `status` to HTTP / RPC errors. */
3
+ export declare class OnboardingError extends Error {
4
+ readonly code: OnboardingErrorCode;
5
+ readonly status: number;
6
+ constructor(code: OnboardingErrorCode, message: string);
7
+ }
@@ -0,0 +1,17 @@
1
+ const STATUS = {
2
+ invalid_input: 400,
3
+ forbidden: 403,
4
+ conflict: 409,
5
+ unavailable: 503,
6
+ };
7
+ /** Transport-independent onboarding error; adapters map `status` to HTTP / RPC errors. */
8
+ export class OnboardingError extends Error {
9
+ code;
10
+ status;
11
+ constructor(code, message) {
12
+ super(message);
13
+ this.code = code;
14
+ this.name = 'OnboardingError';
15
+ this.status = STATUS[code];
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ export * from './errors.js';
2
+ export * from './allowlist.js';
3
+ export * from './state-store.js';
4
+ export * from './package-manager.js';
5
+ export * from './install.js';
6
+ export * from './service.js';
@@ -0,0 +1,6 @@
1
+ export * from './errors.js';
2
+ export * from './allowlist.js';
3
+ export * from './state-store.js';
4
+ export * from './package-manager.js';
5
+ export * from './install.js';
6
+ export * from './service.js';
@@ -0,0 +1,99 @@
1
+ import type { GatewayPluginConfigurationNeed } from '@zhin.js/console-protocol';
2
+ import type { AuthorizedPackage, InstallPackageKind } from './allowlist.js';
3
+ import type { PackageManagerOutput } from './package-manager.js';
4
+ /** The part of `PluginManagementWorkflow` / `PluginManagementPort` installation uses. */
5
+ export interface PackageInstallPort {
6
+ inspect?(packageName: string): Promise<{
7
+ readonly installed: boolean;
8
+ }>;
9
+ install(spec: string, expectedRevision?: string, expectedProjectRevision?: string, options?: {
10
+ readonly enable?: boolean;
11
+ }): Promise<{
12
+ readonly restartRequired: boolean;
13
+ /** Installed but left undeclared: the schema requires fields without defaults. */
14
+ readonly needsConfiguration?: GatewayPluginConfigurationNeed;
15
+ }>;
16
+ uninstall?(packageName: string): Promise<unknown>;
17
+ }
18
+ export type InstallEvent = {
19
+ readonly type: 'start';
20
+ readonly packages: readonly {
21
+ readonly name: string;
22
+ readonly spec: string;
23
+ readonly kind: InstallPackageKind;
24
+ }[];
25
+ } | {
26
+ readonly type: 'package-start';
27
+ readonly name: string;
28
+ readonly spec: string;
29
+ } | {
30
+ readonly type: 'output';
31
+ readonly name: string;
32
+ readonly stream: 'stdout' | 'stderr';
33
+ readonly chunk: string;
34
+ } | {
35
+ readonly type: 'package-done';
36
+ readonly name: string;
37
+ readonly restartRequired: boolean;
38
+ readonly needsConfiguration?: GatewayPluginConfigurationNeed;
39
+ } | {
40
+ readonly type: 'package-failed';
41
+ readonly name: string;
42
+ readonly error: string;
43
+ readonly completedStages: readonly string[];
44
+ } | {
45
+ readonly type: 'rollback';
46
+ readonly name: string;
47
+ readonly ok: boolean;
48
+ readonly error?: string;
49
+ } | {
50
+ readonly type: 'done';
51
+ readonly report: InstallReport;
52
+ };
53
+ export interface InstallReport {
54
+ readonly ok: boolean;
55
+ readonly installed: readonly string[];
56
+ readonly failed?: {
57
+ readonly name: string;
58
+ readonly error: string;
59
+ readonly completedStages: readonly string[];
60
+ };
61
+ /** Packages this batch added and then removed again after a failure. */
62
+ readonly rolledBack: readonly string[];
63
+ /** Rollback attempts that failed; those packages remain and need manual cleanup. */
64
+ readonly rollbackFailed: readonly {
65
+ readonly name: string;
66
+ readonly error: string;
67
+ }[];
68
+ /** Packages that were already installed and therefore never rolled back. */
69
+ readonly preexisting: readonly string[];
70
+ readonly restartRequired: boolean;
71
+ /**
72
+ * Plugins installed but not declared because their configuration is incomplete (e.g. an
73
+ * adapter's first account): configure and declare them next (`gateway.plugins.declare`).
74
+ */
75
+ readonly needsConfiguration?: readonly GatewayPluginConfigurationNeed[];
76
+ }
77
+ export type InstallListener = (event: InstallEvent) => void;
78
+ /** Routes package-manager output to whichever install is running (installs are serialized). */
79
+ export declare class InstallOutputRelay {
80
+ #private;
81
+ readonly output: PackageManagerOutput;
82
+ attach(listener: PackageManagerOutput): () => void;
83
+ }
84
+ export interface InstallPackagesOptions {
85
+ readonly port: PackageInstallPort;
86
+ readonly onEvent?: InstallListener;
87
+ readonly relay?: InstallOutputRelay;
88
+ /** Remove packages this batch added when a later one fails (default true). */
89
+ readonly rollback?: boolean;
90
+ }
91
+ /**
92
+ * Installs already-authorized packages one at a time through the plugin-management workflow.
93
+ * Stops at the first failure and, when possible, removes what this batch added (reported, never assumed).
94
+ */
95
+ export declare function installPackages(packages: readonly AuthorizedPackage[], options: InstallPackagesOptions): Promise<InstallReport>;
96
+ /** Adapts a listener-based run into an async iterator of its events (ends after the run settles). */
97
+ export declare function streamEvents<E, T>(run: (emit: (event: E) => void) => Promise<T>): AsyncIterableIterator<E> & {
98
+ readonly result: Promise<T>;
99
+ };
@@ -0,0 +1,132 @@
1
+ /** Routes package-manager output to whichever install is running (installs are serialized). */
2
+ export class InstallOutputRelay {
3
+ #listener;
4
+ output = (stream, chunk) => this.#listener?.(stream, chunk);
5
+ attach(listener) {
6
+ this.#listener = listener;
7
+ return () => { if (this.#listener === listener)
8
+ this.#listener = undefined; };
9
+ }
10
+ }
11
+ function message(error) {
12
+ return error instanceof Error ? error.message : String(error);
13
+ }
14
+ function completedStages(error) {
15
+ const stages = error?.completedStages;
16
+ return Array.isArray(stages) ? Object.freeze(stages.filter((stage) => typeof stage === 'string')) : Object.freeze([]);
17
+ }
18
+ /**
19
+ * Installs already-authorized packages one at a time through the plugin-management workflow.
20
+ * Stops at the first failure and, when possible, removes what this batch added (reported, never assumed).
21
+ */
22
+ export async function installPackages(packages, options) {
23
+ const emit = (event) => { try {
24
+ options.onEvent?.(event);
25
+ }
26
+ catch { /* listeners never break installs */ } };
27
+ emit({ type: 'start', packages: packages.map(({ name, spec, kind }) => ({ name, spec, kind })) });
28
+ const installed = [];
29
+ const added = [];
30
+ const preexisting = [];
31
+ let restartRequired = false;
32
+ const needsConfiguration = [];
33
+ let failed;
34
+ for (const item of packages) {
35
+ let existed;
36
+ try {
37
+ existed = options.port.inspect ? (await options.port.inspect(item.name)).installed : undefined;
38
+ }
39
+ catch {
40
+ existed = undefined;
41
+ }
42
+ if (existed)
43
+ preexisting.push(item.name);
44
+ emit({ type: 'package-start', name: item.name, spec: item.spec });
45
+ const detach = options.relay?.attach((stream, chunk) => emit({ type: 'output', name: item.name, stream, chunk }));
46
+ try {
47
+ // Drivers and AI stack packages are plain dependencies; adapters and market plugins get declared.
48
+ const enable = item.kind === 'adapter' || item.kind === 'market';
49
+ const result = await options.port.install(item.spec, undefined, undefined, { enable });
50
+ restartRequired ||= result.restartRequired;
51
+ installed.push(item.name);
52
+ // Only packages proven absent beforehand are ours to remove.
53
+ if (existed === false)
54
+ added.push(item.name);
55
+ if (result.needsConfiguration)
56
+ needsConfiguration.push(result.needsConfiguration);
57
+ emit({ type: 'package-done', name: item.name, restartRequired: result.restartRequired,
58
+ ...(result.needsConfiguration ? { needsConfiguration: result.needsConfiguration } : {}) });
59
+ }
60
+ catch (error) {
61
+ const stages = completedStages(error);
62
+ failed = Object.freeze({ name: item.name, error: message(error), completedStages: stages });
63
+ if (existed === false && stages.includes('dependency-install'))
64
+ added.push(item.name);
65
+ emit({ type: 'package-failed', ...failed });
66
+ break;
67
+ }
68
+ finally {
69
+ detach?.();
70
+ }
71
+ }
72
+ const rolledBack = [];
73
+ const rollbackFailed = [];
74
+ if (failed && options.rollback !== false) {
75
+ for (const name of [...added].reverse()) {
76
+ if (!options.port.uninstall) {
77
+ rollbackFailed.push({ name, error: 'uninstall unavailable' });
78
+ emit({ type: 'rollback', name, ok: false, error: 'uninstall unavailable' });
79
+ continue;
80
+ }
81
+ try {
82
+ await options.port.uninstall(name);
83
+ rolledBack.push(name);
84
+ emit({ type: 'rollback', name, ok: true });
85
+ }
86
+ catch (error) {
87
+ rollbackFailed.push({ name, error: message(error) });
88
+ emit({ type: 'rollback', name, ok: false, error: message(error) });
89
+ }
90
+ }
91
+ }
92
+ const report = Object.freeze({
93
+ ok: !failed,
94
+ installed: Object.freeze(installed.filter(name => !rolledBack.includes(name))),
95
+ ...(failed ? { failed } : {}),
96
+ rolledBack: Object.freeze(rolledBack),
97
+ rollbackFailed: Object.freeze(rollbackFailed.map(entry => Object.freeze(entry))),
98
+ preexisting: Object.freeze(preexisting),
99
+ restartRequired: restartRequired && installed.some(name => !rolledBack.includes(name)),
100
+ ...(() => {
101
+ const pending = needsConfiguration.filter(need => installed.includes(need.name) && !rolledBack.includes(need.name));
102
+ return pending.length ? { needsConfiguration: Object.freeze(pending) } : {};
103
+ })(),
104
+ });
105
+ emit({ type: 'done', report });
106
+ return report;
107
+ }
108
+ /** Adapts a listener-based run into an async iterator of its events (ends after the run settles). */
109
+ export function streamEvents(run) {
110
+ const queue = [];
111
+ let wake;
112
+ let settled = false;
113
+ const result = run(event => { queue.push(event); wake?.(); }).finally(() => { settled = true; wake?.(); });
114
+ result.catch(() => { });
115
+ const iterator = {
116
+ result,
117
+ async next() {
118
+ for (;;) {
119
+ if (queue.length)
120
+ return { value: queue.shift(), done: false };
121
+ if (settled) {
122
+ await result;
123
+ return { value: undefined, done: true };
124
+ }
125
+ await new Promise(resolveWake => { wake = resolveWake; });
126
+ wake = undefined;
127
+ }
128
+ },
129
+ [Symbol.asyncIterator]() { return iterator; },
130
+ };
131
+ return iterator;
132
+ }
@@ -0,0 +1,47 @@
1
+ import { spawn } from 'node:child_process';
2
+ export type PackageManagerName = 'pnpm' | 'npm' | 'yarn' | 'bun';
3
+ /** `package.json#packageManager`, then lockfiles; pnpm is the scaffold default. */
4
+ export declare function detectPackageManager(projectRoot: string): Promise<PackageManagerName>;
5
+ /**
6
+ * Maps the workflow's pnpm invocation (`add|remove [-w] [-D] <spec>`) and the gateway's
7
+ * `update [-w] [<name>…]` / `install [--frozen-lockfile]` to another manager.
8
+ * Unknown flags are refused rather than forwarded.
9
+ */
10
+ export declare function translatePackageManagerArgs(manager: PackageManagerName, args: readonly string[]): readonly string[];
11
+ /** How to spawn one command without `shell: true`. */
12
+ export interface ResolvedCommand {
13
+ readonly command: string;
14
+ readonly args: readonly string[];
15
+ /** Set for `cmd.exe /d /s /c "…"`: the command line is already quoted. */
16
+ readonly windowsVerbatimArguments?: boolean;
17
+ }
18
+ export interface ResolveCommandOptions {
19
+ readonly platform?: NodeJS.Platform;
20
+ readonly env?: NodeJS.ProcessEnv;
21
+ /** File probe; defaults to a synchronous `stat().isFile()`. */
22
+ readonly isFile?: (path: string) => boolean;
23
+ }
24
+ /** Quotes one argument for `CommandLineToArgvW`, then escapes cmd metacharacters. */
25
+ export declare function quoteWindowsCmdArgument(argument: string, doubleEscape: boolean): string;
26
+ /**
27
+ * Resolves a package-manager command for `spawn` / `execFile` without a shell. POSIX: unchanged.
28
+ * Windows: `PATH` × `PATHEXT` lookup; an `.exe` / `.com` runs directly, a `.cmd` / `.bat` shim
29
+ * (or an unresolved name, assumed to be `<name>.cmd`) runs through `cmd.exe /d /s /c` with every
30
+ * argument quoted and its metacharacters escaped, since Node refuses to spawn batch files directly.
31
+ */
32
+ export declare function resolveCommand(name: string, args: readonly string[], options?: ResolveCommandOptions): ResolvedCommand;
33
+ export type PackageManagerOutput = (stream: 'stdout' | 'stderr', chunk: string) => void;
34
+ export interface StreamingPackageManagerOptions {
35
+ /** Fixed manager; otherwise detected per project root. */
36
+ readonly manager?: PackageManagerName;
37
+ /** Injection point for tests. */
38
+ readonly spawn?: typeof spawn;
39
+ readonly env?: NodeJS.ProcessEnv;
40
+ /** Platform used for command resolution (tests). */
41
+ readonly platform?: NodeJS.Platform;
42
+ }
43
+ /**
44
+ * An `installDependencies` implementation for `PluginManagementWorkflow` that streams output.
45
+ * No shell: the command and every argument are passed as an argv array.
46
+ */
47
+ export declare function createStreamingPackageManager(output: PackageManagerOutput, options?: StreamingPackageManagerOptions): (args: readonly string[], projectRoot: string) => Promise<void>;
@@ -0,0 +1,179 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { statSync } from 'node:fs';
3
+ import { access, readFile } from 'node:fs/promises';
4
+ import { join, win32 } from 'node:path';
5
+ const LOCKFILES = [
6
+ ['pnpm-lock.yaml', 'pnpm'],
7
+ ['yarn.lock', 'yarn'],
8
+ ['bun.lockb', 'bun'],
9
+ ['bun.lock', 'bun'],
10
+ ['package-lock.json', 'npm'],
11
+ ];
12
+ /** `package.json#packageManager`, then lockfiles; pnpm is the scaffold default. */
13
+ export async function detectPackageManager(projectRoot) {
14
+ try {
15
+ const manifest = JSON.parse(await readFile(join(projectRoot, 'package.json'), 'utf8'));
16
+ const declared = typeof manifest.packageManager === 'string' ? manifest.packageManager.split('@')[0] : undefined;
17
+ if (declared === 'pnpm' || declared === 'npm' || declared === 'yarn' || declared === 'bun')
18
+ return declared;
19
+ }
20
+ catch { /* fall through to lockfiles */ }
21
+ for (const [file, name] of LOCKFILES) {
22
+ if (await access(join(projectRoot, file)).then(() => true, () => false))
23
+ return name;
24
+ }
25
+ return 'pnpm';
26
+ }
27
+ /**
28
+ * Maps the workflow's pnpm invocation (`add|remove [-w] [-D] <spec>`) and the gateway's
29
+ * `update [-w] [<name>…]` / `install [--frozen-lockfile]` to another manager.
30
+ * Unknown flags are refused rather than forwarded.
31
+ */
32
+ export function translatePackageManagerArgs(manager, args) {
33
+ const [command, ...rest] = args;
34
+ if (command === 'install') {
35
+ const frozen = rest.includes('--frozen-lockfile');
36
+ if (rest.some(arg => arg !== '--frozen-lockfile'))
37
+ throw new Error('Unsupported package-manager flag');
38
+ if (!frozen)
39
+ return ['install'];
40
+ return manager === 'npm' ? ['ci'] : ['install', '--frozen-lockfile'];
41
+ }
42
+ if (command !== 'add' && command !== 'remove' && command !== 'update')
43
+ throw new Error(`Unsupported package-manager command: ${String(command)}`);
44
+ const workspace = rest.includes('-w');
45
+ const dev = rest.includes('-D');
46
+ const specs = rest.filter(arg => arg !== '-w' && arg !== '-D');
47
+ if (specs.some(spec => spec.startsWith('-')))
48
+ throw new Error('Unsupported package-manager flag');
49
+ if (command === 'update') {
50
+ if (dev)
51
+ throw new Error('Unsupported package-manager flag');
52
+ switch (manager) {
53
+ case 'pnpm': return ['update', ...(workspace ? ['-w'] : []), ...specs];
54
+ case 'npm': return ['update', ...specs];
55
+ case 'yarn': return ['upgrade', ...specs];
56
+ case 'bun': return ['update', ...specs];
57
+ }
58
+ }
59
+ if (command !== 'add' && command !== 'remove')
60
+ throw new Error('unreachable');
61
+ switch (manager) {
62
+ case 'pnpm': return [command, ...(workspace ? ['-w'] : []), ...(dev ? ['-D'] : []), ...specs];
63
+ case 'npm': return [command === 'add' ? 'install' : 'uninstall', ...(dev ? ['--save-dev'] : []), ...specs];
64
+ case 'yarn': return [command, ...(workspace ? ['-W'] : []), ...(dev ? ['-D'] : []), ...specs];
65
+ case 'bun': return [command, ...(dev ? ['-d'] : []), ...specs];
66
+ }
67
+ }
68
+ // cmd.exe metacharacters (same set as cross-spawn).
69
+ const CMD_META = /([()\][%!^"`<>&|;, *?])/gu;
70
+ /** Quotes one argument for `CommandLineToArgvW`, then escapes cmd metacharacters. */
71
+ export function quoteWindowsCmdArgument(argument, doubleEscape) {
72
+ // Single linear pass (no backtracking regex): backslashes before a quote and trailing
73
+ // backslashes (they precede the closing quote) are doubled; quotes are escaped.
74
+ let body = '';
75
+ let backslashes = 0;
76
+ for (const char of argument) {
77
+ if (char === '\\') {
78
+ backslashes += 1;
79
+ continue;
80
+ }
81
+ if (char === '"') {
82
+ body += '\\'.repeat(backslashes * 2) + '\\"';
83
+ }
84
+ else {
85
+ body += '\\'.repeat(backslashes) + char;
86
+ }
87
+ backslashes = 0;
88
+ }
89
+ body += '\\'.repeat(backslashes * 2);
90
+ const quoted = `"${body}"`.replace(CMD_META, '^$1');
91
+ // Package-manager shims (`pnpm.cmd`) re-expand `%*`, so metacharacters are parsed twice.
92
+ return doubleEscape ? quoted.replace(CMD_META, '^$1') : quoted;
93
+ }
94
+ function envValue(env, name) {
95
+ const key = Object.keys(env).find(item => item.toUpperCase() === name);
96
+ return key === undefined ? undefined : env[key];
97
+ }
98
+ function defaultIsFile(path) {
99
+ try {
100
+ return statSync(path).isFile();
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ }
106
+ /**
107
+ * Resolves a package-manager command for `spawn` / `execFile` without a shell. POSIX: unchanged.
108
+ * Windows: `PATH` × `PATHEXT` lookup; an `.exe` / `.com` runs directly, a `.cmd` / `.bat` shim
109
+ * (or an unresolved name, assumed to be `<name>.cmd`) runs through `cmd.exe /d /s /c` with every
110
+ * argument quoted and its metacharacters escaped, since Node refuses to spawn batch files directly.
111
+ */
112
+ export function resolveCommand(name, args, options = {}) {
113
+ const platform = options.platform ?? process.platform;
114
+ if (platform !== 'win32')
115
+ return Object.freeze({ command: name, args: Object.freeze([...args]) });
116
+ const env = options.env ?? process.env;
117
+ const isFile = options.isFile ?? defaultIsFile;
118
+ const extensions = (envValue(env, 'PATHEXT') ?? '.COM;.EXE;.BAT;.CMD').split(';').map(ext => ext.trim()).filter(Boolean);
119
+ const directories = (envValue(env, 'PATH') ?? '').split(';').map(dir => dir.trim().replace(/^"(.*)"$/u, '$1')).filter(Boolean);
120
+ let resolved;
121
+ if (/[\\/]/u.test(name) || win32.extname(name))
122
+ resolved = name;
123
+ else {
124
+ search: for (const directory of directories) {
125
+ for (const extension of extensions) {
126
+ const candidate = win32.join(directory, `${name}${extension.toLowerCase()}`);
127
+ if (isFile(candidate)) {
128
+ resolved = candidate;
129
+ break search;
130
+ }
131
+ }
132
+ }
133
+ }
134
+ const target = resolved ?? `${name}.cmd`;
135
+ const extension = win32.extname(target).toLowerCase();
136
+ if (extension === '.exe' || extension === '.com')
137
+ return Object.freeze({ command: target, args: Object.freeze([...args]) });
138
+ const doubleEscape = extension === '.cmd' || extension === '.bat';
139
+ const line = [win32.normalize(target).replace(CMD_META, '^$1'), ...args.map(arg => quoteWindowsCmdArgument(arg, doubleEscape))].join(' ');
140
+ return Object.freeze({
141
+ command: envValue(env, 'COMSPEC') ?? 'cmd.exe',
142
+ args: Object.freeze(['/d', '/s', '/c', `"${line}"`]),
143
+ windowsVerbatimArguments: true,
144
+ });
145
+ }
146
+ /**
147
+ * An `installDependencies` implementation for `PluginManagementWorkflow` that streams output.
148
+ * No shell: the command and every argument are passed as an argv array.
149
+ */
150
+ export function createStreamingPackageManager(output, options = {}) {
151
+ const run = options.spawn ?? spawn;
152
+ return async (args, projectRoot) => {
153
+ const manager = options.manager ?? await detectPackageManager(projectRoot);
154
+ const argv = translatePackageManagerArgs(manager, args);
155
+ await new Promise((resolvePromise, reject) => {
156
+ const env = options.env ?? process.env;
157
+ const resolved = resolveCommand(manager, argv, { env, ...(options.platform ? { platform: options.platform } : {}) });
158
+ const child = run(resolved.command, [...resolved.args], {
159
+ cwd: projectRoot,
160
+ shell: false,
161
+ stdio: ['ignore', 'pipe', 'pipe'],
162
+ env,
163
+ windowsHide: true,
164
+ ...(resolved.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
165
+ });
166
+ child.stdout?.setEncoding('utf8');
167
+ child.stderr?.setEncoding('utf8');
168
+ child.stdout?.on('data', (chunk) => output('stdout', chunk));
169
+ child.stderr?.on('data', (chunk) => output('stderr', chunk));
170
+ child.once('error', reject);
171
+ child.once('close', (code, signal) => {
172
+ if (code === 0)
173
+ resolvePromise();
174
+ else
175
+ reject(new Error(`${manager} ${argv[0]} exited with ${signal ?? `code ${code}`}`));
176
+ });
177
+ });
178
+ };
179
+ }