openship 0.2.3 → 0.4.0

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 (82) hide show
  1. package/README.md +104 -61
  2. package/dist/bare-ITM45QBX.js +18 -0
  3. package/dist/bowser-MFQA6OS4.js +2821 -0
  4. package/dist/chunk-3U4SPZD5.js +241 -0
  5. package/dist/chunk-46YJFP4S.js +1771 -0
  6. package/dist/chunk-4CQWT2WL.js +116 -0
  7. package/dist/chunk-4FABQGQP.js +23 -0
  8. package/dist/chunk-4LEL3H2Z.js +84 -0
  9. package/dist/chunk-4MG3BVHE.js +326 -0
  10. package/dist/chunk-4VNS5WPM.js +42 -0
  11. package/dist/chunk-5N7VUGL6.js +2094 -0
  12. package/dist/chunk-5ZCKQ2MI.js +1360 -0
  13. package/dist/chunk-6ERIMTBC.js +1412 -0
  14. package/dist/chunk-7UOB5GGQ.js +132 -0
  15. package/dist/chunk-BUBSSKO6.js +1384 -0
  16. package/dist/chunk-E6W2JMMU.js +341 -0
  17. package/dist/chunk-E77LJWJK.js +1535 -0
  18. package/dist/chunk-F4DFVG6Q.js +573 -0
  19. package/dist/chunk-HFYDYK4D.js +1092 -0
  20. package/dist/chunk-JR2OCMJA.js +46 -0
  21. package/dist/chunk-JUJBBF5U.js +746 -0
  22. package/dist/chunk-JZLNHGLW.js +19145 -0
  23. package/dist/chunk-KD7VO4LG.js +2617 -0
  24. package/dist/chunk-LBFH7THC.js +435 -0
  25. package/dist/chunk-NB2VEIWG.js +129 -0
  26. package/dist/chunk-OJE6YWPQ.js +2866 -0
  27. package/dist/chunk-QKFRZNKN.js +4941 -0
  28. package/dist/chunk-RBZPFTHV.js +13354 -0
  29. package/dist/chunk-S6DSRLNE.js +121 -0
  30. package/dist/chunk-TEYJYSMT.js +409 -0
  31. package/dist/chunk-U2DPZSXA.js +112 -0
  32. package/dist/chunk-WJPCBU7Z.js +3147 -0
  33. package/dist/chunk-XIFRMKYT.js +13 -0
  34. package/dist/chunk-XVSAUO2I.js +7946 -0
  35. package/dist/chunk-Y37KHU7L.js +31 -0
  36. package/dist/chunk-YL67EWYZ.js +416 -0
  37. package/dist/chunk-ZKJOFWBJ.js +1879 -0
  38. package/dist/cloud-JMPHO3CW.js +7 -0
  39. package/dist/cloud-W5G6YXAY.js +24 -0
  40. package/dist/dist-L3MPOWCA.js +65 -0
  41. package/dist/dist-es-4C322O5D.js +22 -0
  42. package/dist/dist-es-GTAMF674.js +87 -0
  43. package/dist/dist-es-KE56MWYO.js +169 -0
  44. package/dist/dist-es-NRZIQY5B.js +484 -0
  45. package/dist/dist-es-WZCR7DHQ.js +317 -0
  46. package/dist/dist-es-XQCOTGC7.js +376 -0
  47. package/dist/dist-es-ZW3HFQUM.js +69 -0
  48. package/dist/docker-3FVJMFC3.js +12 -0
  49. package/dist/docker-edge-executor-ZIWAZWPL.js +9 -0
  50. package/dist/edge-import-53M5DGWZ.js +39 -0
  51. package/dist/event-streams-7LGDL56D.js +42 -0
  52. package/dist/executor-J7BHW5HF.js +26 -0
  53. package/dist/index.js +2459 -3461
  54. package/dist/loadSso-N273C5I7.js +577 -0
  55. package/dist/nginx-YZWKYKQU.js +16 -0
  56. package/dist/noop-RRNCIUI5.js +7 -0
  57. package/dist/openresty-lua-HAO4EUDY.js +32 -0
  58. package/dist/server/index.js +217916 -205055
  59. package/dist/server/migrations/0057_complete_prism.sql +18 -0
  60. package/dist/server/migrations/0058_add_server_is_local.sql +1 -0
  61. package/dist/server/migrations/0059_legal_fixer.sql +18 -0
  62. package/dist/server/migrations/0060_add_route_strategy_host_port.sql +3 -0
  63. package/dist/server/migrations/0061_notification_default_multi_kind.sql +3 -0
  64. package/dist/server/migrations/0062_add_incoming_webhook.sql +21 -0
  65. package/dist/server/migrations/0063_add_migration_logs.sql +1 -0
  66. package/dist/server/migrations/0064_add_migration_partial.sql +2 -0
  67. package/dist/server/migrations/0065_custom_app_template.sql +14 -0
  68. package/dist/server/migrations/0066_ensure_migration_partial_columns.sql +2 -0
  69. package/dist/server/migrations/0067_webhook_delivery.sql +28 -0
  70. package/dist/server/migrations/0068_add_migration_target_volumes.sql +1 -0
  71. package/dist/server/migrations/0069_add_project_composite_routes.sql +1 -0
  72. package/dist/server/migrations/0070_forward_git_to_server.sql +1 -0
  73. package/dist/server/migrations/meta/0057_snapshot.json +9326 -0
  74. package/dist/server/migrations/meta/0058_snapshot.json +9333 -0
  75. package/dist/server/migrations/meta/0059_snapshot.json +9495 -0
  76. package/dist/server/migrations/meta/0060_snapshot.json +9515 -0
  77. package/dist/server/migrations/meta/_journal.json +99 -1
  78. package/dist/setup-5X3WEEHA.js +14 -0
  79. package/dist/signin-DR462ERO.js +663 -0
  80. package/dist/sso-oidc-VBZLJUK5.js +830 -0
  81. package/dist/sts-EJLZ4Z77.js +1422 -0
  82. package/package.json +6 -2
