@workerstack/system 0.0.0-stage → 0.2.3
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 +1155 -2
- package/package.json +46 -5
- package/src/cli.ts +593 -0
- package/src/config.ts +120 -0
- package/src/env.ts +85 -0
- package/src/exec.ts +43 -0
- package/src/graph.ts +45 -0
- package/src/index.ts +15 -0
- package/src/path.ts +134 -0
- package/src/platform.ts +78 -0
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@workerstack/system",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Every OS dependency workerstack needs, installed by one command. Define deps in system.config.ts, run system install.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"cross-platform",
|
|
8
|
+
"dependencies",
|
|
9
|
+
"installer",
|
|
10
|
+
"system"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/workerstack/system#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/workerstack/system/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "workerstack",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/workerstack/system.git"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"system": "./src/cli.ts"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./src/index.ts"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"type:check": "tsc --noEmit -p tsconfig.src.json",
|
|
40
|
+
"type:check:tests": "tsc --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "catalog:",
|
|
44
|
+
"typescript": "catalog:",
|
|
45
|
+
"vitest": "catalog:"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* @workerstack/system CLI — cross-platform system dependency manager.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* system install Install all deps (auto-detect OS, respects depends order)
|
|
7
|
+
* system install <name> Install single dep
|
|
8
|
+
* system check Check all deps
|
|
9
|
+
* system check <name> Check single dep
|
|
10
|
+
* system env Setup environment variables
|
|
11
|
+
* system sync-pm Sync packageManager field
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execSync } from "node:child_process"
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
16
|
+
import { homedir } from "node:os"
|
|
17
|
+
import { dirname, resolve } from "node:path"
|
|
18
|
+
import type { SystemConfig, EnvVar, OsTarget, SystemDepConfig } from "./config.ts"
|
|
19
|
+
import { resolveCheckCommand, resolveOsCommand, versionsMatch } from "./config.ts"
|
|
20
|
+
import {
|
|
21
|
+
applyEnvBlock,
|
|
22
|
+
ensureParentDir,
|
|
23
|
+
hasEnvBlock,
|
|
24
|
+
expandPath,
|
|
25
|
+
reloadHint,
|
|
26
|
+
renderEnvBlock,
|
|
27
|
+
shellConfigPath,
|
|
28
|
+
shellKind,
|
|
29
|
+
} from "./env.ts"
|
|
30
|
+
import { sortByDependencies } from "./graph.ts"
|
|
31
|
+
import { runShellCommand } from "./exec.ts"
|
|
32
|
+
import { TTY_PATH_FALLBACK_WARN, pathEnv, processPath, snapshotTtyPath } from "./path.ts"
|
|
33
|
+
import { commandExists, detectOs, parseOsTarget, whichBinary } from "./platform.ts"
|
|
34
|
+
|
|
35
|
+
const CONFIG_NAMES = ["system.config.ts", "system.config.js", "system.config.mjs"]
|
|
36
|
+
|
|
37
|
+
const USAGE = `Usage: system <command> [args]
|
|
38
|
+
|
|
39
|
+
Commands:
|
|
40
|
+
install Install all deps (auto-detect OS)
|
|
41
|
+
install <name> Install single dep
|
|
42
|
+
check Check all deps
|
|
43
|
+
check <name> Check single dep
|
|
44
|
+
env Setup environment variables
|
|
45
|
+
sync-pm Sync packageManager field in package.json
|
|
46
|
+
|
|
47
|
+
Flags:
|
|
48
|
+
--config <path> Config file (default: system.config.ts in cwd)
|
|
49
|
+
--os <target> Force OS target (or set WORKERSTACK_SYSTEM_OS)
|
|
50
|
+
--dry-run Print install commands without running them
|
|
51
|
+
--upgrade Re-run install when the detected version does not match
|
|
52
|
+
--here check: use this process PATH instead of an interactive-shell snapshot
|
|
53
|
+
`
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve config file path. Checks --config flag, then convention names in cwd.
|
|
57
|
+
*/
|
|
58
|
+
function resolveConfigPath(args: string[]): string {
|
|
59
|
+
const configIdx = args.indexOf("--config")
|
|
60
|
+
if (configIdx !== -1) {
|
|
61
|
+
const configPath = args[configIdx + 1]
|
|
62
|
+
if (!configPath) {
|
|
63
|
+
console.error("--config requires a path argument")
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
const resolved = resolve(process.cwd(), configPath)
|
|
67
|
+
if (!existsSync(resolved)) {
|
|
68
|
+
console.error(`Config file not found: ${resolved}`)
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
return resolved
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const name of CONFIG_NAMES) {
|
|
75
|
+
const candidate = resolve(process.cwd(), name)
|
|
76
|
+
if (existsSync(candidate)) return candidate
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.error(`No system config found. Create one of: ${CONFIG_NAMES.join(", ")}`)
|
|
80
|
+
process.exit(1)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Strip --config <path> from args so command parsing is clean
|
|
85
|
+
*/
|
|
86
|
+
function stripFlag(args: string[], flag: string): { args: string[]; value: string | undefined } {
|
|
87
|
+
const idx = args.indexOf(flag)
|
|
88
|
+
if (idx === -1) return { args, value: undefined }
|
|
89
|
+
return {
|
|
90
|
+
args: [...args.slice(0, idx), ...args.slice(idx + 2)],
|
|
91
|
+
value: args[idx + 1],
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function stripBoolFlag(args: string[], flag: string): { args: string[]; present: boolean } {
|
|
96
|
+
const idx = args.indexOf(flag)
|
|
97
|
+
if (idx === -1) return { args, present: false }
|
|
98
|
+
return { args: [...args.slice(0, idx), ...args.slice(idx + 1)], present: true }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function loadConfig(configPath: string): Promise<SystemConfig> {
|
|
102
|
+
const mod = (await import(configPath)) as Record<string, unknown>
|
|
103
|
+
|
|
104
|
+
/* support: export default defineConfig(...) */
|
|
105
|
+
if (mod.default && typeof mod.default === "object" && "deps" in (mod.default as Record<string, unknown>)) {
|
|
106
|
+
return mod.default as SystemConfig
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* support: export const config = defineConfig(...) */
|
|
110
|
+
for (const value of Object.values(mod)) {
|
|
111
|
+
if (value && typeof value === "object" && "deps" in (value as Record<string, unknown>)) {
|
|
112
|
+
return value as SystemConfig
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.error("Config file must export an object with a `deps` field (use defineConfig)")
|
|
117
|
+
process.exit(1)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function execCommand(command: string, ignoreError = false): boolean {
|
|
121
|
+
try {
|
|
122
|
+
runShellCommand(command)
|
|
123
|
+
return true
|
|
124
|
+
} catch {
|
|
125
|
+
if (!ignoreError) {
|
|
126
|
+
console.error(`\nCommand failed: ${command}`)
|
|
127
|
+
}
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function checkBinaryToken(name: string, config: SystemDepConfig): string | undefined {
|
|
133
|
+
return resolveCheckCommand(name, config).split(/\s+/)[0]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function binaryPathNote(name: string, config: SystemDepConfig, searchPath?: string): string {
|
|
137
|
+
const token = checkBinaryToken(name, config)
|
|
138
|
+
const path = token ? whichBinary(token, process.platform, searchPath) : null
|
|
139
|
+
return path ? ` ${path}` : ""
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getInstalledVersion(name: string, config: SystemDepConfig, searchPath?: string): string | null {
|
|
143
|
+
const checkCmd = resolveCheckCommand(name, config)
|
|
144
|
+
const binary = checkBinaryToken(name, config)
|
|
145
|
+
if (binary && !commandExists(binary, searchPath)) return null
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const execOpts: {
|
|
149
|
+
encoding: "utf-8"
|
|
150
|
+
env?: NodeJS.ProcessEnv
|
|
151
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
152
|
+
} = {
|
|
153
|
+
encoding: "utf-8",
|
|
154
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
155
|
+
}
|
|
156
|
+
if (searchPath !== undefined) execOpts.env = pathEnv(searchPath)
|
|
157
|
+
const output = execSync(checkCmd, execOpts).trim()
|
|
158
|
+
const match = output.match(/(\d+\.\d+[\w.-]*)/)
|
|
159
|
+
return match?.[1] ?? null
|
|
160
|
+
} catch {
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function resolveEnvValue(env: EnvVar): string | null {
|
|
166
|
+
if (env.detect && env.detect.length > 0) {
|
|
167
|
+
for (const path of env.detect) {
|
|
168
|
+
const expanded = expandPath(path, homedir())
|
|
169
|
+
if (existsSync(expanded)) {
|
|
170
|
+
console.log(` Detected ${env.key} at: ${expanded}`)
|
|
171
|
+
return path
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (env.fallback) {
|
|
175
|
+
console.log(` Using fallback for ${env.key}: ${env.fallback}`)
|
|
176
|
+
return env.fallback
|
|
177
|
+
}
|
|
178
|
+
console.log(` Could not detect ${env.key}, no fallback specified`)
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return env.value || null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function configureEnvVars(tool: string, envVars: EnvVar[]): boolean {
|
|
186
|
+
if (envVars.length === 0) return true
|
|
187
|
+
|
|
188
|
+
const kind = shellKind(process.platform)
|
|
189
|
+
const configPath = shellConfigPath(homedir(), kind, process.env.SHELL || "")
|
|
190
|
+
const configName = configPath.split(/[\\/]/).pop()
|
|
191
|
+
|
|
192
|
+
console.log(`\n Configuring environment variables in ${configName}...`)
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const existingContent = existsSync(configPath) ? readFileSync(configPath, "utf-8") : ""
|
|
196
|
+
|
|
197
|
+
const resolvedEnvs: Array<{ appendToPath?: boolean; key: string; value: string }> = []
|
|
198
|
+
for (const env of envVars) {
|
|
199
|
+
const value = resolveEnvValue(env)
|
|
200
|
+
if (value) {
|
|
201
|
+
resolvedEnvs.push({ appendToPath: env.appendToPath, key: env.key, value })
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (resolvedEnvs.length === 0) {
|
|
206
|
+
console.log(" No environment variables to configure")
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (hasEnvBlock(existingContent, tool)) {
|
|
211
|
+
console.log(` Updating existing ${tool} environment config...`)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const next = applyEnvBlock(existingContent, renderEnvBlock(tool, resolvedEnvs, kind), tool)
|
|
215
|
+
ensureParentDir(configPath)
|
|
216
|
+
writeFileSync(configPath, next)
|
|
217
|
+
console.log(` Added to ${configName}:`)
|
|
218
|
+
for (const env of resolvedEnvs) {
|
|
219
|
+
if (env.appendToPath) {
|
|
220
|
+
console.log(` PATH+=:${env.value}`)
|
|
221
|
+
} else {
|
|
222
|
+
console.log(` ${env.key}=${env.value}`)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
console.log(`\n ${reloadHint(kind, configName ?? "")}`)
|
|
226
|
+
|
|
227
|
+
return true
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error(" Failed to configure env vars:", error)
|
|
230
|
+
return false
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function installOne(
|
|
235
|
+
name: string,
|
|
236
|
+
config: SystemDepConfig,
|
|
237
|
+
os: OsTarget,
|
|
238
|
+
dryRun = false,
|
|
239
|
+
upgrade = false,
|
|
240
|
+
): "installed" | "skipped" | "failed" | "unavailable" {
|
|
241
|
+
const command = resolveOsCommand(name, config, os)
|
|
242
|
+
|
|
243
|
+
if (!command) {
|
|
244
|
+
console.log(`${name}@${config.version} — not available on ${os}`)
|
|
245
|
+
return "unavailable"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log(`${name}@${config.version}`)
|
|
249
|
+
console.log(` ${config.description}`)
|
|
250
|
+
|
|
251
|
+
if (dryRun) {
|
|
252
|
+
console.log(` dry-run: ${command}`)
|
|
253
|
+
return "installed"
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const installed = getInstalledVersion(name, config)
|
|
257
|
+
if (installed && versionsMatch(installed, config.version)) {
|
|
258
|
+
console.log(` Already installed (${installed}), skipping`)
|
|
259
|
+
return "skipped"
|
|
260
|
+
}
|
|
261
|
+
if (installed && !upgrade) {
|
|
262
|
+
console.log(
|
|
263
|
+
` Already installed (${installed}), skipping (expected ${config.version})${binaryPathNote(name, config)}`,
|
|
264
|
+
)
|
|
265
|
+
console.log(` run: system install ${name} --upgrade`)
|
|
266
|
+
return "skipped"
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const success = execCommand(command, true)
|
|
270
|
+
if (success) {
|
|
271
|
+
console.log(" Installed successfully")
|
|
272
|
+
if (config.env && config.env.length > 0) {
|
|
273
|
+
configureEnvVars(name, config.env)
|
|
274
|
+
}
|
|
275
|
+
/* Installer often does not win PATH — brew/nvm/fnm still first. */
|
|
276
|
+
const after = getInstalledVersion(name, config)
|
|
277
|
+
if (after && !versionsMatch(after, config.version)) {
|
|
278
|
+
console.log(` PATH still has ${after} (expected ${config.version})${binaryPathNote(name, config)}`)
|
|
279
|
+
console.log(" Unpin/unlink that binary or put the pinned one first on PATH")
|
|
280
|
+
}
|
|
281
|
+
return "installed"
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.error(" Installation failed")
|
|
285
|
+
return "failed"
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function cmdInstall(
|
|
289
|
+
deps: Record<string, SystemDepConfig>,
|
|
290
|
+
target: string | undefined,
|
|
291
|
+
dryRun = false,
|
|
292
|
+
upgrade = false,
|
|
293
|
+
): void {
|
|
294
|
+
const os = detectOs()
|
|
295
|
+
|
|
296
|
+
if (target) {
|
|
297
|
+
const config = deps[target]
|
|
298
|
+
if (!config) {
|
|
299
|
+
console.error(`Unknown dependency: ${target}`)
|
|
300
|
+
console.log(`Available: ${Object.keys(deps).join(", ")}`)
|
|
301
|
+
process.exit(1)
|
|
302
|
+
}
|
|
303
|
+
console.log(`Detected OS: ${os}`)
|
|
304
|
+
const result = installOne(target, config, os, dryRun, upgrade)
|
|
305
|
+
if (result === "failed") process.exit(1)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log("@workerstack/system — install")
|
|
310
|
+
console.log(`Detected OS: ${os}`)
|
|
311
|
+
console.log("=".repeat(50))
|
|
312
|
+
|
|
313
|
+
const entries = sortByDependencies(Object.entries(deps))
|
|
314
|
+
console.log(`\nFound ${entries.length} dependencies to install:\n`)
|
|
315
|
+
for (const [name, config] of entries) {
|
|
316
|
+
const badge = config.required ? "[required]" : "[optional]"
|
|
317
|
+
const available = resolveOsCommand(name, config, os) !== null
|
|
318
|
+
console.log(` ${badge} ${name}@${config.version}${available ? "" : ` (not available on ${os})`}`)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
console.log(`\n${"=".repeat(50)}`)
|
|
322
|
+
console.log(dryRun ? "Dry-run — commands only, nothing installed" : "Installing dependencies...")
|
|
323
|
+
console.log("=".repeat(50))
|
|
324
|
+
|
|
325
|
+
let installed = 0
|
|
326
|
+
let skipped = 0
|
|
327
|
+
let failed = 0
|
|
328
|
+
let unavailable = 0
|
|
329
|
+
|
|
330
|
+
for (const [name, config] of entries) {
|
|
331
|
+
const result = installOne(name, config, os, dryRun, upgrade)
|
|
332
|
+
|
|
333
|
+
if (result === "installed") installed++
|
|
334
|
+
else if (result === "skipped") skipped++
|
|
335
|
+
else if (result === "unavailable") unavailable++
|
|
336
|
+
else if (config.required) failed++
|
|
337
|
+
else skipped++
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
console.log(`\n${"=".repeat(50)}`)
|
|
341
|
+
console.log("Installation Summary")
|
|
342
|
+
console.log("=".repeat(50))
|
|
343
|
+
console.log(`Installed: ${installed}`)
|
|
344
|
+
console.log(`Skipped: ${skipped}`)
|
|
345
|
+
console.log(`Unavailable on ${os}: ${unavailable}`)
|
|
346
|
+
console.log(`Failed: ${failed}`)
|
|
347
|
+
|
|
348
|
+
if (failed > 0) {
|
|
349
|
+
console.log("\nSome required dependencies failed to install")
|
|
350
|
+
process.exit(1)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
console.log("\nAll required system dependencies are ready!")
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function resolveCheckSearchPath(here: boolean): string {
|
|
357
|
+
if (here) return processPath()
|
|
358
|
+
const snapshot = snapshotTtyPath()
|
|
359
|
+
if (snapshot.source === "process") {
|
|
360
|
+
console.warn(TTY_PATH_FALLBACK_WARN)
|
|
361
|
+
}
|
|
362
|
+
return snapshot.path
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function cmdCheck(
|
|
366
|
+
deps: Record<string, SystemDepConfig>,
|
|
367
|
+
configDir: string,
|
|
368
|
+
packageJsonPath: string,
|
|
369
|
+
target: string | undefined,
|
|
370
|
+
here = false,
|
|
371
|
+
): void {
|
|
372
|
+
const os = detectOs()
|
|
373
|
+
const searchPath = resolveCheckSearchPath(here)
|
|
374
|
+
|
|
375
|
+
if (target) {
|
|
376
|
+
const config = deps[target]
|
|
377
|
+
if (!config) {
|
|
378
|
+
console.error(`Unknown dependency: ${target}`)
|
|
379
|
+
console.log(`Available: ${Object.keys(deps).join(", ")}`)
|
|
380
|
+
process.exit(1)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const installed = getInstalledVersion(target, config, searchPath)
|
|
384
|
+
const available = resolveOsCommand(target, config, os) !== null
|
|
385
|
+
|
|
386
|
+
if (!available) {
|
|
387
|
+
console.log(`${target} — not available on ${os}`)
|
|
388
|
+
} else if (!installed) {
|
|
389
|
+
console.log(`${target} — not installed (expected ${config.version})`)
|
|
390
|
+
process.exit(1)
|
|
391
|
+
} else {
|
|
392
|
+
console.log(`${target}@${installed} (expected ${config.version})${binaryPathNote(target, config, searchPath)}`)
|
|
393
|
+
}
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
console.log("@workerstack/system — check")
|
|
398
|
+
console.log(`Detected OS: ${os}`)
|
|
399
|
+
console.log("=".repeat(50))
|
|
400
|
+
|
|
401
|
+
let ok = 0
|
|
402
|
+
let warn = 0
|
|
403
|
+
let missing = 0
|
|
404
|
+
|
|
405
|
+
for (const [name, config] of Object.entries(deps)) {
|
|
406
|
+
const installed = getInstalledVersion(name, config, searchPath)
|
|
407
|
+
const badge = config.required ? "[required]" : "[optional]"
|
|
408
|
+
const available = resolveOsCommand(name, config, os) !== null
|
|
409
|
+
|
|
410
|
+
if (!available) {
|
|
411
|
+
console.log(` - ${badge} ${name} — not available on ${os}`)
|
|
412
|
+
continue
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (!installed) {
|
|
416
|
+
console.log(` x ${badge} ${name} — not installed (expected ${config.version})`)
|
|
417
|
+
if (config.required) missing++
|
|
418
|
+
else warn++
|
|
419
|
+
} else if (versionsMatch(installed, config.version)) {
|
|
420
|
+
console.log(` v ${badge} ${name}@${installed}`)
|
|
421
|
+
ok++
|
|
422
|
+
} else {
|
|
423
|
+
console.log(
|
|
424
|
+
` ~ ${badge} ${name}@${installed} (expected ${config.version})${binaryPathNote(name, config, searchPath)}`,
|
|
425
|
+
)
|
|
426
|
+
warn++
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* packageManager sync check */
|
|
431
|
+
const bunConfig = deps.bun
|
|
432
|
+
if (bunConfig) {
|
|
433
|
+
const pkgJsonPath = resolve(configDir, packageJsonPath)
|
|
434
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8")) as Record<string, unknown>
|
|
435
|
+
const current = pkg.packageManager as string | undefined
|
|
436
|
+
|
|
437
|
+
if (bunConfig.version === "latest") {
|
|
438
|
+
if (current?.startsWith("bun@")) {
|
|
439
|
+
console.log(`\n v packageManager: ${current} (latest mode)`)
|
|
440
|
+
} else {
|
|
441
|
+
console.log(`\n ~ packageManager: ${current ?? "(missing)"} (expected bun@*)`)
|
|
442
|
+
warn++
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
const expected = `bun@${bunConfig.version}`
|
|
446
|
+
if (current === expected) {
|
|
447
|
+
console.log(`\n v packageManager: ${current}`)
|
|
448
|
+
} else {
|
|
449
|
+
console.log(`\n ~ packageManager: ${current ?? "(missing)"} (expected ${expected})`)
|
|
450
|
+
warn++
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
console.log(`\n${"=".repeat(50)}`)
|
|
456
|
+
console.log(`OK: ${ok} Mismatch: ${warn} Missing: ${missing}`)
|
|
457
|
+
|
|
458
|
+
if (missing > 0) {
|
|
459
|
+
console.log("\nRequired dependencies missing — run: system install")
|
|
460
|
+
process.exit(1)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (warn > 0) {
|
|
464
|
+
console.log("\nVersion mismatches found — run: system install --upgrade")
|
|
465
|
+
} else {
|
|
466
|
+
console.log("\nAll system dependencies OK")
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function cmdEnv(deps: Record<string, SystemDepConfig>): void {
|
|
471
|
+
console.log("Setting up environment variables")
|
|
472
|
+
console.log("=".repeat(50))
|
|
473
|
+
|
|
474
|
+
const envDeps = Object.entries(deps).filter(([_, config]) => config.env && config.env.length > 0)
|
|
475
|
+
|
|
476
|
+
if (envDeps.length === 0) {
|
|
477
|
+
console.log("\nNo dependencies with environment variables configured.")
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
for (const [tool, config] of envDeps) {
|
|
482
|
+
if (config.env) {
|
|
483
|
+
console.log(`\n${tool}`)
|
|
484
|
+
configureEnvVars(tool, config.env)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const kind = shellKind(process.platform)
|
|
489
|
+
const configPath = shellConfigPath(homedir(), kind, process.env.SHELL || "")
|
|
490
|
+
console.log(`\n${"=".repeat(50)}`)
|
|
491
|
+
console.log(`Environment variables configured in ${configPath}`)
|
|
492
|
+
console.log(`\n${reloadHint(kind, configPath.split(/[\\/]/).pop() ?? "")}`)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function cmdSyncPm(deps: Record<string, SystemDepConfig>, configDir: string, packageJsonPath: string): boolean {
|
|
496
|
+
const bunConfig = deps.bun
|
|
497
|
+
if (!bunConfig) {
|
|
498
|
+
console.log("\nSkipping packageManager sync — bun not in config")
|
|
499
|
+
return true
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const bunVersion = bunConfig.version
|
|
503
|
+
|
|
504
|
+
if (bunVersion === "latest") {
|
|
505
|
+
console.log('\nSkipping packageManager sync — bun version is "latest"')
|
|
506
|
+
return true
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const pkgJsonPath = resolve(configDir, packageJsonPath)
|
|
510
|
+
const expected = `bun@${bunVersion}`
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
const content = readFileSync(pkgJsonPath, "utf-8")
|
|
514
|
+
const pkg = JSON.parse(content) as Record<string, unknown>
|
|
515
|
+
const current = pkg.packageManager as string | undefined
|
|
516
|
+
|
|
517
|
+
if (current === expected) {
|
|
518
|
+
console.log(`\npackageManager already correct: ${expected}`)
|
|
519
|
+
return true
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
console.log(`\nSyncing packageManager: ${current ?? "(missing)"} -> ${expected}`)
|
|
523
|
+
const pkgName = pkg.name as string
|
|
524
|
+
const updated = current
|
|
525
|
+
? content.replace(`"packageManager": "${current}"`, `"packageManager": "${expected}"`)
|
|
526
|
+
: content.replace(`"name": "${pkgName}"`, `"name": "${pkgName}",\n\t"packageManager": "${expected}"`)
|
|
527
|
+
writeFileSync(pkgJsonPath, updated)
|
|
528
|
+
console.log(`Updated packageManager to ${expected}`)
|
|
529
|
+
return true
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.error("Failed to sync packageManager:", error)
|
|
532
|
+
return false
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function main(): Promise<void> {
|
|
537
|
+
const rawArgs = process.argv.slice(2)
|
|
538
|
+
const osFlag = stripFlag(rawArgs, "--os")
|
|
539
|
+
if (rawArgs.includes("--os") && !osFlag.value) {
|
|
540
|
+
console.error("--os requires a target argument")
|
|
541
|
+
process.exit(1)
|
|
542
|
+
}
|
|
543
|
+
if (osFlag.value) {
|
|
544
|
+
try {
|
|
545
|
+
process.env.WORKERSTACK_SYSTEM_OS = parseOsTarget(osFlag.value)
|
|
546
|
+
} catch (error) {
|
|
547
|
+
console.error(error instanceof Error ? error.message : error)
|
|
548
|
+
process.exit(1)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const configPath = resolveConfigPath(osFlag.args)
|
|
553
|
+
const configDir = dirname(configPath)
|
|
554
|
+
const config = await loadConfig(configPath)
|
|
555
|
+
const deps = config.deps
|
|
556
|
+
const packageJsonPath = config.packageJsonPath ?? "package.json"
|
|
557
|
+
|
|
558
|
+
const configStripped = stripFlag(osFlag.args, "--config")
|
|
559
|
+
const dry = stripBoolFlag(configStripped.args, "--dry-run")
|
|
560
|
+
const upgrade = stripBoolFlag(dry.args, "--upgrade")
|
|
561
|
+
const here = stripBoolFlag(upgrade.args, "--here")
|
|
562
|
+
const args = here.args
|
|
563
|
+
const command = args[0]
|
|
564
|
+
const target = args[1]
|
|
565
|
+
|
|
566
|
+
switch (command) {
|
|
567
|
+
case "install":
|
|
568
|
+
cmdInstall(deps, target, dry.present, upgrade.present)
|
|
569
|
+
break
|
|
570
|
+
case "check":
|
|
571
|
+
cmdCheck(deps, configDir, packageJsonPath, target, here.present)
|
|
572
|
+
break
|
|
573
|
+
case "env":
|
|
574
|
+
cmdEnv(deps)
|
|
575
|
+
break
|
|
576
|
+
case "sync-pm":
|
|
577
|
+
cmdSyncPm(deps, configDir, packageJsonPath)
|
|
578
|
+
break
|
|
579
|
+
default:
|
|
580
|
+
console.log(USAGE)
|
|
581
|
+
if (command) {
|
|
582
|
+
console.error(`Unknown command: ${command}`)
|
|
583
|
+
process.exit(1)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
await main()
|
|
590
|
+
} catch (error) {
|
|
591
|
+
console.error("\nFatal error:", error)
|
|
592
|
+
process.exit(1)
|
|
593
|
+
}
|