@chandresh726/switchy 1.0.6
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.
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/cli.js +968 -0
- package/dist/cli.js.map +7 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chandresh726
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Switchy CLI
|
|
2
|
+
|
|
3
|
+
Run the local-first Switchy application without cloning or building the source repository.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @chandresh726/switchy@latest start
|
|
7
|
+
npx @chandresh726/switchy@latest status
|
|
8
|
+
npx @chandresh726/switchy@latest stop
|
|
9
|
+
npx @chandresh726/switchy@latest update
|
|
10
|
+
npx @chandresh726/switchy@latest logs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Switchy binds to `127.0.0.1`. Application versions, runtime metadata, logs,
|
|
14
|
+
caches, the SQLite database, uploads, and the encryption secret remain under
|
|
15
|
+
`~/.switchy` on the local device.
|
|
16
|
+
|
|
17
|
+
Node.js 24 is required. The first start downloads the runtime for the current
|
|
18
|
+
platform and Playwright Chromium. Supported targets are macOS Intel and Apple
|
|
19
|
+
Silicon, Linux x64 and arm64, and Windows x64.
|
|
20
|
+
|
|
21
|
+
## License
|
|
22
|
+
|
|
23
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands.ts
|
|
4
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
5
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
// package.json
|
|
8
|
+
var package_default = {
|
|
9
|
+
name: "@chandresh726/switchy",
|
|
10
|
+
version: "1.0.6",
|
|
11
|
+
description: "Install, run, update, and stop the local-first Switchy application.",
|
|
12
|
+
license: "MIT",
|
|
13
|
+
type: "module",
|
|
14
|
+
bin: {
|
|
15
|
+
switchy: "dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
files: [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
engines: {
|
|
22
|
+
node: ">=24 <25"
|
|
23
|
+
},
|
|
24
|
+
repository: {
|
|
25
|
+
type: "git",
|
|
26
|
+
url: "git+https://github.com/Chandresh726/Switchy.git",
|
|
27
|
+
directory: "packages/cli"
|
|
28
|
+
},
|
|
29
|
+
homepage: "https://github.com/Chandresh726/Switchy#readme",
|
|
30
|
+
bugs: {
|
|
31
|
+
url: "https://github.com/Chandresh726/Switchy/issues"
|
|
32
|
+
},
|
|
33
|
+
publishConfig: {
|
|
34
|
+
access: "public"
|
|
35
|
+
},
|
|
36
|
+
scripts: {
|
|
37
|
+
build: "node scripts/build.mjs",
|
|
38
|
+
typecheck: "tsc --noEmit -p tsconfig.json",
|
|
39
|
+
prepack: "pnpm build"
|
|
40
|
+
},
|
|
41
|
+
dependencies: {
|
|
42
|
+
tar: "^7.5.22"
|
|
43
|
+
},
|
|
44
|
+
devDependencies: {
|
|
45
|
+
"@types/node": "^24.13.3",
|
|
46
|
+
esbuild: "^0.28.1",
|
|
47
|
+
typescript: "^5.9.3"
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/config.ts
|
|
52
|
+
var CLI_VERSION = package_default.version;
|
|
53
|
+
var DEFAULT_PORT = 3e3;
|
|
54
|
+
var DEFAULT_HOSTNAME = "127.0.0.1";
|
|
55
|
+
var GITHUB_REPOSITORY = "Chandresh726/Switchy";
|
|
56
|
+
var RELEASE_MANIFEST_FILE = "switchy-manifest.json";
|
|
57
|
+
var STARTUP_TIMEOUT_MS = 6e4;
|
|
58
|
+
var SHUTDOWN_TIMEOUT_MS = 15e3;
|
|
59
|
+
function resolveApplicationVersion(requestedVersion) {
|
|
60
|
+
return requestedVersion ?? CLI_VERSION;
|
|
61
|
+
}
|
|
62
|
+
function assertSupportedNodeVersion(nodeVersion = process.versions.node) {
|
|
63
|
+
if (!/^24\.\d+\.\d+$/u.test(nodeVersion)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Switchy requires Node.js 24; current version is ${nodeVersion}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/files.ts
|
|
71
|
+
import { randomUUID } from "node:crypto";
|
|
72
|
+
import {
|
|
73
|
+
mkdir,
|
|
74
|
+
readFile,
|
|
75
|
+
rename,
|
|
76
|
+
rm,
|
|
77
|
+
stat,
|
|
78
|
+
writeFile
|
|
79
|
+
} from "node:fs/promises";
|
|
80
|
+
import path from "node:path";
|
|
81
|
+
var LOCK_TIMEOUT_MS = 6e4;
|
|
82
|
+
var INVALID_LOCK_GRACE_MS = 3e4;
|
|
83
|
+
var LOCK_OWNER_FILE = "owner.json";
|
|
84
|
+
function processIsAlive(pid) {
|
|
85
|
+
try {
|
|
86
|
+
process.kill(pid, 0);
|
|
87
|
+
return true;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return error.code === "EPERM";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function readLockOwner(lockPath) {
|
|
93
|
+
try {
|
|
94
|
+
const value = JSON.parse(
|
|
95
|
+
await readFile(path.join(lockPath, LOCK_OWNER_FILE), "utf8")
|
|
96
|
+
);
|
|
97
|
+
if (!Number.isInteger(value.pid) || (value.pid ?? 0) <= 0 || typeof value.token !== "string" || value.token.length === 0) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function removeOwnedLock(lockPath, token) {
|
|
106
|
+
const owner = await readLockOwner(lockPath);
|
|
107
|
+
if (owner?.token === token) {
|
|
108
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function readJsonFile(filePath) {
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error.code === "ENOENT") return null;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function writeJsonFileAtomic(filePath, value) {
|
|
120
|
+
const temporaryPath = path.join(
|
|
121
|
+
path.dirname(filePath),
|
|
122
|
+
`.${path.basename(filePath)}.${randomUUID()}.tmp`
|
|
123
|
+
);
|
|
124
|
+
await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}
|
|
125
|
+
`, {
|
|
126
|
+
mode: 384
|
|
127
|
+
});
|
|
128
|
+
await rename(temporaryPath, filePath);
|
|
129
|
+
}
|
|
130
|
+
async function removeFile(filePath) {
|
|
131
|
+
await rm(filePath, { force: true });
|
|
132
|
+
}
|
|
133
|
+
async function withDirectoryLock(lockPath, operation) {
|
|
134
|
+
await mkdir(path.dirname(lockPath), { recursive: true, mode: 448 });
|
|
135
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
136
|
+
const token = randomUUID();
|
|
137
|
+
while (true) {
|
|
138
|
+
try {
|
|
139
|
+
await mkdir(lockPath, { mode: 448 });
|
|
140
|
+
try {
|
|
141
|
+
await writeFile(
|
|
142
|
+
path.join(lockPath, LOCK_OWNER_FILE),
|
|
143
|
+
`${JSON.stringify({ pid: process.pid, token })}
|
|
144
|
+
`,
|
|
145
|
+
{ mode: 384 }
|
|
146
|
+
);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error.code !== "EEXIST") throw error;
|
|
154
|
+
const [owner, lockStats] = await Promise.all([
|
|
155
|
+
readLockOwner(lockPath),
|
|
156
|
+
stat(lockPath).catch(() => null)
|
|
157
|
+
]);
|
|
158
|
+
if (owner && !processIsAlive(owner.pid) || !owner && lockStats && Date.now() - lockStats.mtimeMs > INVALID_LOCK_GRACE_MS) {
|
|
159
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (Date.now() >= deadline) {
|
|
163
|
+
throw new Error(`Timed out waiting for ${path.basename(lockPath)}`);
|
|
164
|
+
}
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
return await operation();
|
|
170
|
+
} finally {
|
|
171
|
+
await removeOwnedLock(lockPath, token);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/process-manager.ts
|
|
176
|
+
import { open } from "node:fs/promises";
|
|
177
|
+
import path2 from "node:path";
|
|
178
|
+
import { spawn } from "node:child_process";
|
|
179
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
180
|
+
function processIsAlive2(pid) {
|
|
181
|
+
try {
|
|
182
|
+
process.kill(pid, 0);
|
|
183
|
+
return true;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return error.code === "EPERM";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function readProcessRecord(paths) {
|
|
189
|
+
const record = await readJsonFile(paths.processRecord).catch(() => null);
|
|
190
|
+
if (!record || record.schemaVersion !== 1 || !Number.isInteger(record.pid) || record.pid <= 0 || typeof record.instanceId !== "string" || record.instanceId.length === 0 || !/^\d+\.\d+\.\d+$/u.test(record.version) || record.hostname !== DEFAULT_HOSTNAME || !Number.isInteger(record.port) || record.port < 1 || record.port > 65535 || !Number.isFinite(Date.parse(record.startedAt))) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
return record;
|
|
194
|
+
}
|
|
195
|
+
async function fetchLiveness(record) {
|
|
196
|
+
try {
|
|
197
|
+
const response = await fetch(
|
|
198
|
+
`http://${record.hostname}:${record.port}/api/health/live`,
|
|
199
|
+
{ signal: AbortSignal.timeout(2e3) }
|
|
200
|
+
);
|
|
201
|
+
if (!response.ok) return null;
|
|
202
|
+
const value = await response.json();
|
|
203
|
+
if (value.status !== "live" || typeof value.version !== "string") return null;
|
|
204
|
+
return {
|
|
205
|
+
status: "live",
|
|
206
|
+
version: value.version,
|
|
207
|
+
instanceId: typeof value.instanceId === "string" ? value.instanceId : null
|
|
208
|
+
};
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function getRunningProcess(paths) {
|
|
214
|
+
const record = await readProcessRecord(paths);
|
|
215
|
+
if (!record) {
|
|
216
|
+
await removeFile(paths.processRecord);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
if (!processIsAlive2(record.pid)) {
|
|
220
|
+
await removeFile(paths.processRecord);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return { record, live: await fetchLiveness(record) };
|
|
224
|
+
}
|
|
225
|
+
async function waitForExit(pid, timeoutMs) {
|
|
226
|
+
const deadline = Date.now() + timeoutMs;
|
|
227
|
+
while (Date.now() < deadline) {
|
|
228
|
+
if (!processIsAlive2(pid)) return true;
|
|
229
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
230
|
+
}
|
|
231
|
+
return !processIsAlive2(pid);
|
|
232
|
+
}
|
|
233
|
+
async function signalProcessTree(pid, signal) {
|
|
234
|
+
if (process.platform !== "win32") {
|
|
235
|
+
try {
|
|
236
|
+
process.kill(-pid, signal);
|
|
237
|
+
return;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (error.code !== "ESRCH") throw error;
|
|
240
|
+
try {
|
|
241
|
+
process.kill(pid, signal);
|
|
242
|
+
} catch (fallbackError) {
|
|
243
|
+
if (fallbackError.code !== "ESRCH") {
|
|
244
|
+
throw fallbackError;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
await new Promise((resolve, reject) => {
|
|
251
|
+
const child = spawn(
|
|
252
|
+
"taskkill",
|
|
253
|
+
[
|
|
254
|
+
"/PID",
|
|
255
|
+
String(pid),
|
|
256
|
+
"/T",
|
|
257
|
+
"/F"
|
|
258
|
+
],
|
|
259
|
+
{ stdio: "ignore", windowsHide: true }
|
|
260
|
+
);
|
|
261
|
+
child.once("error", reject);
|
|
262
|
+
child.once("exit", (code) => {
|
|
263
|
+
if (code === 0 || !processIsAlive2(pid)) resolve();
|
|
264
|
+
else reject(new Error(`taskkill exited with code ${code}`));
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
async function stopSwitchy(paths, force = false) {
|
|
269
|
+
const running = await getRunningProcess(paths);
|
|
270
|
+
if (!running) return false;
|
|
271
|
+
if (!force && (!running.live || running.live.instanceId !== running.record.instanceId)) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
"The recorded process could not be verified as Switchy. Re-run with stop --force."
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
await signalProcessTree(
|
|
277
|
+
running.record.pid,
|
|
278
|
+
force ? "SIGKILL" : "SIGTERM"
|
|
279
|
+
);
|
|
280
|
+
if (!await waitForExit(running.record.pid, SHUTDOWN_TIMEOUT_MS)) {
|
|
281
|
+
throw new Error("Switchy did not stop in time. Re-run with stop --force.");
|
|
282
|
+
}
|
|
283
|
+
await removeFile(paths.processRecord);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
async function waitUntilReady(record) {
|
|
287
|
+
const deadline = Date.now() + STARTUP_TIMEOUT_MS;
|
|
288
|
+
while (Date.now() < deadline) {
|
|
289
|
+
if (!processIsAlive2(record.pid)) {
|
|
290
|
+
throw new Error("Switchy exited before becoming ready");
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const response = await fetch(
|
|
294
|
+
`http://${record.hostname}:${record.port}/api/health/ready`,
|
|
295
|
+
{ signal: AbortSignal.timeout(2e3) }
|
|
296
|
+
);
|
|
297
|
+
if (response.ok) return;
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
301
|
+
}
|
|
302
|
+
throw new Error("Switchy did not become ready within 60 seconds");
|
|
303
|
+
}
|
|
304
|
+
async function startSwitchyProcess({
|
|
305
|
+
paths,
|
|
306
|
+
runtimeDirectory,
|
|
307
|
+
version,
|
|
308
|
+
port,
|
|
309
|
+
foreground
|
|
310
|
+
}) {
|
|
311
|
+
const instanceId = randomUUID2();
|
|
312
|
+
const environment = {
|
|
313
|
+
...process.env,
|
|
314
|
+
NODE_ENV: "production",
|
|
315
|
+
HOSTNAME: DEFAULT_HOSTNAME,
|
|
316
|
+
PORT: String(port),
|
|
317
|
+
SWITCHY_HOME: paths.root,
|
|
318
|
+
SWITCHY_INSTANCE_ID: instanceId,
|
|
319
|
+
PLAYWRIGHT_BROWSERS_PATH: paths.playwright
|
|
320
|
+
};
|
|
321
|
+
const serverPath = path2.join(runtimeDirectory, "server.js");
|
|
322
|
+
const logHandle = foreground ? null : await open(paths.logFile, "a", 384);
|
|
323
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
324
|
+
cwd: runtimeDirectory,
|
|
325
|
+
detached: !foreground,
|
|
326
|
+
env: environment,
|
|
327
|
+
stdio: foreground ? "inherit" : ["ignore", logHandle.fd, logHandle.fd]
|
|
328
|
+
});
|
|
329
|
+
await logHandle?.close();
|
|
330
|
+
if (!child.pid) throw new Error("Switchy did not return a process ID");
|
|
331
|
+
const record = {
|
|
332
|
+
schemaVersion: 1,
|
|
333
|
+
pid: child.pid,
|
|
334
|
+
instanceId,
|
|
335
|
+
version,
|
|
336
|
+
hostname: DEFAULT_HOSTNAME,
|
|
337
|
+
port,
|
|
338
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
339
|
+
};
|
|
340
|
+
await writeJsonFileAtomic(paths.processRecord, record);
|
|
341
|
+
if (foreground) {
|
|
342
|
+
await new Promise((resolve, reject) => {
|
|
343
|
+
child.once("error", reject);
|
|
344
|
+
child.once("exit", (code) => {
|
|
345
|
+
if (code === 0 || code === null) resolve();
|
|
346
|
+
else reject(new Error(`Switchy exited with code ${code}`));
|
|
347
|
+
});
|
|
348
|
+
}).finally(() => removeFile(paths.processRecord));
|
|
349
|
+
return record;
|
|
350
|
+
}
|
|
351
|
+
child.unref();
|
|
352
|
+
try {
|
|
353
|
+
await waitUntilReady(record);
|
|
354
|
+
return record;
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (processIsAlive2(record.pid)) {
|
|
357
|
+
await signalProcessTree(record.pid, "SIGTERM");
|
|
358
|
+
if (!await waitForExit(record.pid, SHUTDOWN_TIMEOUT_MS)) {
|
|
359
|
+
await signalProcessTree(record.pid, "SIGKILL");
|
|
360
|
+
if (!await waitForExit(record.pid, SHUTDOWN_TIMEOUT_MS)) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`Switchy failed to start and process ${record.pid} could not be stopped`,
|
|
363
|
+
{ cause: error }
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
await removeFile(paths.processRecord);
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/paths.ts
|
|
374
|
+
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
375
|
+
import os from "node:os";
|
|
376
|
+
import path3 from "node:path";
|
|
377
|
+
function getSwitchyPaths(configuredRoot = process.env.SWITCHY_HOME) {
|
|
378
|
+
const override = configuredRoot?.trim();
|
|
379
|
+
if (override && !path3.isAbsolute(override)) {
|
|
380
|
+
throw new Error("SWITCHY_HOME must be an absolute path");
|
|
381
|
+
}
|
|
382
|
+
const root = override ? path3.normalize(override) : path3.join(os.homedir(), ".switchy");
|
|
383
|
+
const data = path3.join(root, "data");
|
|
384
|
+
const app = path3.join(root, "app");
|
|
385
|
+
const runtime = path3.join(root, "runtime");
|
|
386
|
+
const logs = path3.join(root, "logs");
|
|
387
|
+
const cache = path3.join(root, "cache");
|
|
388
|
+
return {
|
|
389
|
+
root,
|
|
390
|
+
data,
|
|
391
|
+
productionData: path3.join(data, "production"),
|
|
392
|
+
app,
|
|
393
|
+
versions: path3.join(app, "versions"),
|
|
394
|
+
currentVersion: path3.join(app, "current.json"),
|
|
395
|
+
runtime,
|
|
396
|
+
processRecord: path3.join(runtime, "process.json"),
|
|
397
|
+
installLock: path3.join(runtime, "install.lock"),
|
|
398
|
+
startLock: path3.join(runtime, "start.lock"),
|
|
399
|
+
logs,
|
|
400
|
+
logFile: path3.join(logs, "switchy.log"),
|
|
401
|
+
cache,
|
|
402
|
+
downloads: path3.join(cache, "downloads"),
|
|
403
|
+
playwright: path3.join(cache, "playwright"),
|
|
404
|
+
updateSnapshots: path3.join(root, "update-snapshots")
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
async function ensureSwitchyDirectories(paths) {
|
|
408
|
+
for (const directory of [
|
|
409
|
+
paths.root,
|
|
410
|
+
paths.data,
|
|
411
|
+
paths.app,
|
|
412
|
+
paths.versions,
|
|
413
|
+
paths.runtime,
|
|
414
|
+
paths.logs,
|
|
415
|
+
paths.cache,
|
|
416
|
+
paths.downloads,
|
|
417
|
+
paths.playwright,
|
|
418
|
+
paths.updateSnapshots
|
|
419
|
+
]) {
|
|
420
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/release.ts
|
|
425
|
+
import { createHash, randomUUID as randomUUID3 } from "node:crypto";
|
|
426
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
427
|
+
import {
|
|
428
|
+
cp,
|
|
429
|
+
mkdir as mkdir3,
|
|
430
|
+
readFile as readFile2,
|
|
431
|
+
rename as rename2,
|
|
432
|
+
rm as rm2,
|
|
433
|
+
stat as stat2
|
|
434
|
+
} from "node:fs/promises";
|
|
435
|
+
import { Readable, Transform } from "node:stream";
|
|
436
|
+
import { pipeline } from "node:stream/promises";
|
|
437
|
+
import path4 from "node:path";
|
|
438
|
+
import { extract } from "tar";
|
|
439
|
+
|
|
440
|
+
// src/platform.ts
|
|
441
|
+
var SUPPORTED_TARGETS = [
|
|
442
|
+
"darwin-arm64",
|
|
443
|
+
"darwin-x64",
|
|
444
|
+
"linux-arm64",
|
|
445
|
+
"linux-x64",
|
|
446
|
+
"win32-x64"
|
|
447
|
+
];
|
|
448
|
+
var supportedTargets = new Set(SUPPORTED_TARGETS);
|
|
449
|
+
function isSupportedTarget(target) {
|
|
450
|
+
return supportedTargets.has(target);
|
|
451
|
+
}
|
|
452
|
+
function currentTarget(platform = process.platform, architecture = process.arch) {
|
|
453
|
+
const target = `${platform}-${architecture}`;
|
|
454
|
+
if (!isSupportedTarget(target)) {
|
|
455
|
+
throw new Error(`Switchy does not provide a runtime for ${target}`);
|
|
456
|
+
}
|
|
457
|
+
return target;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/release.ts
|
|
461
|
+
var STABLE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/u;
|
|
462
|
+
function assertStableVersion(version) {
|
|
463
|
+
if (!STABLE_VERSION_PATTERN.test(version)) {
|
|
464
|
+
throw new Error(`Invalid stable Switchy version: ${version}`);
|
|
465
|
+
}
|
|
466
|
+
return version;
|
|
467
|
+
}
|
|
468
|
+
function releaseManifestUrl(version) {
|
|
469
|
+
return process.env.SWITCHY_MANIFEST_URL ?? `https://github.com/${GITHUB_REPOSITORY}/releases/download/v${version}/${RELEASE_MANIFEST_FILE}`;
|
|
470
|
+
}
|
|
471
|
+
function assertReleaseArtifact(value) {
|
|
472
|
+
if (typeof value !== "object" || value === null || typeof value.file !== "string" || path4.basename(value.file) !== value.file || !/^switchy-\d+\.\d+\.\d+-(?:darwin|linux|win32)-(?:arm64|x64)\.tar\.gz$/u.test(value.file) || typeof value.sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(value.sha256) || typeof value.size !== "number" || !Number.isSafeInteger(value.size) || value.size <= 0 || value.size > 1e9) {
|
|
473
|
+
throw new Error("Release manifest contains an invalid artifact");
|
|
474
|
+
}
|
|
475
|
+
return value;
|
|
476
|
+
}
|
|
477
|
+
function parseReleaseManifest(value) {
|
|
478
|
+
if (typeof value !== "object" || value === null || value.schemaVersion !== 1 || typeof value.version !== "string" || !STABLE_VERSION_PATTERN.test(value.version) || typeof value.publishedAt !== "string" || typeof value.nodeVersion !== "string" || typeof value.artifacts !== "object" || value.artifacts === null) {
|
|
479
|
+
throw new Error("Release manifest is invalid");
|
|
480
|
+
}
|
|
481
|
+
const manifest = value;
|
|
482
|
+
if (!Number.isFinite(Date.parse(manifest.publishedAt)) || !/^\d+\.\d+\.\d+$/u.test(manifest.nodeVersion) || Array.isArray(manifest.artifacts)) {
|
|
483
|
+
throw new Error("Release manifest is invalid");
|
|
484
|
+
}
|
|
485
|
+
for (const [target, valueArtifact] of Object.entries(manifest.artifacts)) {
|
|
486
|
+
if (!isSupportedTarget(target)) {
|
|
487
|
+
throw new Error(`Release manifest contains unsupported target ${target}`);
|
|
488
|
+
}
|
|
489
|
+
const artifact = assertReleaseArtifact(valueArtifact);
|
|
490
|
+
if (artifact.file !== `switchy-${manifest.version}-${target}.tar.gz`) {
|
|
491
|
+
throw new Error("Release manifest contains inconsistent artifact metadata");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return manifest;
|
|
495
|
+
}
|
|
496
|
+
async function fetchJson(url) {
|
|
497
|
+
const response = await fetch(url, {
|
|
498
|
+
headers: {
|
|
499
|
+
Accept: "application/json",
|
|
500
|
+
"User-Agent": `switchy-cli/${CLI_VERSION}`
|
|
501
|
+
},
|
|
502
|
+
signal: AbortSignal.timeout(3e4)
|
|
503
|
+
});
|
|
504
|
+
if (!response.ok) {
|
|
505
|
+
throw new Error(`Request failed with ${response.status}: ${url}`);
|
|
506
|
+
}
|
|
507
|
+
return response.json();
|
|
508
|
+
}
|
|
509
|
+
async function fetchReleaseManifest(version) {
|
|
510
|
+
const url = releaseManifestUrl(version);
|
|
511
|
+
const manifest = parseReleaseManifest(await fetchJson(url));
|
|
512
|
+
if (manifest.version !== version) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`Release manifest version ${manifest.version} does not match ${version}`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
return { manifest, url };
|
|
518
|
+
}
|
|
519
|
+
async function resolveLatestStableVersion() {
|
|
520
|
+
const override = process.env.SWITCHY_LATEST_VERSION?.trim();
|
|
521
|
+
if (override) return assertStableVersion(override);
|
|
522
|
+
const value = await fetchJson(
|
|
523
|
+
`https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/latest`
|
|
524
|
+
);
|
|
525
|
+
const tagName = typeof value === "object" && value !== null ? value.tag_name : null;
|
|
526
|
+
if (typeof tagName !== "string" || !/^v\d+\.\d+\.\d+$/u.test(tagName)) {
|
|
527
|
+
throw new Error("Latest GitHub release does not contain a stable version tag");
|
|
528
|
+
}
|
|
529
|
+
return tagName.slice(1);
|
|
530
|
+
}
|
|
531
|
+
async function sha256(filePath) {
|
|
532
|
+
const hash = createHash("sha256");
|
|
533
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
534
|
+
hash.update(chunk);
|
|
535
|
+
}
|
|
536
|
+
return hash.digest("hex");
|
|
537
|
+
}
|
|
538
|
+
async function downloadFile(url, destination, expectedSize) {
|
|
539
|
+
const response = await fetch(url, {
|
|
540
|
+
headers: { "User-Agent": `switchy-cli/${CLI_VERSION}` },
|
|
541
|
+
redirect: "follow",
|
|
542
|
+
signal: AbortSignal.timeout(3e5)
|
|
543
|
+
});
|
|
544
|
+
if (!response.ok || !response.body) {
|
|
545
|
+
throw new Error(`Runtime download failed with ${response.status}`);
|
|
546
|
+
}
|
|
547
|
+
const contentLengthHeader = response.headers.get("content-length");
|
|
548
|
+
const contentLength = Number(contentLengthHeader);
|
|
549
|
+
if (contentLengthHeader !== null && Number.isFinite(contentLength) && contentLength !== expectedSize) {
|
|
550
|
+
throw new Error("Runtime download size does not match the release manifest");
|
|
551
|
+
}
|
|
552
|
+
let downloadedSize = 0;
|
|
553
|
+
const sizeLimit = new Transform({
|
|
554
|
+
transform(chunk, _encoding, callback) {
|
|
555
|
+
downloadedSize += chunk.length;
|
|
556
|
+
if (downloadedSize > expectedSize) {
|
|
557
|
+
callback(new Error("Runtime download exceeded its declared size"));
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
callback(null, chunk);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
await pipeline(
|
|
564
|
+
Readable.fromWeb(response.body),
|
|
565
|
+
sizeLimit,
|
|
566
|
+
createWriteStream(destination, { mode: 384 })
|
|
567
|
+
);
|
|
568
|
+
if (downloadedSize !== expectedSize) {
|
|
569
|
+
throw new Error("Runtime download size does not match the release manifest");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async function validateRuntimeDirectory(runtimeDirectory, version, target) {
|
|
573
|
+
for (const relativePath of [
|
|
574
|
+
"server.js",
|
|
575
|
+
"bin/migrate.cjs",
|
|
576
|
+
"drizzle/meta/_journal.json",
|
|
577
|
+
"switchy-runtime.json"
|
|
578
|
+
]) {
|
|
579
|
+
const fileStats = await stat2(path4.join(runtimeDirectory, relativePath)).catch(() => null);
|
|
580
|
+
if (!fileStats?.isFile()) {
|
|
581
|
+
throw new Error(`Runtime is missing ${relativePath}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
let metadata;
|
|
585
|
+
try {
|
|
586
|
+
metadata = JSON.parse(
|
|
587
|
+
await readFile2(
|
|
588
|
+
path4.join(runtimeDirectory, "switchy-runtime.json"),
|
|
589
|
+
"utf8"
|
|
590
|
+
)
|
|
591
|
+
);
|
|
592
|
+
} catch {
|
|
593
|
+
throw new Error("Runtime metadata is invalid");
|
|
594
|
+
}
|
|
595
|
+
if (metadata.schemaVersion !== 1 || metadata.version !== version || metadata.target !== target || typeof metadata.nodeVersion !== "string" || !/^\d+\.\d+\.\d+$/u.test(metadata.nodeVersion) || metadata.nodeVersion.split(".", 1)[0] !== process.versions.node.split(".", 1)[0] || typeof metadata.builtAt !== "string" || !Number.isFinite(Date.parse(metadata.builtAt))) {
|
|
596
|
+
throw new Error("Runtime metadata does not match this installation");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async function installRuntimeUnlocked(version, paths) {
|
|
600
|
+
assertStableVersion(version);
|
|
601
|
+
const target = currentTarget();
|
|
602
|
+
const versionDirectory = path4.join(paths.versions, version);
|
|
603
|
+
if ((await stat2(versionDirectory).catch(() => null))?.isDirectory()) {
|
|
604
|
+
try {
|
|
605
|
+
await validateRuntimeDirectory(versionDirectory, version, target);
|
|
606
|
+
return versionDirectory;
|
|
607
|
+
} catch {
|
|
608
|
+
await rm2(versionDirectory, { recursive: true, force: true });
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const localRuntime = process.env.SWITCHY_RUNTIME_SOURCE?.trim();
|
|
612
|
+
const temporaryDirectory = path4.join(
|
|
613
|
+
paths.app,
|
|
614
|
+
`.install-${version}-${randomUUID3()}`
|
|
615
|
+
);
|
|
616
|
+
await rm2(temporaryDirectory, { recursive: true, force: true });
|
|
617
|
+
await mkdir3(temporaryDirectory, { recursive: true, mode: 448 });
|
|
618
|
+
try {
|
|
619
|
+
if (localRuntime) {
|
|
620
|
+
await cp(path4.resolve(localRuntime), temporaryDirectory, {
|
|
621
|
+
recursive: true,
|
|
622
|
+
verbatimSymlinks: true
|
|
623
|
+
});
|
|
624
|
+
} else {
|
|
625
|
+
const { manifest, url: manifestUrl } = await fetchReleaseManifest(version);
|
|
626
|
+
const artifact = manifest.artifacts[target];
|
|
627
|
+
if (!artifact) {
|
|
628
|
+
throw new Error(`Release ${version} does not support ${target}`);
|
|
629
|
+
}
|
|
630
|
+
const archivePath = path4.join(paths.downloads, artifact.file);
|
|
631
|
+
const artifactUrl = new URL(artifact.file, manifestUrl).toString();
|
|
632
|
+
const existing = await stat2(archivePath).catch(() => null);
|
|
633
|
+
const cachedArchiveIsValid = existing?.size === artifact.size && await sha256(archivePath) === artifact.sha256;
|
|
634
|
+
if (!cachedArchiveIsValid) {
|
|
635
|
+
await rm2(archivePath, { force: true });
|
|
636
|
+
try {
|
|
637
|
+
await downloadFile(artifactUrl, archivePath, artifact.size);
|
|
638
|
+
} catch (error) {
|
|
639
|
+
await rm2(archivePath, { force: true });
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const downloadedArchive = await stat2(archivePath).catch(() => null);
|
|
644
|
+
if (downloadedArchive?.size !== artifact.size || await sha256(archivePath) !== artifact.sha256) {
|
|
645
|
+
await rm2(archivePath, { force: true });
|
|
646
|
+
throw new Error("Runtime checksum verification failed");
|
|
647
|
+
}
|
|
648
|
+
await extract({
|
|
649
|
+
cwd: temporaryDirectory,
|
|
650
|
+
file: archivePath,
|
|
651
|
+
preservePaths: false,
|
|
652
|
+
strict: true
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
await validateRuntimeDirectory(temporaryDirectory, version, target);
|
|
656
|
+
await rename2(temporaryDirectory, versionDirectory);
|
|
657
|
+
return versionDirectory;
|
|
658
|
+
} catch (error) {
|
|
659
|
+
await rm2(temporaryDirectory, { recursive: true, force: true });
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async function installRuntime(version, paths) {
|
|
664
|
+
return withDirectoryLock(paths.installLock, async () => {
|
|
665
|
+
return installRuntimeUnlocked(version, paths);
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
async function setCurrentVersion(paths, version) {
|
|
669
|
+
const value = {
|
|
670
|
+
schemaVersion: 1,
|
|
671
|
+
version,
|
|
672
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
673
|
+
};
|
|
674
|
+
await writeJsonFileAtomic(paths.currentVersion, value);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/runtime.ts
|
|
678
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
679
|
+
import { cp as cp2, mkdir as mkdir4, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
|
|
680
|
+
import path5 from "node:path";
|
|
681
|
+
async function run(executable, args, options) {
|
|
682
|
+
await new Promise((resolve, reject) => {
|
|
683
|
+
const child = spawn2(executable, args, {
|
|
684
|
+
cwd: options.cwd,
|
|
685
|
+
env: options.env,
|
|
686
|
+
stdio: options.quiet ? "ignore" : "inherit"
|
|
687
|
+
});
|
|
688
|
+
child.once("error", reject);
|
|
689
|
+
child.once("exit", (code) => {
|
|
690
|
+
if (code === 0) resolve();
|
|
691
|
+
else reject(new Error(`${path5.basename(executable)} exited with code ${code}`));
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
async function ensurePlaywrightBrowser(runtimeDirectory, paths) {
|
|
696
|
+
if (process.env.SWITCHY_SKIP_BROWSER_INSTALL === "1") return;
|
|
697
|
+
const cliPath = path5.join(
|
|
698
|
+
runtimeDirectory,
|
|
699
|
+
"node_modules",
|
|
700
|
+
"playwright",
|
|
701
|
+
"cli.js"
|
|
702
|
+
);
|
|
703
|
+
if (!(await stat3(cliPath).catch(() => null))?.isFile()) {
|
|
704
|
+
throw new Error("The packaged runtime is missing the Playwright installer");
|
|
705
|
+
}
|
|
706
|
+
await run(process.execPath, [cliPath, "install", "chromium"], {
|
|
707
|
+
cwd: runtimeDirectory,
|
|
708
|
+
env: {
|
|
709
|
+
...process.env,
|
|
710
|
+
PLAYWRIGHT_BROWSERS_PATH: paths.playwright
|
|
711
|
+
},
|
|
712
|
+
quiet: true
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
async function runDatabaseMigration(runtimeDirectory, paths) {
|
|
716
|
+
await run(
|
|
717
|
+
process.execPath,
|
|
718
|
+
[path5.join(runtimeDirectory, "bin", "migrate.cjs")],
|
|
719
|
+
{
|
|
720
|
+
cwd: runtimeDirectory,
|
|
721
|
+
env: {
|
|
722
|
+
...process.env,
|
|
723
|
+
NODE_ENV: "production",
|
|
724
|
+
SWITCHY_HOME: paths.root
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
async function createUpdateSnapshot(paths, fromVersion, toVersion) {
|
|
730
|
+
if (!(await stat3(paths.productionData).catch(() => null))?.isDirectory()) {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(/[:.]/gu, "-");
|
|
734
|
+
const snapshotDirectory = path5.join(
|
|
735
|
+
paths.updateSnapshots,
|
|
736
|
+
`update-${fromVersion}-to-${toVersion}-${timestamp}`
|
|
737
|
+
);
|
|
738
|
+
await mkdir4(snapshotDirectory, { recursive: true, mode: 448 });
|
|
739
|
+
await cp2(paths.productionData, path5.join(snapshotDirectory, "production"), {
|
|
740
|
+
recursive: true
|
|
741
|
+
});
|
|
742
|
+
await writeFile2(
|
|
743
|
+
path5.join(snapshotDirectory, "update.json"),
|
|
744
|
+
`${JSON.stringify({
|
|
745
|
+
fromVersion,
|
|
746
|
+
toVersion,
|
|
747
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
748
|
+
}, null, 2)}
|
|
749
|
+
`,
|
|
750
|
+
{ mode: 384 }
|
|
751
|
+
);
|
|
752
|
+
return snapshotDirectory;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/commands.ts
|
|
756
|
+
async function installedVersion() {
|
|
757
|
+
const paths = getSwitchyPaths();
|
|
758
|
+
const current = await readJsonFile(paths.currentVersion);
|
|
759
|
+
return current?.schemaVersion === 1 ? current.version : null;
|
|
760
|
+
}
|
|
761
|
+
async function startCommand({
|
|
762
|
+
version,
|
|
763
|
+
port = DEFAULT_PORT,
|
|
764
|
+
foreground = false
|
|
765
|
+
} = {}) {
|
|
766
|
+
const paths = getSwitchyPaths();
|
|
767
|
+
await ensureSwitchyDirectories(paths);
|
|
768
|
+
await withDirectoryLock(
|
|
769
|
+
paths.startLock,
|
|
770
|
+
() => startCommandUnlocked({ paths, version, port, foreground })
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
async function startCommandUnlocked({
|
|
774
|
+
paths,
|
|
775
|
+
version,
|
|
776
|
+
port,
|
|
777
|
+
foreground
|
|
778
|
+
}) {
|
|
779
|
+
const existing = await getRunningProcess(paths);
|
|
780
|
+
if (existing) {
|
|
781
|
+
console.log(
|
|
782
|
+
`Switchy ${existing.record.version} is already running at http://${existing.record.hostname}:${existing.record.port}`
|
|
783
|
+
);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const selectedVersion = resolveApplicationVersion(version);
|
|
787
|
+
const runtimeDirectory = await installRuntime(selectedVersion, paths);
|
|
788
|
+
await ensurePlaywrightBrowser(runtimeDirectory, paths);
|
|
789
|
+
await runDatabaseMigration(runtimeDirectory, paths);
|
|
790
|
+
await setCurrentVersion(paths, selectedVersion);
|
|
791
|
+
const record = await startSwitchyProcess({
|
|
792
|
+
paths,
|
|
793
|
+
runtimeDirectory,
|
|
794
|
+
version: selectedVersion,
|
|
795
|
+
port,
|
|
796
|
+
foreground
|
|
797
|
+
});
|
|
798
|
+
if (!foreground) {
|
|
799
|
+
console.log(
|
|
800
|
+
`Switchy ${selectedVersion} is running at http://${record.hostname}:${record.port}`
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
async function stopCommand(force = false) {
|
|
805
|
+
const paths = getSwitchyPaths();
|
|
806
|
+
await ensureSwitchyDirectories(paths);
|
|
807
|
+
await withDirectoryLock(paths.startLock, async () => {
|
|
808
|
+
if (await stopSwitchy(paths, force)) console.log("Switchy stopped.");
|
|
809
|
+
else console.log("Switchy is not running.");
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
async function statusCommand() {
|
|
813
|
+
const paths = getSwitchyPaths();
|
|
814
|
+
await ensureSwitchyDirectories(paths);
|
|
815
|
+
const running = await getRunningProcess(paths);
|
|
816
|
+
if (!running) {
|
|
817
|
+
console.log("Switchy is stopped.");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const live = running.live ?? await fetchLiveness(running.record);
|
|
821
|
+
console.log(`Switchy ${running.record.version} is running.`);
|
|
822
|
+
console.log(`URL: http://${running.record.hostname}:${running.record.port}`);
|
|
823
|
+
console.log(`PID: ${running.record.pid}`);
|
|
824
|
+
console.log(`Health: ${live ? "Live" : "Starting or unavailable"}`);
|
|
825
|
+
}
|
|
826
|
+
async function updateCommand() {
|
|
827
|
+
const paths = getSwitchyPaths();
|
|
828
|
+
await ensureSwitchyDirectories(paths);
|
|
829
|
+
await withDirectoryLock(paths.startLock, async () => {
|
|
830
|
+
const latestVersion = await resolveLatestStableVersion();
|
|
831
|
+
const currentVersion = await installedVersion();
|
|
832
|
+
if (currentVersion === latestVersion) {
|
|
833
|
+
console.log(`Switchy ${latestVersion} is already current.`);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const wasRunning = await getRunningProcess(paths);
|
|
837
|
+
await installRuntime(latestVersion, paths);
|
|
838
|
+
if (wasRunning) await stopSwitchy(paths);
|
|
839
|
+
const snapshot = currentVersion ? await createUpdateSnapshot(paths, currentVersion, latestVersion) : null;
|
|
840
|
+
try {
|
|
841
|
+
await startCommandUnlocked({
|
|
842
|
+
paths,
|
|
843
|
+
version: latestVersion,
|
|
844
|
+
port: wasRunning?.record.port ?? DEFAULT_PORT,
|
|
845
|
+
foreground: false
|
|
846
|
+
});
|
|
847
|
+
} catch (error) {
|
|
848
|
+
if (snapshot) {
|
|
849
|
+
console.error(`Update snapshot retained at ${snapshot}`);
|
|
850
|
+
}
|
|
851
|
+
throw error;
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
async function logsCommand() {
|
|
856
|
+
const paths = getSwitchyPaths();
|
|
857
|
+
const contents = await readFile3(paths.logFile, "utf8").catch((error) => {
|
|
858
|
+
if (error.code === "ENOENT") return "";
|
|
859
|
+
throw error;
|
|
860
|
+
});
|
|
861
|
+
const lines = contents.trimEnd().split("\n");
|
|
862
|
+
console.log(lines.slice(-200).join("\n"));
|
|
863
|
+
}
|
|
864
|
+
async function openCommand() {
|
|
865
|
+
const paths = getSwitchyPaths();
|
|
866
|
+
const running = await getRunningProcess(paths);
|
|
867
|
+
if (!running) throw new Error("Switchy is not running");
|
|
868
|
+
const url = `http://${running.record.hostname}:${running.record.port}`;
|
|
869
|
+
let executable;
|
|
870
|
+
let args;
|
|
871
|
+
if (process.platform === "darwin") {
|
|
872
|
+
executable = "open";
|
|
873
|
+
args = [url];
|
|
874
|
+
} else if (process.platform === "win32") {
|
|
875
|
+
executable = "cmd";
|
|
876
|
+
args = ["/c", "start", "", url];
|
|
877
|
+
} else {
|
|
878
|
+
executable = "xdg-open";
|
|
879
|
+
args = [url];
|
|
880
|
+
}
|
|
881
|
+
const child = spawn3(executable, args, {
|
|
882
|
+
detached: true,
|
|
883
|
+
stdio: "ignore"
|
|
884
|
+
});
|
|
885
|
+
child.unref();
|
|
886
|
+
console.log(url);
|
|
887
|
+
}
|
|
888
|
+
function versionCommand() {
|
|
889
|
+
console.log(CLI_VERSION);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/cli.ts
|
|
893
|
+
function optionValue(args, name) {
|
|
894
|
+
const index = args.indexOf(name);
|
|
895
|
+
if (index < 0) return void 0;
|
|
896
|
+
const value = args[index + 1];
|
|
897
|
+
if (!value || value.startsWith("--")) {
|
|
898
|
+
throw new Error(`${name} requires a value`);
|
|
899
|
+
}
|
|
900
|
+
return value;
|
|
901
|
+
}
|
|
902
|
+
function parsePort(args) {
|
|
903
|
+
const value = optionValue(args, "--port");
|
|
904
|
+
if (!value) return void 0;
|
|
905
|
+
const port = Number(value);
|
|
906
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
907
|
+
throw new Error("--port must be an integer between 1 and 65535");
|
|
908
|
+
}
|
|
909
|
+
return port;
|
|
910
|
+
}
|
|
911
|
+
function printHelp() {
|
|
912
|
+
console.log(`Switchy CLI
|
|
913
|
+
|
|
914
|
+
Usage:
|
|
915
|
+
switchy start [--port 3000] [--foreground] [--app-version <version>]
|
|
916
|
+
switchy stop [--force]
|
|
917
|
+
switchy status
|
|
918
|
+
switchy update
|
|
919
|
+
switchy logs
|
|
920
|
+
switchy open
|
|
921
|
+
switchy version`);
|
|
922
|
+
}
|
|
923
|
+
async function main() {
|
|
924
|
+
assertSupportedNodeVersion();
|
|
925
|
+
const args = process.argv.slice(2);
|
|
926
|
+
const command = args[0] ?? "help";
|
|
927
|
+
switch (command) {
|
|
928
|
+
case "start":
|
|
929
|
+
await startCommand({
|
|
930
|
+
version: optionValue(args, "--app-version"),
|
|
931
|
+
port: parsePort(args),
|
|
932
|
+
foreground: args.includes("--foreground")
|
|
933
|
+
});
|
|
934
|
+
break;
|
|
935
|
+
case "stop":
|
|
936
|
+
await stopCommand(args.includes("--force"));
|
|
937
|
+
break;
|
|
938
|
+
case "status":
|
|
939
|
+
await statusCommand();
|
|
940
|
+
break;
|
|
941
|
+
case "update":
|
|
942
|
+
await updateCommand();
|
|
943
|
+
break;
|
|
944
|
+
case "logs":
|
|
945
|
+
await logsCommand();
|
|
946
|
+
break;
|
|
947
|
+
case "open":
|
|
948
|
+
await openCommand();
|
|
949
|
+
break;
|
|
950
|
+
case "version":
|
|
951
|
+
case "--version":
|
|
952
|
+
case "-v":
|
|
953
|
+
versionCommand();
|
|
954
|
+
break;
|
|
955
|
+
case "help":
|
|
956
|
+
case "--help":
|
|
957
|
+
case "-h":
|
|
958
|
+
printHelp();
|
|
959
|
+
break;
|
|
960
|
+
default:
|
|
961
|
+
throw new Error(`Unknown command: ${command}`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
main().catch((error) => {
|
|
965
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
966
|
+
process.exitCode = 1;
|
|
967
|
+
});
|
|
968
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/commands.ts", "../package.json", "../src/config.ts", "../src/files.ts", "../src/process-manager.ts", "../src/paths.ts", "../src/release.ts", "../src/platform.ts", "../src/runtime.ts", "../src/cli.ts"],
|
|
4
|
+
"sourcesContent": ["import { readFile } from \"node:fs/promises\";\nimport { spawn } from \"node:child_process\";\n\nimport {\n CLI_VERSION,\n DEFAULT_PORT,\n resolveApplicationVersion,\n} from \"./config.js\";\nimport {\n readJsonFile,\n withDirectoryLock,\n} from \"./files.js\";\nimport {\n fetchLiveness,\n getRunningProcess,\n startSwitchyProcess,\n stopSwitchy,\n} from \"./process-manager.js\";\nimport {\n ensureSwitchyDirectories,\n getSwitchyPaths,\n} from \"./paths.js\";\nimport {\n installRuntime,\n resolveLatestStableVersion,\n setCurrentVersion,\n} from \"./release.js\";\nimport {\n createUpdateSnapshot,\n ensurePlaywrightBrowser,\n runDatabaseMigration,\n} from \"./runtime.js\";\nimport type { CurrentVersionRecord } from \"./types.js\";\n\nasync function installedVersion(): Promise<string | null> {\n const paths = getSwitchyPaths();\n const current = await readJsonFile<CurrentVersionRecord>(paths.currentVersion);\n return current?.schemaVersion === 1 ? current.version : null;\n}\n\nexport async function startCommand({\n version,\n port = DEFAULT_PORT,\n foreground = false,\n}: {\n version?: string;\n port?: number;\n foreground?: boolean;\n} = {}): Promise<void> {\n const paths = getSwitchyPaths();\n await ensureSwitchyDirectories(paths);\n await withDirectoryLock(paths.startLock, () =>\n startCommandUnlocked({ paths, version, port, foreground })\n );\n}\n\nasync function startCommandUnlocked({\n paths,\n version,\n port,\n foreground,\n}: {\n paths: ReturnType<typeof getSwitchyPaths>;\n version?: string;\n port: number;\n foreground: boolean;\n}): Promise<void> {\n const existing = await getRunningProcess(paths);\n if (existing) {\n console.log(\n `Switchy ${existing.record.version} is already running at `\n + `http://${existing.record.hostname}:${existing.record.port}`\n );\n return;\n }\n\n const selectedVersion = resolveApplicationVersion(version);\n const runtimeDirectory = await installRuntime(selectedVersion, paths);\n await ensurePlaywrightBrowser(runtimeDirectory, paths);\n await runDatabaseMigration(runtimeDirectory, paths);\n await setCurrentVersion(paths, selectedVersion);\n const record = await startSwitchyProcess({\n paths,\n runtimeDirectory,\n version: selectedVersion,\n port,\n foreground,\n });\n if (!foreground) {\n console.log(\n `Switchy ${selectedVersion} is running at `\n + `http://${record.hostname}:${record.port}`\n );\n }\n}\n\nexport async function stopCommand(force = false): Promise<void> {\n const paths = getSwitchyPaths();\n await ensureSwitchyDirectories(paths);\n await withDirectoryLock(paths.startLock, async () => {\n if (await stopSwitchy(paths, force)) console.log(\"Switchy stopped.\");\n else console.log(\"Switchy is not running.\");\n });\n}\n\nexport async function statusCommand(): Promise<void> {\n const paths = getSwitchyPaths();\n await ensureSwitchyDirectories(paths);\n const running = await getRunningProcess(paths);\n if (!running) {\n console.log(\"Switchy is stopped.\");\n return;\n }\n const live = running.live ?? await fetchLiveness(running.record);\n console.log(`Switchy ${running.record.version} is running.`);\n console.log(`URL: http://${running.record.hostname}:${running.record.port}`);\n console.log(`PID: ${running.record.pid}`);\n console.log(`Health: ${live ? \"Live\" : \"Starting or unavailable\"}`);\n}\n\nexport async function updateCommand(): Promise<void> {\n const paths = getSwitchyPaths();\n await ensureSwitchyDirectories(paths);\n await withDirectoryLock(paths.startLock, async () => {\n const latestVersion = await resolveLatestStableVersion();\n const currentVersion = await installedVersion();\n if (currentVersion === latestVersion) {\n console.log(`Switchy ${latestVersion} is already current.`);\n return;\n }\n\n const wasRunning = await getRunningProcess(paths);\n await installRuntime(latestVersion, paths);\n if (wasRunning) await stopSwitchy(paths);\n const snapshot = currentVersion\n ? await createUpdateSnapshot(paths, currentVersion, latestVersion)\n : null;\n try {\n await startCommandUnlocked({\n paths,\n version: latestVersion,\n port: wasRunning?.record.port ?? DEFAULT_PORT,\n foreground: false,\n });\n } catch (error) {\n if (snapshot) {\n console.error(`Update snapshot retained at ${snapshot}`);\n }\n throw error;\n }\n });\n}\n\nexport async function logsCommand(): Promise<void> {\n const paths = getSwitchyPaths();\n const contents = await readFile(paths.logFile, \"utf8\").catch((error: unknown) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return \"\";\n throw error;\n });\n const lines = contents.trimEnd().split(\"\\n\");\n console.log(lines.slice(-200).join(\"\\n\"));\n}\n\nexport async function openCommand(): Promise<void> {\n const paths = getSwitchyPaths();\n const running = await getRunningProcess(paths);\n if (!running) throw new Error(\"Switchy is not running\");\n const url = `http://${running.record.hostname}:${running.record.port}`;\n let executable: string;\n let args: string[];\n if (process.platform === \"darwin\") {\n executable = \"open\";\n args = [url];\n } else if (process.platform === \"win32\") {\n executable = \"cmd\";\n args = [\"/c\", \"start\", \"\", url];\n } else {\n executable = \"xdg-open\";\n args = [url];\n }\n const child = spawn(executable, args, {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n console.log(url);\n}\n\nexport function versionCommand(): void {\n console.log(CLI_VERSION);\n}\n", "{\n \"name\": \"@chandresh726/switchy\",\n \"version\": \"1.0.6\",\n \"description\": \"Install, run, update, and stop the local-first Switchy application.\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"bin\": {\n \"switchy\": \"dist/cli.js\"\n },\n \"files\": [\n \"dist\",\n \"README.md\"\n ],\n \"engines\": {\n \"node\": \">=24 <25\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/Chandresh726/Switchy.git\",\n \"directory\": \"packages/cli\"\n },\n \"homepage\": \"https://github.com/Chandresh726/Switchy#readme\",\n \"bugs\": {\n \"url\": \"https://github.com/Chandresh726/Switchy/issues\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"build\": \"node scripts/build.mjs\",\n \"typecheck\": \"tsc --noEmit -p tsconfig.json\",\n \"prepack\": \"pnpm build\"\n },\n \"dependencies\": {\n \"tar\": \"^7.5.22\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^24.13.3\",\n \"esbuild\": \"^0.28.1\",\n \"typescript\": \"^5.9.3\"\n }\n}\n", "import packageJson from \"../package.json\" with { type: \"json\" };\n\nexport const CLI_VERSION = packageJson.version;\nexport const DEFAULT_PORT = 3000;\nexport const DEFAULT_HOSTNAME = \"127.0.0.1\";\nexport const GITHUB_REPOSITORY = \"Chandresh726/Switchy\";\nexport const RELEASE_MANIFEST_FILE = \"switchy-manifest.json\";\nexport const STARTUP_TIMEOUT_MS = 60_000;\nexport const SHUTDOWN_TIMEOUT_MS = 15_000;\n\nexport function resolveApplicationVersion(requestedVersion?: string): string {\n return requestedVersion ?? CLI_VERSION;\n}\n\nexport function assertSupportedNodeVersion(\n nodeVersion = process.versions.node\n): void {\n if (!/^24\\.\\d+\\.\\d+$/u.test(nodeVersion)) {\n throw new Error(\n `Switchy requires Node.js 24; current version is ${nodeVersion}`\n );\n }\n}\n", "import { randomUUID } from \"node:crypto\";\nimport {\n mkdir,\n readFile,\n rename,\n rm,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport path from \"node:path\";\n\nconst LOCK_TIMEOUT_MS = 60_000;\nconst INVALID_LOCK_GRACE_MS = 30_000;\nconst LOCK_OWNER_FILE = \"owner.json\";\n\ninterface LockOwner {\n pid: number;\n token: string;\n}\n\nfunction processIsAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\nasync function readLockOwner(lockPath: string): Promise<LockOwner | null> {\n try {\n const value = JSON.parse(\n await readFile(path.join(lockPath, LOCK_OWNER_FILE), \"utf8\")\n ) as Partial<LockOwner>;\n if (\n !Number.isInteger(value.pid)\n || (value.pid ?? 0) <= 0\n || typeof value.token !== \"string\"\n || value.token.length === 0\n ) {\n return null;\n }\n return value as LockOwner;\n } catch {\n return null;\n }\n}\n\nasync function removeOwnedLock(\n lockPath: string,\n token: string\n): Promise<void> {\n const owner = await readLockOwner(lockPath);\n if (owner?.token === token) {\n await rm(lockPath, { recursive: true, force: true });\n }\n}\n\nexport async function readJsonFile<T>(filePath: string): Promise<T | null> {\n try {\n return JSON.parse(await readFile(filePath, \"utf8\")) as T;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n }\n}\n\nexport async function writeJsonFileAtomic(\n filePath: string,\n value: unknown\n): Promise<void> {\n const temporaryPath = path.join(\n path.dirname(filePath),\n `.${path.basename(filePath)}.${randomUUID()}.tmp`\n );\n await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\\n`, {\n mode: 0o600,\n });\n await rename(temporaryPath, filePath);\n}\n\nexport async function removeFile(filePath: string): Promise<void> {\n await rm(filePath, { force: true });\n}\n\nexport async function withDirectoryLock<T>(\n lockPath: string,\n operation: () => Promise<T>\n): Promise<T> {\n await mkdir(path.dirname(lockPath), { recursive: true, mode: 0o700 });\n const deadline = Date.now() + LOCK_TIMEOUT_MS;\n const token = randomUUID();\n while (true) {\n try {\n await mkdir(lockPath, { mode: 0o700 });\n try {\n await writeFile(\n path.join(lockPath, LOCK_OWNER_FILE),\n `${JSON.stringify({ pid: process.pid, token })}\\n`,\n { mode: 0o600 }\n );\n } catch (error) {\n await rm(lockPath, { recursive: true, force: true });\n throw error;\n }\n break;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n const [owner, lockStats] = await Promise.all([\n readLockOwner(lockPath),\n stat(lockPath).catch(() => null),\n ]);\n if (\n (owner && !processIsAlive(owner.pid))\n || (\n !owner\n && lockStats\n && Date.now() - lockStats.mtimeMs > INVALID_LOCK_GRACE_MS\n )\n ) {\n await rm(lockPath, { recursive: true, force: true });\n continue;\n }\n if (Date.now() >= deadline) {\n throw new Error(`Timed out waiting for ${path.basename(lockPath)}`);\n }\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n }\n\n try {\n return await operation();\n } finally {\n await removeOwnedLock(lockPath, token);\n }\n}\n", "import { open } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { spawn } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\n\nimport {\n DEFAULT_HOSTNAME,\n SHUTDOWN_TIMEOUT_MS,\n STARTUP_TIMEOUT_MS,\n} from \"./config.js\";\nimport {\n readJsonFile,\n removeFile,\n writeJsonFileAtomic,\n} from \"./files.js\";\nimport type { ProcessRecord, SwitchyPaths } from \"./types.js\";\n\nfunction processIsAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\nasync function readProcessRecord(\n paths: SwitchyPaths\n): Promise<ProcessRecord | null> {\n const record = await readJsonFile<ProcessRecord>(paths.processRecord)\n .catch(() => null);\n if (\n !record\n || record.schemaVersion !== 1\n || !Number.isInteger(record.pid)\n || record.pid <= 0\n || typeof record.instanceId !== \"string\"\n || record.instanceId.length === 0\n || !/^\\d+\\.\\d+\\.\\d+$/u.test(record.version)\n || record.hostname !== DEFAULT_HOSTNAME\n || !Number.isInteger(record.port)\n || record.port < 1\n || record.port > 65_535\n || !Number.isFinite(Date.parse(record.startedAt))\n ) {\n return null;\n }\n return record;\n}\n\nexport async function fetchLiveness(record: ProcessRecord): Promise<{\n status: \"live\";\n version: string;\n instanceId: string | null;\n} | null> {\n try {\n const response = await fetch(\n `http://${record.hostname}:${record.port}/api/health/live`,\n { signal: AbortSignal.timeout(2_000) }\n );\n if (!response.ok) return null;\n const value = await response.json() as {\n status?: unknown;\n version?: unknown;\n instanceId?: unknown;\n };\n if (value.status !== \"live\" || typeof value.version !== \"string\") return null;\n return {\n status: \"live\",\n version: value.version,\n instanceId: typeof value.instanceId === \"string\" ? value.instanceId : null,\n };\n } catch {\n return null;\n }\n}\n\nexport async function getRunningProcess(\n paths: SwitchyPaths\n): Promise<{ record: ProcessRecord; live: Awaited<ReturnType<typeof fetchLiveness>> } | null> {\n const record = await readProcessRecord(paths);\n if (!record) {\n await removeFile(paths.processRecord);\n return null;\n }\n if (!processIsAlive(record.pid)) {\n await removeFile(paths.processRecord);\n return null;\n }\n return { record, live: await fetchLiveness(record) };\n}\n\nasync function waitForExit(pid: number, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (!processIsAlive(pid)) return true;\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n return !processIsAlive(pid);\n}\n\nasync function signalProcessTree(\n pid: number,\n signal: \"SIGTERM\" | \"SIGKILL\"\n): Promise<void> {\n if (process.platform !== \"win32\") {\n try {\n process.kill(-pid, signal);\n return;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ESRCH\") throw error;\n try {\n process.kill(pid, signal);\n } catch (fallbackError) {\n if ((fallbackError as NodeJS.ErrnoException).code !== \"ESRCH\") {\n throw fallbackError;\n }\n }\n return;\n }\n }\n\n await new Promise<void>((resolve, reject) => {\n const child = spawn(\n \"taskkill\",\n [\n \"/PID\",\n String(pid),\n \"/T\",\n \"/F\",\n ],\n { stdio: \"ignore\", windowsHide: true }\n );\n child.once(\"error\", reject);\n child.once(\"exit\", (code) => {\n if (code === 0 || !processIsAlive(pid)) resolve();\n else reject(new Error(`taskkill exited with code ${code}`));\n });\n });\n}\n\nexport async function stopSwitchy(\n paths: SwitchyPaths,\n force = false\n): Promise<boolean> {\n const running = await getRunningProcess(paths);\n if (!running) return false;\n if (\n !force\n && (\n !running.live\n || running.live.instanceId !== running.record.instanceId\n )\n ) {\n throw new Error(\n \"The recorded process could not be verified as Switchy. Re-run with stop --force.\"\n );\n }\n\n await signalProcessTree(\n running.record.pid,\n force ? \"SIGKILL\" : \"SIGTERM\"\n );\n if (!(await waitForExit(running.record.pid, SHUTDOWN_TIMEOUT_MS))) {\n throw new Error(\"Switchy did not stop in time. Re-run with stop --force.\");\n }\n await removeFile(paths.processRecord);\n return true;\n}\n\nasync function waitUntilReady(record: ProcessRecord): Promise<void> {\n const deadline = Date.now() + STARTUP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n if (!processIsAlive(record.pid)) {\n throw new Error(\"Switchy exited before becoming ready\");\n }\n try {\n const response = await fetch(\n `http://${record.hostname}:${record.port}/api/health/ready`,\n { signal: AbortSignal.timeout(2_000) }\n );\n if (response.ok) return;\n } catch {\n // The server may still be starting.\n }\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n throw new Error(\"Switchy did not become ready within 60 seconds\");\n}\n\nexport async function startSwitchyProcess({\n paths,\n runtimeDirectory,\n version,\n port,\n foreground,\n}: {\n paths: SwitchyPaths;\n runtimeDirectory: string;\n version: string;\n port: number;\n foreground: boolean;\n}): Promise<ProcessRecord> {\n const instanceId = randomUUID();\n const environment: NodeJS.ProcessEnv = {\n ...process.env,\n NODE_ENV: \"production\",\n HOSTNAME: DEFAULT_HOSTNAME,\n PORT: String(port),\n SWITCHY_HOME: paths.root,\n SWITCHY_INSTANCE_ID: instanceId,\n PLAYWRIGHT_BROWSERS_PATH: paths.playwright,\n };\n const serverPath = path.join(runtimeDirectory, \"server.js\");\n const logHandle = foreground\n ? null\n : await open(paths.logFile, \"a\", 0o600);\n const child = spawn(process.execPath, [serverPath], {\n cwd: runtimeDirectory,\n detached: !foreground,\n env: environment,\n stdio: foreground\n ? \"inherit\"\n : [\"ignore\", logHandle!.fd, logHandle!.fd],\n });\n await logHandle?.close();\n if (!child.pid) throw new Error(\"Switchy did not return a process ID\");\n\n const record: ProcessRecord = {\n schemaVersion: 1,\n pid: child.pid,\n instanceId,\n version,\n hostname: DEFAULT_HOSTNAME,\n port,\n startedAt: new Date().toISOString(),\n };\n await writeJsonFileAtomic(paths.processRecord, record);\n\n if (foreground) {\n await new Promise<void>((resolve, reject) => {\n child.once(\"error\", reject);\n child.once(\"exit\", (code) => {\n if (code === 0 || code === null) resolve();\n else reject(new Error(`Switchy exited with code ${code}`));\n });\n }).finally(() => removeFile(paths.processRecord));\n return record;\n }\n\n child.unref();\n try {\n await waitUntilReady(record);\n return record;\n } catch (error) {\n if (processIsAlive(record.pid)) {\n await signalProcessTree(record.pid, \"SIGTERM\");\n if (!(await waitForExit(record.pid, SHUTDOWN_TIMEOUT_MS))) {\n await signalProcessTree(record.pid, \"SIGKILL\");\n if (!(await waitForExit(record.pid, SHUTDOWN_TIMEOUT_MS))) {\n throw new Error(\n `Switchy failed to start and process ${record.pid} could not be stopped`,\n { cause: error }\n );\n }\n }\n }\n await removeFile(paths.processRecord);\n throw error;\n }\n}\n", "import { mkdir } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nimport type { SwitchyPaths } from \"./types.js\";\n\nexport function getSwitchyPaths(\n configuredRoot = process.env.SWITCHY_HOME\n): SwitchyPaths {\n const override = configuredRoot?.trim();\n if (override && !path.isAbsolute(override)) {\n throw new Error(\"SWITCHY_HOME must be an absolute path\");\n }\n const root = override\n ? path.normalize(override)\n : path.join(os.homedir(), \".switchy\");\n const data = path.join(root, \"data\");\n const app = path.join(root, \"app\");\n const runtime = path.join(root, \"runtime\");\n const logs = path.join(root, \"logs\");\n const cache = path.join(root, \"cache\");\n return {\n root,\n data,\n productionData: path.join(data, \"production\"),\n app,\n versions: path.join(app, \"versions\"),\n currentVersion: path.join(app, \"current.json\"),\n runtime,\n processRecord: path.join(runtime, \"process.json\"),\n installLock: path.join(runtime, \"install.lock\"),\n startLock: path.join(runtime, \"start.lock\"),\n logs,\n logFile: path.join(logs, \"switchy.log\"),\n cache,\n downloads: path.join(cache, \"downloads\"),\n playwright: path.join(cache, \"playwright\"),\n updateSnapshots: path.join(root, \"update-snapshots\"),\n };\n}\n\nexport async function ensureSwitchyDirectories(paths: SwitchyPaths): Promise<void> {\n for (const directory of [\n paths.root,\n paths.data,\n paths.app,\n paths.versions,\n paths.runtime,\n paths.logs,\n paths.cache,\n paths.downloads,\n paths.playwright,\n paths.updateSnapshots,\n ]) {\n await mkdir(directory, { recursive: true, mode: 0o700 });\n }\n}\n", "import { createHash, randomUUID } from \"node:crypto\";\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport {\n cp,\n mkdir,\n readFile,\n rename,\n rm,\n stat,\n} from \"node:fs/promises\";\nimport { Readable, Transform } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport path from \"node:path\";\n\nimport { extract } from \"tar\";\n\nimport {\n CLI_VERSION,\n GITHUB_REPOSITORY,\n RELEASE_MANIFEST_FILE,\n} from \"./config.js\";\nimport {\n withDirectoryLock,\n writeJsonFileAtomic,\n} from \"./files.js\";\nimport {\n currentTarget,\n isSupportedTarget,\n} from \"./platform.js\";\nimport type {\n CurrentVersionRecord,\n ReleaseArtifact,\n ReleaseManifest,\n SwitchyPaths,\n} from \"./types.js\";\n\nconst STABLE_VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+$/u;\n\nexport function assertStableVersion(version: string): string {\n if (!STABLE_VERSION_PATTERN.test(version)) {\n throw new Error(`Invalid stable Switchy version: ${version}`);\n }\n return version;\n}\n\nfunction releaseManifestUrl(version: string): string {\n return process.env.SWITCHY_MANIFEST_URL\n ?? `https://github.com/${GITHUB_REPOSITORY}/releases/download/v${version}/${RELEASE_MANIFEST_FILE}`;\n}\n\nfunction assertReleaseArtifact(value: unknown): ReleaseArtifact {\n if (\n typeof value !== \"object\"\n || value === null\n || typeof (value as ReleaseArtifact).file !== \"string\"\n || path.basename((value as ReleaseArtifact).file)\n !== (value as ReleaseArtifact).file\n || !/^switchy-\\d+\\.\\d+\\.\\d+-(?:darwin|linux|win32)-(?:arm64|x64)\\.tar\\.gz$/u\n .test((value as ReleaseArtifact).file)\n || typeof (value as ReleaseArtifact).sha256 !== \"string\"\n || !/^[a-f0-9]{64}$/u.test((value as ReleaseArtifact).sha256)\n || typeof (value as ReleaseArtifact).size !== \"number\"\n || !Number.isSafeInteger((value as ReleaseArtifact).size)\n || (value as ReleaseArtifact).size <= 0\n || (value as ReleaseArtifact).size > 1_000_000_000\n ) {\n throw new Error(\"Release manifest contains an invalid artifact\");\n }\n return value as ReleaseArtifact;\n}\n\nexport function parseReleaseManifest(value: unknown): ReleaseManifest {\n if (\n typeof value !== \"object\"\n || value === null\n || (value as ReleaseManifest).schemaVersion !== 1\n || typeof (value as ReleaseManifest).version !== \"string\"\n || !STABLE_VERSION_PATTERN.test((value as ReleaseManifest).version)\n || typeof (value as ReleaseManifest).publishedAt !== \"string\"\n || typeof (value as ReleaseManifest).nodeVersion !== \"string\"\n || typeof (value as ReleaseManifest).artifacts !== \"object\"\n || (value as ReleaseManifest).artifacts === null\n ) {\n throw new Error(\"Release manifest is invalid\");\n }\n const manifest = value as ReleaseManifest;\n if (\n !Number.isFinite(Date.parse(manifest.publishedAt))\n || !/^\\d+\\.\\d+\\.\\d+$/u.test(manifest.nodeVersion)\n || Array.isArray(manifest.artifacts)\n ) {\n throw new Error(\"Release manifest is invalid\");\n }\n for (const [target, valueArtifact] of Object.entries(manifest.artifacts)) {\n if (!isSupportedTarget(target)) {\n throw new Error(`Release manifest contains unsupported target ${target}`);\n }\n const artifact = assertReleaseArtifact(valueArtifact);\n if (artifact.file !== `switchy-${manifest.version}-${target}.tar.gz`) {\n throw new Error(\"Release manifest contains inconsistent artifact metadata\");\n }\n }\n return manifest;\n}\n\nasync function fetchJson(url: string): Promise<unknown> {\n const response = await fetch(url, {\n headers: {\n Accept: \"application/json\",\n \"User-Agent\": `switchy-cli/${CLI_VERSION}`,\n },\n signal: AbortSignal.timeout(30_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed with ${response.status}: ${url}`);\n }\n return response.json() as Promise<unknown>;\n}\n\nasync function fetchReleaseManifest(version: string): Promise<{\n manifest: ReleaseManifest;\n url: string;\n}> {\n const url = releaseManifestUrl(version);\n const manifest = parseReleaseManifest(await fetchJson(url));\n if (manifest.version !== version) {\n throw new Error(\n `Release manifest version ${manifest.version} does not match ${version}`\n );\n }\n return { manifest, url };\n}\n\nexport async function resolveLatestStableVersion(): Promise<string> {\n const override = process.env.SWITCHY_LATEST_VERSION?.trim();\n if (override) return assertStableVersion(override);\n const value = await fetchJson(\n `https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/latest`\n );\n const tagName = typeof value === \"object\" && value !== null\n ? (value as { tag_name?: unknown }).tag_name\n : null;\n if (typeof tagName !== \"string\" || !/^v\\d+\\.\\d+\\.\\d+$/u.test(tagName)) {\n throw new Error(\"Latest GitHub release does not contain a stable version tag\");\n }\n return tagName.slice(1);\n}\n\nasync function sha256(filePath: string): Promise<string> {\n const hash = createHash(\"sha256\");\n for await (const chunk of createReadStream(filePath)) {\n hash.update(chunk);\n }\n return hash.digest(\"hex\");\n}\n\nasync function downloadFile(\n url: string,\n destination: string,\n expectedSize: number\n): Promise<void> {\n const response = await fetch(url, {\n headers: { \"User-Agent\": `switchy-cli/${CLI_VERSION}` },\n redirect: \"follow\",\n signal: AbortSignal.timeout(300_000),\n });\n if (!response.ok || !response.body) {\n throw new Error(`Runtime download failed with ${response.status}`);\n }\n const contentLengthHeader = response.headers.get(\"content-length\");\n const contentLength = Number(contentLengthHeader);\n if (\n contentLengthHeader !== null\n && Number.isFinite(contentLength)\n && contentLength !== expectedSize\n ) {\n throw new Error(\"Runtime download size does not match the release manifest\");\n }\n let downloadedSize = 0;\n const sizeLimit = new Transform({\n transform(chunk: Buffer, _encoding, callback) {\n downloadedSize += chunk.length;\n if (downloadedSize > expectedSize) {\n callback(new Error(\"Runtime download exceeded its declared size\"));\n return;\n }\n callback(null, chunk);\n },\n });\n await pipeline(\n Readable.fromWeb(response.body as import(\"node:stream/web\").ReadableStream),\n sizeLimit,\n createWriteStream(destination, { mode: 0o600 })\n );\n if (downloadedSize !== expectedSize) {\n throw new Error(\"Runtime download size does not match the release manifest\");\n }\n}\n\nasync function validateRuntimeDirectory(\n runtimeDirectory: string,\n version: string,\n target: string\n): Promise<void> {\n for (const relativePath of [\n \"server.js\",\n \"bin/migrate.cjs\",\n \"drizzle/meta/_journal.json\",\n \"switchy-runtime.json\",\n ]) {\n const fileStats = await stat(path.join(runtimeDirectory, relativePath))\n .catch(() => null);\n if (!fileStats?.isFile()) {\n throw new Error(`Runtime is missing ${relativePath}`);\n }\n }\n let metadata: {\n schemaVersion?: unknown;\n version?: unknown;\n target?: unknown;\n nodeVersion?: unknown;\n builtAt?: unknown;\n };\n try {\n metadata = JSON.parse(\n await readFile(\n path.join(runtimeDirectory, \"switchy-runtime.json\"),\n \"utf8\"\n )\n ) as typeof metadata;\n } catch {\n throw new Error(\"Runtime metadata is invalid\");\n }\n if (\n metadata.schemaVersion !== 1\n || metadata.version !== version\n || metadata.target !== target\n || typeof metadata.nodeVersion !== \"string\"\n || !/^\\d+\\.\\d+\\.\\d+$/u.test(metadata.nodeVersion)\n || metadata.nodeVersion.split(\".\", 1)[0]\n !== process.versions.node.split(\".\", 1)[0]\n || typeof metadata.builtAt !== \"string\"\n || !Number.isFinite(Date.parse(metadata.builtAt))\n ) {\n throw new Error(\"Runtime metadata does not match this installation\");\n }\n}\n\nasync function installRuntimeUnlocked(\n version: string,\n paths: SwitchyPaths\n): Promise<string> {\n assertStableVersion(version);\n const target = currentTarget();\n const versionDirectory = path.join(paths.versions, version);\n if ((await stat(versionDirectory).catch(() => null))?.isDirectory()) {\n try {\n await validateRuntimeDirectory(versionDirectory, version, target);\n return versionDirectory;\n } catch {\n await rm(versionDirectory, { recursive: true, force: true });\n }\n }\n\n const localRuntime = process.env.SWITCHY_RUNTIME_SOURCE?.trim();\n const temporaryDirectory = path.join(\n paths.app,\n `.install-${version}-${randomUUID()}`\n );\n await rm(temporaryDirectory, { recursive: true, force: true });\n await mkdir(temporaryDirectory, { recursive: true, mode: 0o700 });\n\n try {\n if (localRuntime) {\n await cp(path.resolve(localRuntime), temporaryDirectory, {\n recursive: true,\n verbatimSymlinks: true,\n });\n } else {\n const { manifest, url: manifestUrl } = await fetchReleaseManifest(version);\n const artifact = manifest.artifacts[target];\n if (!artifact) {\n throw new Error(`Release ${version} does not support ${target}`);\n }\n const archivePath = path.join(paths.downloads, artifact.file);\n const artifactUrl = new URL(artifact.file, manifestUrl).toString();\n const existing = await stat(archivePath).catch(() => null);\n const cachedArchiveIsValid = existing?.size === artifact.size\n && await sha256(archivePath) === artifact.sha256;\n if (!cachedArchiveIsValid) {\n await rm(archivePath, { force: true });\n try {\n await downloadFile(artifactUrl, archivePath, artifact.size);\n } catch (error) {\n await rm(archivePath, { force: true });\n throw error;\n }\n }\n const downloadedArchive = await stat(archivePath).catch(() => null);\n if (\n downloadedArchive?.size !== artifact.size\n || await sha256(archivePath) !== artifact.sha256\n ) {\n await rm(archivePath, { force: true });\n throw new Error(\"Runtime checksum verification failed\");\n }\n await extract({\n cwd: temporaryDirectory,\n file: archivePath,\n preservePaths: false,\n strict: true,\n });\n }\n await validateRuntimeDirectory(temporaryDirectory, version, target);\n await rename(temporaryDirectory, versionDirectory);\n return versionDirectory;\n } catch (error) {\n await rm(temporaryDirectory, { recursive: true, force: true });\n throw error;\n }\n}\n\nexport async function installRuntime(\n version: string,\n paths: SwitchyPaths\n): Promise<string> {\n return withDirectoryLock(paths.installLock, async () => {\n return installRuntimeUnlocked(version, paths);\n });\n}\n\nexport async function setCurrentVersion(\n paths: SwitchyPaths,\n version: string\n): Promise<void> {\n const value: CurrentVersionRecord = {\n schemaVersion: 1,\n version,\n updatedAt: new Date().toISOString(),\n };\n await writeJsonFileAtomic(paths.currentVersion, value);\n}\n", "const SUPPORTED_TARGETS = [\n \"darwin-arm64\",\n \"darwin-x64\",\n \"linux-arm64\",\n \"linux-x64\",\n \"win32-x64\",\n] as const;\n\nconst supportedTargets = new Set<string>(SUPPORTED_TARGETS);\n\nexport function isSupportedTarget(target: string): boolean {\n return supportedTargets.has(target);\n}\n\nexport function currentTarget(\n platform = process.platform,\n architecture = process.arch\n): string {\n const target = `${platform}-${architecture}`;\n if (!isSupportedTarget(target)) {\n throw new Error(`Switchy does not provide a runtime for ${target}`);\n }\n return target;\n}\n", "import { spawn } from \"node:child_process\";\nimport { cp, mkdir, stat, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { SwitchyPaths } from \"./types.js\";\n\nasync function run(\n executable: string,\n args: string[],\n options: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n quiet?: boolean;\n }\n): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const child = spawn(executable, args, {\n cwd: options.cwd,\n env: options.env,\n stdio: options.quiet ? \"ignore\" : \"inherit\",\n });\n child.once(\"error\", reject);\n child.once(\"exit\", (code) => {\n if (code === 0) resolve();\n else reject(new Error(`${path.basename(executable)} exited with code ${code}`));\n });\n });\n}\n\nexport async function ensurePlaywrightBrowser(\n runtimeDirectory: string,\n paths: SwitchyPaths\n): Promise<void> {\n if (process.env.SWITCHY_SKIP_BROWSER_INSTALL === \"1\") return;\n const cliPath = path.join(\n runtimeDirectory,\n \"node_modules\",\n \"playwright\",\n \"cli.js\"\n );\n if (!(await stat(cliPath).catch(() => null))?.isFile()) {\n throw new Error(\"The packaged runtime is missing the Playwright installer\");\n }\n await run(process.execPath, [cliPath, \"install\", \"chromium\"], {\n cwd: runtimeDirectory,\n env: {\n ...process.env,\n PLAYWRIGHT_BROWSERS_PATH: paths.playwright,\n },\n quiet: true,\n });\n}\n\nexport async function runDatabaseMigration(\n runtimeDirectory: string,\n paths: SwitchyPaths\n): Promise<void> {\n await run(\n process.execPath,\n [path.join(runtimeDirectory, \"bin\", \"migrate.cjs\")],\n {\n cwd: runtimeDirectory,\n env: {\n ...process.env,\n NODE_ENV: \"production\",\n SWITCHY_HOME: paths.root,\n },\n }\n );\n}\n\nexport async function createUpdateSnapshot(\n paths: SwitchyPaths,\n fromVersion: string,\n toVersion: string\n): Promise<string | null> {\n if (!(await stat(paths.productionData).catch(() => null))?.isDirectory()) {\n return null;\n }\n const timestamp = new Date().toISOString().replaceAll(/[:.]/gu, \"-\");\n const snapshotDirectory = path.join(\n paths.updateSnapshots,\n `update-${fromVersion}-to-${toVersion}-${timestamp}`\n );\n await mkdir(snapshotDirectory, { recursive: true, mode: 0o700 });\n await cp(paths.productionData, path.join(snapshotDirectory, \"production\"), {\n recursive: true,\n });\n await writeFile(\n path.join(snapshotDirectory, \"update.json\"),\n `${JSON.stringify({\n fromVersion,\n toVersion,\n createdAt: new Date().toISOString(),\n }, null, 2)}\\n`,\n { mode: 0o600 }\n );\n return snapshotDirectory;\n}\n", "import {\n logsCommand,\n openCommand,\n startCommand,\n statusCommand,\n stopCommand,\n updateCommand,\n versionCommand,\n} from \"./commands.js\";\nimport { assertSupportedNodeVersion } from \"./config.js\";\n\nfunction optionValue(args: string[], name: string): string | undefined {\n const index = args.indexOf(name);\n if (index < 0) return undefined;\n const value = args[index + 1];\n if (!value || value.startsWith(\"--\")) {\n throw new Error(`${name} requires a value`);\n }\n return value;\n}\n\nfunction parsePort(args: string[]): number | undefined {\n const value = optionValue(args, \"--port\");\n if (!value) return undefined;\n const port = Number(value);\n if (!Number.isInteger(port) || port < 1 || port > 65_535) {\n throw new Error(\"--port must be an integer between 1 and 65535\");\n }\n return port;\n}\n\nfunction printHelp(): void {\n console.log(`Switchy CLI\n\nUsage:\n switchy start [--port 3000] [--foreground] [--app-version <version>]\n switchy stop [--force]\n switchy status\n switchy update\n switchy logs\n switchy open\n switchy version`);\n}\n\nasync function main(): Promise<void> {\n assertSupportedNodeVersion();\n const args = process.argv.slice(2);\n const command = args[0] ?? \"help\";\n switch (command) {\n case \"start\":\n await startCommand({\n version: optionValue(args, \"--app-version\"),\n port: parsePort(args),\n foreground: args.includes(\"--foreground\"),\n });\n break;\n case \"stop\":\n await stopCommand(args.includes(\"--force\"));\n break;\n case \"status\":\n await statusCommand();\n break;\n case \"update\":\n await updateCommand();\n break;\n case \"logs\":\n await logsCommand();\n break;\n case \"open\":\n await openCommand();\n break;\n case \"version\":\n case \"--version\":\n case \"-v\":\n versionCommand();\n break;\n case \"help\":\n case \"--help\":\n case \"-h\":\n printHelp();\n break;\n default:\n throw new Error(`Unknown command: ${command}`);\n }\n}\n\nmain().catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["readFile", "spawn", "path", "randomUUID", "processIsAlive", "randomUUID", "path", "mkdir", "path", "randomUUID", "mkdir", "readFile", "rename", "rm", "stat", "path", "path", "stat", "readFile", "rm", "randomUUID", "mkdir", "rename", "spawn", "cp", "mkdir", "stat", "writeFile", "path", "readFile", "spawn"]
|
|
7
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chandresh726/switchy",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Install, run, update, and stop the local-first Switchy application.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"switchy": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=24 <25"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/Chandresh726/Switchy.git",
|
|
20
|
+
"directory": "packages/cli"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/Chandresh726/Switchy#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/Chandresh726/Switchy/issues"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "node scripts/build.mjs",
|
|
31
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
32
|
+
"prepack": "pnpm build"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"tar": "^7.5.22"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^24.13.3",
|
|
39
|
+
"esbuild": "^0.28.1",
|
|
40
|
+
"typescript": "^5.9.3"
|
|
41
|
+
}
|
|
42
|
+
}
|