@@ -0,0 +1,121 @@
1
+ import {
2
+ LocalExecutor
3
+ } from "./chunk-U2DPZSXA.js";
4
+ import {
5
+ logEntry,
6
+ sq
7
+ } from "./chunk-4CQWT2WL.js";
8
+
9
+ // ../../packages/adapters/src/system/docker-edge-executor.ts
10
+ import Dockerode from "dockerode";
11
+ import { PassThrough } from "stream";
12
+ var DEFAULT_SOCKET = "/var/run/docker.sock";
13
+ var WRITE_CHUNK = 6e4;
14
+ var DockerEdgeExecutor = class {
15
+ docker;
16
+ containerName;
17
+ fileMode;
18
+ /** node:fs backend — used only in "mounted" mode, where the routing volumes
19
+ * really are in this process's filesystem. */
20
+ files = new LocalExecutor();
21
+ constructor(opts) {
22
+ this.containerName = opts.containerName;
23
+ this.docker = opts.docker ?? new Dockerode({ socketPath: opts.socketPath ?? DEFAULT_SOCKET });
24
+ this.fileMode = opts.fileMode ?? "mounted";
25
+ }
26
+ /** Run one command inside the edge container, capturing stdout/stderr + exit. */
27
+ async run(command) {
28
+ const container = this.docker.getContainer(this.containerName);
29
+ const exec = await container.exec({
30
+ Cmd: ["/bin/sh", "-c", command],
31
+ AttachStdout: true,
32
+ AttachStderr: true,
33
+ Tty: false
34
+ });
35
+ const stream = await exec.start({ stdin: false });
36
+ const outStream = new PassThrough();
37
+ const errStream = new PassThrough();
38
+ let stdout = "";
39
+ let stderr = "";
40
+ outStream.on("data", (c) => stdout += c.toString());
41
+ errStream.on("data", (c) => stderr += c.toString());
42
+ this.docker.modem.demuxStream(stream, outStream, errStream);
43
+ await new Promise((resolve, reject) => {
44
+ stream.on("end", () => resolve());
45
+ stream.on("error", reject);
46
+ });
47
+ outStream.end();
48
+ errStream.end();
49
+ let info = await exec.inspect();
50
+ for (let i = 0; info.Running && i < 50; i++) {
51
+ await new Promise((r) => setTimeout(r, 100));
52
+ info = await exec.inspect();
53
+ }
54
+ return { code: info.ExitCode ?? (info.Running ? 1 : 0), stdout, stderr };
55
+ }
56
+ async exec(command) {
57
+ const { code, stdout, stderr } = await this.run(command);
58
+ if (code !== 0) {
59
+ const detail = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
60
+ throw new Error(
61
+ detail || `command exited ${code} in edge container ${this.containerName}`
62
+ );
63
+ }
64
+ return stdout.trim();
65
+ }
66
+ async streamExec(command, onLog) {
67
+ const { code, stdout, stderr } = await this.run(command);
68
+ const output = [stdout, stderr].filter(Boolean).join("");
69
+ if (output) onLog(logEntry(output, code === 0 ? "info" : "error"));
70
+ return { code, output };
71
+ }
72
+ // ── File ops ─────────────────────────────────────────────────────────────
73
+ // "mounted": the shared volume is in this process's filesystem → node:fs.
74
+ // "container": reach it through the daemon → `docker exec` in the edge.
75
+ async writeFile(path, content) {
76
+ if (this.fileMode === "mounted") return this.files.writeFile(path, content);
77
+ const encoded = Buffer.from(content, "utf8").toString("base64");
78
+ const dir = path.replace(/\/[^/]*$/, "");
79
+ if (dir && dir !== path) await this.exec(`mkdir -p ${sq(dir)}`);
80
+ if (encoded.length === 0) {
81
+ await this.exec(`: > ${sq(path)}`);
82
+ return;
83
+ }
84
+ for (let offset = 0; offset < encoded.length; offset += WRITE_CHUNK) {
85
+ const chunk = encoded.slice(offset, offset + WRITE_CHUNK);
86
+ const redirect = offset === 0 ? ">" : ">>";
87
+ await this.exec(`printf '%s' ${sq(chunk)} | base64 -d ${redirect} ${sq(path)}`);
88
+ }
89
+ }
90
+ async readFile(path) {
91
+ if (this.fileMode === "mounted") return this.files.readFile(path);
92
+ return this.exec(`cat ${sq(path)}`);
93
+ }
94
+ async exists(path) {
95
+ if (this.fileMode === "mounted") return this.files.exists(path);
96
+ const { code } = await this.run(`test -e ${sq(path)}`);
97
+ return code === 0;
98
+ }
99
+ async mkdir(path) {
100
+ if (this.fileMode === "mounted") return this.files.mkdir(path);
101
+ await this.exec(`mkdir -p ${sq(path)}`);
102
+ }
103
+ async rm(path) {
104
+ if (this.fileMode === "mounted") return this.files.rm(path);
105
+ await this.exec(`rm -rf ${sq(path)}`);
106
+ }
107
+ transferIn(localPath, remotePath, onLog, options) {
108
+ if (this.fileMode === "container") {
109
+ return Promise.reject(
110
+ new Error("transferIn is not supported against a container-mode edge")
111
+ );
112
+ }
113
+ return this.files.transferIn(localPath, remotePath, onLog, options);
114
+ }
115
+ async dispose() {
116
+ }
117
+ };
118
+
119
+ export {
120
+ DockerEdgeExecutor
121
+ };
@@ -0,0 +1,409 @@
1
+ import {
2
+ COMPONENT_INSTALLERS,
3
+ checkAll,
4
+ checkComponents
5
+ } from "./chunk-46YJFP4S.js";
6
+
7
+ // ../../packages/adapters/src/system/state.ts
8
+ var DEFAULT_STATE_PATH = "/etc/openship/setup-state.json";
9
+ var FileStateStore = class {
10
+ statePath;
11
+ executor;
12
+ constructor(executor, statePath) {
13
+ this.executor = executor;
14
+ this.statePath = statePath ?? DEFAULT_STATE_PATH;
15
+ }
16
+ async get() {
17
+ try {
18
+ const exists = await this.executor.exists(this.statePath);
19
+ if (!exists) return null;
20
+ const content = await this.executor.readFile(this.statePath);
21
+ return JSON.parse(content);
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+ async set(state) {
27
+ const content = JSON.stringify(state, null, 2);
28
+ await this.executor.writeFile(this.statePath, content);
29
+ }
30
+ async clear() {
31
+ await this.executor.rm(this.statePath);
32
+ }
33
+ };
34
+
35
+ // ../../packages/adapters/src/system/setup.ts
36
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
37
+ function resolveRules(mode) {
38
+ if (mode === "docker") {
39
+ return [
40
+ { feature: "build", requires: ["git", "docker"], message: "Build requires Git and Docker" },
41
+ { feature: "deploy", requires: ["docker"], message: "Deploy requires Docker" },
42
+ { feature: "routing", requires: ["openresty"], message: "Routing requires OpenResty" },
43
+ { feature: "ssl", requires: ["openresty", "certbot"], message: "SSL requires OpenResty and certbot" }
44
+ ];
45
+ }
46
+ return [
47
+ { feature: "build", requires: ["git"], message: "Build requires Git" },
48
+ { feature: "routing", requires: ["openresty"], message: "Routing requires OpenResty" },
49
+ { feature: "ssl", requires: ["openresty", "certbot"], message: "SSL requires OpenResty and certbot" }
50
+ ];
51
+ }
52
+ function resolveRequired(mode) {
53
+ return mode === "docker" ? ["docker", "git", "openresty", "certbot"] : ["git", "openresty", "certbot"];
54
+ }
55
+ var SystemManager = class {
56
+ mode;
57
+ executor;
58
+ rules;
59
+ required;
60
+ stateStore;
61
+ installerConfig;
62
+ provisionLock;
63
+ assumeInstalled;
64
+ /** In-memory cache to avoid even reading from disk/DB on hot paths. */
65
+ cachedState = null;
66
+ constructor(mode, opts) {
67
+ this.mode = mode;
68
+ this.executor = opts.executor;
69
+ this.rules = resolveRules(mode);
70
+ this.required = resolveRequired(mode);
71
+ this.stateStore = opts.stateStore ?? new FileStateStore(opts.executor);
72
+ this.installerConfig = opts.installerConfig ?? {};
73
+ this.provisionLock = opts.provisionLock;
74
+ this.assumeInstalled = opts.assumeInstalled ?? false;
75
+ }
76
+ // ── Fast-path state queries (cached) ─────────────────────────────────
77
+ /**
78
+ * Is the server fully set up? Reads from cache - never runs checks.
79
+ *
80
+ * Returns false if:
81
+ * - No cached state (first boot)
82
+ * - Cached state says setupComplete = false
83
+ * - Cache is stale (> 24h since last verification)
84
+ *
85
+ * Use this on hot paths (every request). It's essentially free.
86
+ */
87
+ async isReady() {
88
+ const state = await this.loadState();
89
+ if (!state?.setupComplete) return false;
90
+ if (this.isStale(state)) {
91
+ this.verify().catch(() => {
92
+ });
93
+ return true;
94
+ }
95
+ return true;
96
+ }
97
+ /**
98
+ * Get the cached setup state without running any checks.
99
+ * Returns null if no state has been stored yet.
100
+ */
101
+ async getState() {
102
+ return this.loadState();
103
+ }
104
+ // ── Status checks (run on demand) ────────────────────────────────────
105
+ /**
106
+ * Check all registered components - runs actual system commands.
107
+ * Updates the cached state with fresh results.
108
+ */
109
+ async checkAll() {
110
+ const components = await checkAll(this.executor);
111
+ const missing = components.filter((c) => this.required.includes(c.name) && !c.healthy).map((c) => c.name);
112
+ await this.updateStateFromChecks(components);
113
+ return { components, ready: missing.length === 0, missing };
114
+ }
115
+ /** Check only the components required for this runtime mode. */
116
+ async checkRequired() {
117
+ const components = await checkComponents(this.executor, this.required);
118
+ const missing = components.filter((c) => !c.healthy).map((c) => c.name);
119
+ await this.updateStateFromChecks(components);
120
+ return { components, ready: missing.length === 0, missing };
121
+ }
122
+ /**
123
+ * Check whether a specific feature is ready.
124
+ *
125
+ * Fast path: if cached state shows all prerequisites installed,
126
+ * returns immediately without running system commands.
127
+ */
128
+ async checkFeature(feature) {
129
+ if (this.assumeInstalled) {
130
+ return { feature, ready: true, missing: [], message: `${feature} assumed installed` };
131
+ }
132
+ const rule = this.rules.find((r) => r.feature === feature);
133
+ if (!rule) {
134
+ return { feature, ready: true, missing: [], message: `No prerequisites for "${feature}"` };
135
+ }
136
+ const state = await this.loadState();
137
+ if (state?.setupComplete) {
138
+ const allPresent = rule.requires.every(
139
+ (name) => state.components[name]?.healthy === true
140
+ );
141
+ if (allPresent) {
142
+ return { feature, ready: true, missing: [], message: `${feature} is ready` };
143
+ }
144
+ }
145
+ const statuses = await checkComponents(this.executor, rule.requires);
146
+ const unhealthy = statuses.filter((s) => !s.healthy);
147
+ return {
148
+ feature,
149
+ ready: unhealthy.length === 0,
150
+ missing: unhealthy,
151
+ message: unhealthy.length === 0 ? `${feature} is ready` : `${rule.message} - missing: ${unhealthy.map((s) => s.name).join(", ")}`
152
+ };
153
+ }
154
+ /**
155
+ * Require a feature to be ready, or throw.
156
+ *
157
+ * Fast path reads cached state. Use before operations:
158
+ * await system.requireFeature("deploy");
159
+ */
160
+ async requireFeature(feature) {
161
+ const readiness = await this.checkFeature(feature);
162
+ if (!readiness.ready) {
163
+ throw new Error(readiness.message);
164
+ }
165
+ }
166
+ /**
167
+ * Ensure a feature is ready: check prerequisites, install missing ones, re-validate.
168
+ *
169
+ * This is the operational path used by deploy/build flows when they can safely
170
+ * self-heal missing system components instead of hard-failing immediately.
171
+ */
172
+ async ensureFeature(feature, onLog, config) {
173
+ if (this.assumeInstalled) return;
174
+ const logFn = onLog ?? (() => {
175
+ });
176
+ const installerConfig = config ?? this.installerConfig;
177
+ const readiness = await this.checkFeature(feature);
178
+ if (readiness.ready) return;
179
+ await this.ensureNamedComponents(
180
+ readiness.missing.map((component) => component.name),
181
+ logFn,
182
+ installerConfig,
183
+ `Checking required system components for ${feature}...`,
184
+ (names) => `${this.rules.find((rule) => rule.feature === feature)?.message ?? feature} - missing: ${names.join(", ")}`
185
+ );
186
+ }
187
+ async ensureComponents(names, onLog, config) {
188
+ if (this.assumeInstalled) return;
189
+ const logFn = onLog ?? (() => {
190
+ });
191
+ const installerConfig = config ?? this.installerConfig;
192
+ await this.ensureNamedComponents(
193
+ names,
194
+ logFn,
195
+ installerConfig,
196
+ "Checking required system components...",
197
+ (missingNames) => `Required components are still not ready: ${missingNames.join(", ")}`
198
+ );
199
+ }
200
+ // ── Installation ─────────────────────────────────────────────────────
201
+ /**
202
+ * Install a single component with streamed logs.
203
+ */
204
+ async installComponent(name, onLog) {
205
+ const logFn = onLog ?? (() => {
206
+ });
207
+ return this.runInstaller(name, logFn, this.installerConfig);
208
+ }
209
+ /**
210
+ * Full setup: check → install missing → validate → cache state.
211
+ *
212
+ * This is the main provisioning entrypoint called from the dashboard
213
+ * setup wizard. The InstallerConfig should be pre-collected from the
214
+ * user (ACME email, domain, etc.) before calling this.
215
+ */
216
+ async setup(onLog, config) {
217
+ const logFn = onLog ?? (() => {
218
+ });
219
+ const installerConfig = config ?? this.installerConfig;
220
+ logFn(info(`Starting ${this.mode} server setup...`));
221
+ logFn(info(`Required components: ${this.required.join(", ")}`));
222
+ logFn(info("Checking installed components..."));
223
+ const initial = await this.checkRequired();
224
+ const alreadyInstalled = initial.components.filter((c) => c.healthy).map((c) => c.name);
225
+ if (alreadyInstalled.length > 0) {
226
+ logFn(info(`Already installed: ${alreadyInstalled.join(", ")}`));
227
+ }
228
+ if (initial.ready) {
229
+ logFn(info("All components are already installed - nothing to do"));
230
+ await this.markSetupComplete();
231
+ return { installed: [], skipped: alreadyInstalled, failed: [], ready: true };
232
+ }
233
+ logFn(info(`Missing: ${initial.missing.join(", ")}`));
234
+ const { installed, failed } = await this.installMany(
235
+ initial.missing,
236
+ logFn,
237
+ installerConfig,
238
+ false
239
+ );
240
+ logFn(info("\nValidating installation..."));
241
+ const final = await this.checkRequired();
242
+ if (final.ready) {
243
+ logFn(info("Server setup complete - all components healthy"));
244
+ await this.markSetupComplete();
245
+ } else {
246
+ logFn({
247
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
248
+ message: `Setup incomplete - still missing: ${final.missing.join(", ")}`,
249
+ level: "warn"
250
+ });
251
+ }
252
+ return {
253
+ installed,
254
+ skipped: alreadyInstalled,
255
+ failed,
256
+ ready: final.ready
257
+ };
258
+ }
259
+ // ── State management ─────────────────────────────────────────────────
260
+ /**
261
+ * Invalidate the cached state.
262
+ *
263
+ * Call this when an operation fails because a component is missing
264
+ * (e.g., Docker daemon went down). The next `isReady()` call will
265
+ * return false, and `requireFeature()` will re-check.
266
+ */
267
+ async invalidate() {
268
+ this.cachedState = null;
269
+ await this.stateStore.clear();
270
+ }
271
+ /**
272
+ * Force re-verification: run all checks and update state.
273
+ * Called automatically when cache is stale, or manually by the user.
274
+ */
275
+ async verify() {
276
+ const result = await this.checkAll();
277
+ return result;
278
+ }
279
+ // ── Utilities ────────────────────────────────────────────────────────
280
+ getRules() {
281
+ return [...this.rules];
282
+ }
283
+ getRequired() {
284
+ return [...this.required];
285
+ }
286
+ // ── Internal state helpers ───────────────────────────────────────────
287
+ async loadState() {
288
+ if (this.cachedState) return this.cachedState;
289
+ this.cachedState = await this.stateStore.get();
290
+ return this.cachedState;
291
+ }
292
+ isStale(state) {
293
+ if (!state.lastVerifiedAt) return true;
294
+ const age = Date.now() - new Date(state.lastVerifiedAt).getTime();
295
+ return age > CACHE_TTL_MS;
296
+ }
297
+ async updateStateFromChecks(components) {
298
+ const existing = await this.loadState() ?? this.emptyState();
299
+ for (const c of components) {
300
+ existing.components[c.name] = {
301
+ installed: c.installed,
302
+ version: c.version,
303
+ running: c.running,
304
+ healthy: c.healthy,
305
+ installedAt: existing.components[c.name]?.installedAt
306
+ };
307
+ }
308
+ const allRequired = this.required.every(
309
+ (name) => existing.components[name]?.installed
310
+ );
311
+ existing.setupComplete = allRequired;
312
+ existing.lastVerifiedAt = (/* @__PURE__ */ new Date()).toISOString();
313
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
314
+ this.cachedState = existing;
315
+ await this.stateStore.set(existing);
316
+ }
317
+ async markComponentInstalled(name, version) {
318
+ const state = await this.loadState() ?? this.emptyState();
319
+ state.components[name] = {
320
+ installed: true,
321
+ version,
322
+ healthy: false,
323
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
324
+ };
325
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
326
+ this.cachedState = state;
327
+ await this.stateStore.set(state);
328
+ }
329
+ async markSetupComplete() {
330
+ const state = await this.loadState() ?? this.emptyState();
331
+ state.setupComplete = true;
332
+ state.lastVerifiedAt = (/* @__PURE__ */ new Date()).toISOString();
333
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
334
+ this.cachedState = state;
335
+ await this.stateStore.set(state);
336
+ }
337
+ emptyState() {
338
+ return {
339
+ setupComplete: false,
340
+ mode: this.mode,
341
+ components: {},
342
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
343
+ };
344
+ }
345
+ async runInstaller(name, logFn, installerConfig) {
346
+ const installer = COMPONENT_INSTALLERS[name];
347
+ if (!installer) {
348
+ logFn(info(`No installer available for "${name}"`));
349
+ return { component: name, success: false, error: `No installer for "${name}"` };
350
+ }
351
+ const result = await installer(this.executor, logFn, installerConfig);
352
+ if (result.success) {
353
+ await this.markComponentInstalled(name, result.version);
354
+ }
355
+ return result;
356
+ }
357
+ async installMany(names, logFn, installerConfig, stopOnFailure) {
358
+ const installed = [];
359
+ const failed = [];
360
+ for (const name of names) {
361
+ logFn(info(`
362
+ \u2500\u2500 Installing ${name} ${"\u2500".repeat(50)}`));
363
+ const result = await this.runInstaller(name, logFn, installerConfig);
364
+ if (result.success) {
365
+ installed.push(result);
366
+ continue;
367
+ }
368
+ failed.push(result);
369
+ logFn(info(`Failed to install ${name}: ${result.error}`));
370
+ if (stopOnFailure) break;
371
+ }
372
+ return { installed, failed };
373
+ }
374
+ async ensureNamedComponents(names, logFn, installerConfig, heading, errorMessage) {
375
+ const critical = async () => {
376
+ const statuses = await checkComponents(this.executor, names);
377
+ const missing = statuses.filter((status) => !status.healthy);
378
+ if (missing.length === 0) {
379
+ await this.updateStateFromChecks(statuses);
380
+ return;
381
+ }
382
+ logFn(info(heading));
383
+ logFn(info(`Missing: ${missing.map((component) => component.name).join(", ")}`));
384
+ const { failed } = await this.installMany(
385
+ missing.map((component) => component.name),
386
+ logFn,
387
+ installerConfig,
388
+ true
389
+ );
390
+ if (failed.length > 0) {
391
+ throw new Error(failed[0].error ?? `Failed to install ${failed[0].component}`);
392
+ }
393
+ const recheck = await checkComponents(this.executor, names);
394
+ await this.updateStateFromChecks(recheck);
395
+ const unhealthy = recheck.filter((status) => !status.healthy);
396
+ if (unhealthy.length > 0) {
397
+ throw new Error(errorMessage(unhealthy.map((status) => status.name)));
398
+ }
399
+ };
400
+ return this.provisionLock ? this.provisionLock.run(critical) : critical();
401
+ }
402
+ };
403
+ function info(message) {
404
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level: "info" };
405
+ }
406
+
407
+ export {
408
+ SystemManager
409
+ };
@@ -0,0 +1,112 @@
1
+ import {
2
+ getLocalExecEnv,
3
+ getLocalShellArgs,
4
+ getLocalShellPath,
5
+ logEntry,
6
+ sq
7
+ } from "./chunk-4CQWT2WL.js";
8
+
9
+ // ../../packages/adapters/src/system/local-executor.ts
10
+ import { exec, spawn } from "child_process";
11
+ import {
12
+ access,
13
+ mkdir as fsMkdir,
14
+ readFile as fsReadFile,
15
+ rm as fsRm,
16
+ writeFile as fsWriteFile
17
+ } from "fs/promises";
18
+ import { dirname } from "path";
19
+ var LocalExecutor = class {
20
+ async exec(command, opts) {
21
+ return new Promise((resolve, reject) => {
22
+ const shell = getLocalShellPath();
23
+ exec(
24
+ command,
25
+ {
26
+ timeout: opts?.timeout ?? 3e4,
27
+ shell,
28
+ env: getLocalExecEnv()
29
+ },
30
+ (err, stdout, stderr) => {
31
+ if (err) {
32
+ const detail = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
33
+ reject(new Error(detail || err.message));
34
+ } else resolve(stdout.trim());
35
+ }
36
+ );
37
+ });
38
+ }
39
+ streamExec(command, onLog) {
40
+ return new Promise((resolve) => {
41
+ const child = spawn(getLocalShellPath(), getLocalShellArgs(command), {
42
+ stdio: ["ignore", "pipe", "pipe"],
43
+ env: getLocalExecEnv()
44
+ });
45
+ const chunks = [];
46
+ const onChunk = (data, level) => {
47
+ const text = data.toString();
48
+ if (!text) return;
49
+ chunks.push(text);
50
+ onLog(logEntry(text, level, data.toString("base64")));
51
+ };
52
+ child.stdout.on("data", (data) => onChunk(data, "info"));
53
+ child.stderr.on("data", (data) => onChunk(data, "warn"));
54
+ child.on("close", (code) => {
55
+ resolve({ code: code ?? 1, output: chunks.join("") });
56
+ });
57
+ child.on("error", (err) => {
58
+ onLog(logEntry(`Process error: ${err.message}`, "error"));
59
+ resolve({ code: 1, output: err.message });
60
+ });
61
+ });
62
+ }
63
+ async writeFile(path, content) {
64
+ await fsMkdir(dirname(path), { recursive: true });
65
+ await fsWriteFile(path, content, "utf-8");
66
+ }
67
+ async readFile(path) {
68
+ return fsReadFile(path, "utf-8");
69
+ }
70
+ async exists(path) {
71
+ try {
72
+ await access(path);
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+ async mkdir(path) {
79
+ await fsMkdir(path, { recursive: true });
80
+ }
81
+ async rm(path) {
82
+ try {
83
+ await fsRm(path, { recursive: true, force: true });
84
+ } catch {
85
+ }
86
+ }
87
+ async transferIn(localPath, remotePath, onLog, options) {
88
+ const log = onLog ?? (() => {
89
+ });
90
+ if (options?.includes?.length) {
91
+ for (const p of options.includes) {
92
+ const { code: code2 } = await this.streamExec(
93
+ `cp -a ${sq(localPath + "/" + p)} ${sq(remotePath + "/" + p)}`,
94
+ log
95
+ );
96
+ if (code2 !== 0) throw new Error(`Failed to copy ${p}`);
97
+ }
98
+ return;
99
+ }
100
+ const { code } = await this.streamExec(
101
+ `cp -a ${sq(localPath + "/")}. ${sq(remotePath)}`,
102
+ log
103
+ );
104
+ if (code !== 0) throw new Error("Failed to copy local project files");
105
+ }
106
+ async dispose() {
107
+ }
108
+ };
109
+
110
+ export {
111
+ LocalExecutor
112
+ };