@spencer-kit/coder-studio 0.4.0 → 0.4.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.
@@ -0,0 +1,156 @@
1
+ // @spencer-kit/coder-studio - ESM bundle
2
+
3
+ // packages/cli/src/update-worker.ts
4
+ import { spawn } from "node:child_process";
5
+ import { createWriteStream } from "node:fs";
6
+ import { mkdir } from "node:fs/promises";
7
+ import { dirname } from "node:path";
8
+ async function writeState(filePath, value) {
9
+ await mkdir(dirname(filePath), { recursive: true });
10
+ await import("node:fs/promises").then(
11
+ ({ writeFile }) => writeFile(filePath, JSON.stringify(value, null, 2) + "\n", "utf-8")
12
+ );
13
+ }
14
+ function parseJsonArray(value, fallback) {
15
+ if (!value) {
16
+ return fallback;
17
+ }
18
+ try {
19
+ const parsed = JSON.parse(value);
20
+ if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
21
+ return parsed;
22
+ }
23
+ } catch {
24
+ }
25
+ return fallback;
26
+ }
27
+ function readEnv(env = process.env) {
28
+ const stateFilePath = env.CODER_STUDIO_UPDATE_STATE_PATH;
29
+ const logFilePath = env.CODER_STUDIO_UPDATE_LOG_PATH;
30
+ const packageName = env.CODER_STUDIO_UPDATE_PACKAGE_NAME;
31
+ const targetVersion = env.CODER_STUDIO_UPDATE_TARGET_VERSION;
32
+ const cliCommand = env.CODER_STUDIO_UPDATE_CLI_COMMAND;
33
+ const currentVersion = env.CODER_STUDIO_UPDATE_CURRENT_VERSION;
34
+ if (!stateFilePath || !logFilePath || !packageName || !targetVersion || !cliCommand || !currentVersion) {
35
+ throw new Error("Missing detached update worker environment");
36
+ }
37
+ return {
38
+ stateFilePath,
39
+ logFilePath,
40
+ packageName,
41
+ targetVersion,
42
+ cliCommand,
43
+ currentVersion,
44
+ npmCommand: env.CODER_STUDIO_UPDATE_NPM_COMMAND || "npm",
45
+ restartArgs: parseJsonArray(env.CODER_STUDIO_UPDATE_RESTART_ARGS, ["serve", "--restart"]),
46
+ installArgsPrefix: parseJsonArray(env.CODER_STUDIO_UPDATE_INSTALL_ARGS_PREFIX, [
47
+ "install",
48
+ "-g"
49
+ ])
50
+ };
51
+ }
52
+ function buildManualCommand(input) {
53
+ return [
54
+ `${input.npmCommand} ${[...input.installArgsPrefix, `${input.packageName}@${input.targetVersion}`].join(" ")}`,
55
+ `${input.cliCommand} ${input.restartArgs.join(" ")}`
56
+ ].join("\n");
57
+ }
58
+ function runCommand(command, args, options) {
59
+ return new Promise((resolve, reject) => {
60
+ const child = spawn(command, args, {
61
+ stdio: options?.stdio === "ignore" ? "ignore" : "pipe",
62
+ env: process.env
63
+ });
64
+ if (options?.logStream && child.stdout) {
65
+ child.stdout.pipe(options.logStream, { end: false });
66
+ }
67
+ if (options?.logStream && child.stderr) {
68
+ child.stderr.pipe(options.logStream, { end: false });
69
+ }
70
+ child.on("error", reject);
71
+ child.on("exit", (code) => {
72
+ if (code === 0) {
73
+ resolve();
74
+ return;
75
+ }
76
+ reject(new Error(`${command} exited with code ${code ?? 1}`));
77
+ });
78
+ });
79
+ }
80
+ async function runUpdateWorker(input = readEnv(), deps) {
81
+ const now = deps?.now ?? Date.now;
82
+ await mkdir(dirname(input.logFilePath), { recursive: true });
83
+ const logStream = createWriteStream(input.logFilePath, { flags: "a" });
84
+ const execute = deps?.runCommand ?? runCommand;
85
+ try {
86
+ await execute(
87
+ input.npmCommand,
88
+ [...input.installArgsPrefix, `${input.packageName}@${input.targetVersion}`],
89
+ { logStream }
90
+ );
91
+ } catch (error) {
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ const permissionRelated = /EACCES|EPERM|permission|not permitted/i.test(message) || /requires elevated privileges/i.test(message);
94
+ await writeState(input.stateFilePath, {
95
+ version: 1,
96
+ currentVersion: input.currentVersion,
97
+ latestVersion: input.targetVersion,
98
+ availability: "update_available",
99
+ updateStatus: permissionRelated ? "manual_required" : "failed",
100
+ lastCheckedAt: now(),
101
+ targetVersion: input.targetVersion,
102
+ startedAt: now(),
103
+ finishedAt: now(),
104
+ requiresManualStep: permissionRelated,
105
+ manualCommand: permissionRelated ? buildManualCommand(input) : null,
106
+ errorSummary: message
107
+ });
108
+ logStream.end();
109
+ return;
110
+ }
111
+ await writeState(input.stateFilePath, {
112
+ version: 1,
113
+ currentVersion: input.currentVersion,
114
+ latestVersion: input.targetVersion,
115
+ availability: "update_available",
116
+ updateStatus: "restarting",
117
+ lastCheckedAt: now(),
118
+ targetVersion: input.targetVersion,
119
+ startedAt: now(),
120
+ finishedAt: null,
121
+ requiresManualStep: false,
122
+ manualCommand: null,
123
+ errorSummary: null
124
+ });
125
+ try {
126
+ await execute(input.cliCommand, input.restartArgs, { logStream });
127
+ } catch (error) {
128
+ const message = error instanceof Error ? error.message : String(error);
129
+ await writeState(input.stateFilePath, {
130
+ version: 1,
131
+ currentVersion: input.currentVersion,
132
+ latestVersion: input.targetVersion,
133
+ availability: "update_available",
134
+ updateStatus: "failed",
135
+ lastCheckedAt: now(),
136
+ targetVersion: input.targetVersion,
137
+ startedAt: now(),
138
+ finishedAt: now(),
139
+ requiresManualStep: true,
140
+ manualCommand: `${input.cliCommand} ${input.restartArgs.join(" ")}`,
141
+ errorSummary: `new version installed but service restart failed: ${message}`
142
+ });
143
+ } finally {
144
+ logStream.end();
145
+ }
146
+ }
147
+ if (process.env.CODER_STUDIO_UPDATE_STATE_PATH) {
148
+ void runUpdateWorker().catch((error) => {
149
+ console.error("[update-worker]", error);
150
+ process.exitCode = 1;
151
+ });
152
+ }
153
+ export {
154
+ runUpdateWorker
155
+ };
156
+ //# sourceMappingURL=update-worker.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/update-worker.ts"],
4
+ "sourcesContent": ["import { spawn } from \"node:child_process\";\nimport { createWriteStream } from \"node:fs\";\nimport { mkdir } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\ninterface UpdateStateSnapshot {\n version: 1;\n currentVersion: string;\n latestVersion: string | null;\n availability: \"unknown\" | \"up_to_date\" | \"update_available\" | \"check_failed\";\n updateStatus:\n | \"idle\"\n | \"checking\"\n | \"installing\"\n | \"restarting\"\n | \"succeeded\"\n | \"failed\"\n | \"manual_required\";\n lastCheckedAt: number | null;\n targetVersion: string | null;\n startedAt: number | null;\n finishedAt: number | null;\n requiresManualStep: boolean;\n manualCommand: string | null;\n errorSummary: string | null;\n}\n\ninterface WorkerEnv {\n stateFilePath: string;\n logFilePath: string;\n packageName: string;\n targetVersion: string;\n cliCommand: string;\n currentVersion: string;\n npmCommand: string;\n restartArgs: string[];\n installArgsPrefix: string[];\n}\n\nasync function writeState(filePath: string, value: UpdateStateSnapshot): Promise<void> {\n await mkdir(dirname(filePath), { recursive: true });\n await import(\"node:fs/promises\").then(({ writeFile }) =>\n writeFile(filePath, JSON.stringify(value, null, 2) + \"\\n\", \"utf-8\")\n );\n}\n\nfunction parseJsonArray(value: string | undefined, fallback: string[]): string[] {\n if (!value) {\n return fallback;\n }\n try {\n const parsed = JSON.parse(value) as unknown;\n if (Array.isArray(parsed) && parsed.every((item) => typeof item === \"string\")) {\n return parsed;\n }\n } catch {}\n return fallback;\n}\n\nfunction readEnv(env = process.env): WorkerEnv {\n const stateFilePath = env.CODER_STUDIO_UPDATE_STATE_PATH;\n const logFilePath = env.CODER_STUDIO_UPDATE_LOG_PATH;\n const packageName = env.CODER_STUDIO_UPDATE_PACKAGE_NAME;\n const targetVersion = env.CODER_STUDIO_UPDATE_TARGET_VERSION;\n const cliCommand = env.CODER_STUDIO_UPDATE_CLI_COMMAND;\n const currentVersion = env.CODER_STUDIO_UPDATE_CURRENT_VERSION;\n if (\n !stateFilePath ||\n !logFilePath ||\n !packageName ||\n !targetVersion ||\n !cliCommand ||\n !currentVersion\n ) {\n throw new Error(\"Missing detached update worker environment\");\n }\n return {\n stateFilePath,\n logFilePath,\n packageName,\n targetVersion,\n cliCommand,\n currentVersion,\n npmCommand: env.CODER_STUDIO_UPDATE_NPM_COMMAND || \"npm\",\n restartArgs: parseJsonArray(env.CODER_STUDIO_UPDATE_RESTART_ARGS, [\"serve\", \"--restart\"]),\n installArgsPrefix: parseJsonArray(env.CODER_STUDIO_UPDATE_INSTALL_ARGS_PREFIX, [\n \"install\",\n \"-g\",\n ]),\n };\n}\n\nfunction buildManualCommand(input: WorkerEnv): string {\n return [\n `${input.npmCommand} ${[...input.installArgsPrefix, `${input.packageName}@${input.targetVersion}`].join(\" \")}`,\n `${input.cliCommand} ${input.restartArgs.join(\" \")}`,\n ].join(\"\\n\");\n}\n\nfunction runCommand(\n command: string,\n args: string[],\n options?: { stdio?: \"ignore\" | \"pipe\"; logStream?: NodeJS.WritableStream }\n): Promise<void> {\n return new Promise((resolve, reject) => {\n const child = spawn(command, args, {\n stdio: options?.stdio === \"ignore\" ? \"ignore\" : \"pipe\",\n env: process.env,\n });\n\n if (options?.logStream && child.stdout) {\n child.stdout.pipe(options.logStream, { end: false });\n }\n if (options?.logStream && child.stderr) {\n child.stderr.pipe(options.logStream, { end: false });\n }\n\n child.on(\"error\", reject);\n child.on(\"exit\", (code) => {\n if (code === 0) {\n resolve();\n return;\n }\n reject(new Error(`${command} exited with code ${code ?? 1}`));\n });\n });\n}\n\nexport async function runUpdateWorker(\n input = readEnv(),\n deps?: {\n runCommand?: typeof runCommand;\n now?: () => number;\n }\n): Promise<void> {\n const now = deps?.now ?? Date.now;\n await mkdir(dirname(input.logFilePath), { recursive: true });\n const logStream = createWriteStream(input.logFilePath, { flags: \"a\" });\n const execute = deps?.runCommand ?? runCommand;\n\n try {\n await execute(\n input.npmCommand,\n [...input.installArgsPrefix, `${input.packageName}@${input.targetVersion}`],\n { logStream }\n );\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const permissionRelated =\n /EACCES|EPERM|permission|not permitted/i.test(message) ||\n /requires elevated privileges/i.test(message);\n await writeState(input.stateFilePath, {\n version: 1,\n currentVersion: input.currentVersion,\n latestVersion: input.targetVersion,\n availability: \"update_available\",\n updateStatus: permissionRelated ? \"manual_required\" : \"failed\",\n lastCheckedAt: now(),\n targetVersion: input.targetVersion,\n startedAt: now(),\n finishedAt: now(),\n requiresManualStep: permissionRelated,\n manualCommand: permissionRelated ? buildManualCommand(input) : null,\n errorSummary: message,\n });\n logStream.end();\n return;\n }\n\n await writeState(input.stateFilePath, {\n version: 1,\n currentVersion: input.currentVersion,\n latestVersion: input.targetVersion,\n availability: \"update_available\",\n updateStatus: \"restarting\",\n lastCheckedAt: now(),\n targetVersion: input.targetVersion,\n startedAt: now(),\n finishedAt: null,\n requiresManualStep: false,\n manualCommand: null,\n errorSummary: null,\n });\n\n try {\n await execute(input.cliCommand, input.restartArgs, { logStream });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n await writeState(input.stateFilePath, {\n version: 1,\n currentVersion: input.currentVersion,\n latestVersion: input.targetVersion,\n availability: \"update_available\",\n updateStatus: \"failed\",\n lastCheckedAt: now(),\n targetVersion: input.targetVersion,\n startedAt: now(),\n finishedAt: now(),\n requiresManualStep: true,\n manualCommand: `${input.cliCommand} ${input.restartArgs.join(\" \")}`,\n errorSummary: `new version installed but service restart failed: ${message}`,\n });\n } finally {\n logStream.end();\n }\n}\n\nif (process.env.CODER_STUDIO_UPDATE_STATE_PATH) {\n void runUpdateWorker().catch((error) => {\n console.error(\"[update-worker]\", error);\n process.exitCode = 1;\n });\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }