@workerstack/system 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +319 -2
- package/package.json +48 -5
- package/src/args.ts +67 -0
- package/src/bin.ts +5 -0
- package/src/builtins/agents.ts +73 -0
- package/src/builtins/bootstrap.ts +39 -0
- package/src/builtins/browsers.ts +79 -0
- package/src/builtins/cloudflare.ts +32 -0
- package/src/builtins/database.ts +41 -0
- package/src/builtins/index.ts +41 -0
- package/src/builtins/languages.ts +218 -0
- package/src/builtins/recipes.ts +145 -0
- package/src/builtins/release.ts +133 -0
- package/src/builtins/resolve.ts +80 -0
- package/src/builtins/runtimes.ts +54 -0
- package/src/builtins/types.ts +38 -0
- package/src/commands/check.ts +101 -0
- package/src/commands/env.ts +99 -0
- package/src/commands/install.ts +134 -0
- package/src/commands/list.ts +29 -0
- package/src/commands/sync-pm.ts +53 -0
- package/src/config.ts +123 -0
- package/src/env.ts +103 -0
- package/src/errors.ts +4 -0
- package/src/exec.ts +64 -0
- package/src/graph.ts +33 -0
- package/src/index.ts +21 -0
- package/src/load.ts +43 -0
- package/src/main.ts +99 -0
- package/src/path.ts +172 -0
- package/src/platform.ts +73 -0
- package/src/probe.ts +44 -0
- package/src/runtime.ts +99 -0
- package/src/validate.ts +154 -0
- package/src/version.ts +33 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release and CI tooling: git, gh, jq, curl, goreleaser, cosign, shellcheck, pwsh.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { archive, binary, localBinEnv, pick, tree, unix, winArchive, winBinary, winTree } from "./recipes.ts"
|
|
6
|
+
import type { BuiltinDep } from "./types.ts"
|
|
7
|
+
|
|
8
|
+
const GH = "https://github.com"
|
|
9
|
+
|
|
10
|
+
export const RELEASE = {
|
|
11
|
+
curl: {
|
|
12
|
+
dependsOn: ["brew", "choco"],
|
|
13
|
+
description: "HTTP client the install commands use (the OS package manager's current build)",
|
|
14
|
+
os: () => ({
|
|
15
|
+
"linux-apt": "sudo apt-get install -y curl",
|
|
16
|
+
"linux-dnf": "sudo dnf install -y curl",
|
|
17
|
+
"linux-pacman": "sudo pacman -S --noconfirm curl",
|
|
18
|
+
macos: "brew install curl",
|
|
19
|
+
windows: "choco install curl -y",
|
|
20
|
+
}),
|
|
21
|
+
versions: "latest",
|
|
22
|
+
},
|
|
23
|
+
git: {
|
|
24
|
+
dependsOn: ["brew", "choco"],
|
|
25
|
+
description: "Version control (the OS package manager's current build)",
|
|
26
|
+
os: () => ({
|
|
27
|
+
"linux-apt": "sudo apt-get install -y git",
|
|
28
|
+
"linux-dnf": "sudo dnf install -y git",
|
|
29
|
+
"linux-pacman": "sudo pacman -S --noconfirm git",
|
|
30
|
+
macos: "brew install git",
|
|
31
|
+
windows: "choco install git -y",
|
|
32
|
+
}),
|
|
33
|
+
versions: "latest",
|
|
34
|
+
},
|
|
35
|
+
gh: {
|
|
36
|
+
description: "GitHub CLI, for releases, PRs and the API",
|
|
37
|
+
env: localBinEnv,
|
|
38
|
+
os: ({ arch, version }) => {
|
|
39
|
+
const a = pick(arch, { arm64: "arm64", x64: "amd64" })
|
|
40
|
+
const base = `${GH}/cli/cli/releases/download/v${version}/gh_${version}`
|
|
41
|
+
return {
|
|
42
|
+
...unix(
|
|
43
|
+
archive(`${base}_linux_${a}.tar.gz`, `gh_${version}_linux_${a}/bin/gh`, "gh"),
|
|
44
|
+
archive(`${base}_macOS_${a}.zip`, `gh_${version}_macOS_${a}/bin/gh`, "gh"),
|
|
45
|
+
),
|
|
46
|
+
windows: winArchive(`${base}_windows_amd64.zip`, "bin\\gh.exe", "gh"),
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
versions: "pinned",
|
|
50
|
+
},
|
|
51
|
+
goreleaser: {
|
|
52
|
+
check: { command: "goreleaser --version", pattern: "GitVersion:\\s+v?(\\d+\\.\\d+\\.\\d+)" },
|
|
53
|
+
description: "Release automation for the generated Go CLI",
|
|
54
|
+
env: localBinEnv,
|
|
55
|
+
os: ({ arch, version }) => {
|
|
56
|
+
const base = `${GH}/goreleaser/goreleaser/releases/download/v${version}/goreleaser`
|
|
57
|
+
const a = pick(arch, { arm64: "arm64", x64: "x86_64" })
|
|
58
|
+
return {
|
|
59
|
+
...unix(
|
|
60
|
+
archive(`${base}_Linux_${a}.tar.gz`, "goreleaser", "goreleaser"),
|
|
61
|
+
archive(`${base}_Darwin_${a}.tar.gz`, "goreleaser", "goreleaser"),
|
|
62
|
+
),
|
|
63
|
+
windows: winArchive(`${base}_Windows_x86_64.zip`, "goreleaser.exe", "goreleaser"),
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
versions: "pinned",
|
|
67
|
+
},
|
|
68
|
+
cosign: {
|
|
69
|
+
check: { command: "cosign version", pattern: "GitVersion:\\s+v?(\\d+\\.\\d+\\.\\d+)" },
|
|
70
|
+
description: "Sigstore signing for release artifacts",
|
|
71
|
+
env: localBinEnv,
|
|
72
|
+
os: ({ arch, version }) => {
|
|
73
|
+
const base = `${GH}/sigstore/cosign/releases/download/v${version}/cosign`
|
|
74
|
+
const a = pick(arch, { arm64: "arm64", x64: "amd64" })
|
|
75
|
+
return {
|
|
76
|
+
...unix(binary(`${base}-linux-${a}`, "cosign"), binary(`${base}-darwin-${a}`, "cosign")),
|
|
77
|
+
windows: winBinary(`${base}-windows-amd64.exe`, "cosign"),
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
versions: "pinned",
|
|
81
|
+
},
|
|
82
|
+
jq: {
|
|
83
|
+
description: "JSON processor",
|
|
84
|
+
env: localBinEnv,
|
|
85
|
+
os: ({ arch, version }) => {
|
|
86
|
+
const base = `${GH}/jqlang/jq/releases/download/jq-${version}/jq`
|
|
87
|
+
const a = pick(arch, { arm64: "arm64", x64: "amd64" })
|
|
88
|
+
return {
|
|
89
|
+
...unix(binary(`${base}-linux-${a}`, "jq"), binary(`${base}-macos-${a}`, "jq")),
|
|
90
|
+
windows: winBinary(`${base}-windows-amd64.exe`, "jq"),
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
versions: "pinned",
|
|
94
|
+
},
|
|
95
|
+
pwsh: {
|
|
96
|
+
check: { command: "pwsh --version" },
|
|
97
|
+
description: "PowerShell 7, for the generated Windows install scripts",
|
|
98
|
+
env: (os) => (os === "windows" ? [{ path: "$env:LOCALAPPDATA\\Programs\\pwsh" }] : localBinEnv(os)),
|
|
99
|
+
os: ({ arch, version }) => {
|
|
100
|
+
const base = `${GH}/PowerShell/PowerShell/releases/download/v${version}`
|
|
101
|
+
const a = pick(arch, { arm64: "arm64", x64: "x64" })
|
|
102
|
+
const dir = "$HOME/.local/share/pwsh"
|
|
103
|
+
const linux = `${tree(`${base}/powershell-${version}-linux-${a}.tar.gz`, dir, { bins: ["pwsh"] })} && chmod +x "${dir}/pwsh"`
|
|
104
|
+
return {
|
|
105
|
+
...unix(
|
|
106
|
+
linux,
|
|
107
|
+
`${tree(`${base}/powershell-${version}-osx-${a}.tar.gz`, dir, { bins: ["pwsh"] })} && chmod +x "${dir}/pwsh"`,
|
|
108
|
+
),
|
|
109
|
+
/* Fedora images lack the ICU library PowerShell needs at startup */
|
|
110
|
+
"linux-dnf": `sudo dnf install -y libicu && ${linux}`,
|
|
111
|
+
windows: winTree(`${base}/PowerShell-${version}-win-x64.zip`, "$env:LOCALAPPDATA\\Programs\\pwsh"),
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
versions: "pinned",
|
|
115
|
+
},
|
|
116
|
+
shellcheck: {
|
|
117
|
+
description: "Shell script linter, for the generated install scripts",
|
|
118
|
+
env: localBinEnv,
|
|
119
|
+
os: ({ arch, version }) => {
|
|
120
|
+
const base = `${GH}/koalaman/shellcheck/releases/download/v${version}/shellcheck-v${version}`
|
|
121
|
+
const a = pick(arch, { arm64: "aarch64", x64: "x86_64" })
|
|
122
|
+
const member = `shellcheck-v${version}/shellcheck`
|
|
123
|
+
return {
|
|
124
|
+
...unix(
|
|
125
|
+
archive(`${base}.linux.${a}.tar.xz`, member, "shellcheck"),
|
|
126
|
+
archive(`${base}.darwin.${a}.tar.xz`, member, "shellcheck"),
|
|
127
|
+
),
|
|
128
|
+
windows: winArchive(`${base}.zip`, "shellcheck.exe", "shellcheck"),
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
versions: "pinned",
|
|
132
|
+
},
|
|
133
|
+
} satisfies Record<string, BuiltinDep>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { OsTarget, SystemDepConfig } from "../config.ts"
|
|
2
|
+
import { ConfigError } from "../errors.ts"
|
|
3
|
+
import type { Arch, BuiltinDep, BuiltinOverride } from "./types.ts"
|
|
4
|
+
|
|
5
|
+
/** A config entry before resolution: a built-in's version or override, or a full custom dep. */
|
|
6
|
+
export type RawEntry = BuiltinOverride | SystemDepConfig | string
|
|
7
|
+
|
|
8
|
+
function isArch(arch: string): arch is Arch {
|
|
9
|
+
return arch === "x64" || arch === "arm64"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function expand(builtin: BuiltinDep, entry: BuiltinOverride | string, arch: Arch, os: OsTarget): SystemDepConfig {
|
|
13
|
+
const o: BuiltinOverride = typeof entry === "string" ? { version: entry } : entry
|
|
14
|
+
const config: SystemDepConfig = {
|
|
15
|
+
description: o.description ?? builtin.description,
|
|
16
|
+
os: { ...builtin.os({ arch, version: o.version }), ...o.os },
|
|
17
|
+
required: o.required ?? true,
|
|
18
|
+
version: o.version,
|
|
19
|
+
}
|
|
20
|
+
const check = o.check ?? builtin.check
|
|
21
|
+
const dependsOn = o.dependsOn ?? builtin.dependsOn
|
|
22
|
+
const env = o.env ?? builtin.env?.(os)
|
|
23
|
+
if (check) config.check = check
|
|
24
|
+
if (dependsOn) config.dependsOn = [...dependsOn]
|
|
25
|
+
if (env) config.env = [...env]
|
|
26
|
+
return config
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Every entry as a full `SystemDepConfig`. Built-in names expand from `catalog` for `arch` and `os`; other names
|
|
31
|
+
* pass through. A `latest`-only built-in that a dep needs is added when missing. Throws `ConfigError`
|
|
32
|
+
* listing every version-policy, prerequisite and CPU problem.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveDeps(
|
|
35
|
+
deps: Record<string, RawEntry>,
|
|
36
|
+
{ arch, os }: { arch: string; os: OsTarget },
|
|
37
|
+
catalog: Readonly<Record<string, BuiltinDep>>,
|
|
38
|
+
): Record<string, SystemDepConfig> {
|
|
39
|
+
const problems: string[] = []
|
|
40
|
+
const usesBuiltin = Object.keys(deps).some((name) => name in catalog)
|
|
41
|
+
if (usesBuiltin && !isArch(arch)) {
|
|
42
|
+
throw new ConfigError(`Invalid system config:\n - built-in deps support x64 and arm64; this machine is ${arch}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const resolved: Record<string, SystemDepConfig> = {}
|
|
46
|
+
for (const [name, entry] of Object.entries(deps)) {
|
|
47
|
+
const builtin = catalog[name]
|
|
48
|
+
if (builtin === undefined) {
|
|
49
|
+
resolved[name] = entry as SystemDepConfig
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
const version = typeof entry === "string" ? entry : entry.version
|
|
53
|
+
if (builtin.versions === "latest" && version !== "latest")
|
|
54
|
+
problems.push(`deps.${name}: ${name} only installs "latest"`)
|
|
55
|
+
if (builtin.versions === "pinned" && version === "latest") {
|
|
56
|
+
problems.push(`deps.${name}: ${name} needs a pinned version, not "latest"`)
|
|
57
|
+
}
|
|
58
|
+
resolved[name] = expand(builtin, entry as BuiltinOverride | string, arch as Arch, os)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* prerequisites: add latest-only built-ins, name the fix for the rest */
|
|
62
|
+
const queue = Object.keys(resolved)
|
|
63
|
+
for (let i = 0; i < queue.length; i++) {
|
|
64
|
+
const name = queue[i] as string
|
|
65
|
+
for (const need of resolved[name]?.dependsOn ?? []) {
|
|
66
|
+
if (need in resolved) continue
|
|
67
|
+
const builtin = catalog[need]
|
|
68
|
+
if (builtin?.versions === "latest") {
|
|
69
|
+
resolved[need] = expand(builtin, "latest", arch as Arch, os)
|
|
70
|
+
queue.push(need)
|
|
71
|
+
} else if (builtin) {
|
|
72
|
+
problems.push(`deps.${name}: needs ${need}; add ${need}: "<version>"`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (problems.length > 0)
|
|
78
|
+
throw new ConfigError(`Invalid system config:\n${problems.map((p) => ` - ${p}`).join("\n")}`)
|
|
79
|
+
return resolved
|
|
80
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript runtimes. bun is the package manager; node and deno are runtimes only.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { LOCAL_SHARE, pick, ps, tree, unix, winArchive, WIN_BIN, WIN_PROGRAMS, winTree } from "./recipes.ts"
|
|
6
|
+
import type { BuiltinDep } from "./types.ts"
|
|
7
|
+
|
|
8
|
+
export const RUNTIMES = {
|
|
9
|
+
bun: {
|
|
10
|
+
description: "Bun: runtime and the only package manager (its version also pins packageManager)",
|
|
11
|
+
env: (os) => [{ path: os === "windows" ? "$HOME\\.bun\\bin" : "$HOME/.bun/bin" }],
|
|
12
|
+
os: ({ version }) => ({
|
|
13
|
+
...unix(
|
|
14
|
+
`curl -fsSL https://bun.sh/install | bash -s "bun-v${version}"`,
|
|
15
|
+
`curl -fsSL https://bun.sh/install | bash -s "bun-v${version}"`,
|
|
16
|
+
),
|
|
17
|
+
windows: ps(`& ([scriptblock]::Create((irm https://bun.sh/install.ps1))) -Version ${version}`),
|
|
18
|
+
}),
|
|
19
|
+
versions: "pinned",
|
|
20
|
+
},
|
|
21
|
+
deno: {
|
|
22
|
+
description: "Deno, for the generated TypeScript SDK checks",
|
|
23
|
+
env: (os) => (os === "windows" ? [{ path: WIN_BIN }] : [{ path: "$HOME/.deno/bin" }]),
|
|
24
|
+
os: ({ version }) => {
|
|
25
|
+
const zip = `https://dl.deno.land/release/v${version}/deno-x86_64-pc-windows-msvc.zip`
|
|
26
|
+
return {
|
|
27
|
+
/* CI=1 keeps the installer from prompting for shell setup */
|
|
28
|
+
...unix(
|
|
29
|
+
`curl -fsSL https://deno.com/install.sh | CI=1 sh -s v${version}`,
|
|
30
|
+
`curl -fsSL https://deno.com/install.sh | CI=1 sh -s v${version}`,
|
|
31
|
+
),
|
|
32
|
+
windows: winArchive(zip, "deno.exe", "deno"),
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
versions: "pinned",
|
|
36
|
+
},
|
|
37
|
+
node: {
|
|
38
|
+
description: "Node.js runtime, for tools that need it (bun stays the package manager)",
|
|
39
|
+
env: (os) => [{ path: os === "windows" ? `${WIN_PROGRAMS}\\node` : `${LOCAL_SHARE}/node/bin` }],
|
|
40
|
+
os: ({ arch, version }) => {
|
|
41
|
+
const base = `https://nodejs.org/dist/v${version}/node-v${version}`
|
|
42
|
+
const a = pick(arch, { arm64: "arm64", x64: "x64" })
|
|
43
|
+
const dir = `${LOCAL_SHARE}/node`
|
|
44
|
+
return {
|
|
45
|
+
...unix(
|
|
46
|
+
tree(`${base}-linux-${a}.tar.xz`, dir, { nested: true }),
|
|
47
|
+
tree(`${base}-darwin-${a}.tar.gz`, dir, { nested: true }),
|
|
48
|
+
),
|
|
49
|
+
windows: winTree(`${base}-win-x64.zip`, `${WIN_PROGRAMS}\\node`, { nested: true }),
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
versions: "pinned",
|
|
53
|
+
},
|
|
54
|
+
} satisfies Record<string, BuiltinDep>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { EnvVar, OsCommands, OsTarget, SystemDepConfig } from "../config.ts"
|
|
2
|
+
|
|
3
|
+
/** CPU architectures built-ins ship recipes for. */
|
|
4
|
+
export type Arch = "arm64" | "x64"
|
|
5
|
+
|
|
6
|
+
export interface RecipeContext {
|
|
7
|
+
arch: Arch
|
|
8
|
+
version: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A tool workerstack supports by name: the config gives a version, the catalog gives the rest. */
|
|
12
|
+
export interface BuiltinDep {
|
|
13
|
+
/** `check` runs this; default `<name> --version` */
|
|
14
|
+
check?: SystemDepConfig["check"]
|
|
15
|
+
/** Other deps installed first. A `latest`-only built-in missing from the config is added; any other is an error. */
|
|
16
|
+
dependsOn?: readonly string[]
|
|
17
|
+
description: string
|
|
18
|
+
/** Shell env entries for the OS the config resolves on (PATH dirs differ between unix and Windows). */
|
|
19
|
+
env?: (os: OsTarget) => readonly EnvVar[]
|
|
20
|
+
/** Per-OS install commands for this version and CPU. */
|
|
21
|
+
os: (ctx: RecipeContext) => OsCommands
|
|
22
|
+
/**
|
|
23
|
+
* `pinned`: needs an exact version, rejects `"latest"`. `latest`: only `"latest"` (the source cannot pin).
|
|
24
|
+
* `any`: a pin or `"latest"`.
|
|
25
|
+
*/
|
|
26
|
+
versions: "any" | "latest" | "pinned"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The object form of a built-in entry: a version plus anything to override. `os` merges per key. */
|
|
30
|
+
export interface BuiltinOverride {
|
|
31
|
+
check?: SystemDepConfig["check"]
|
|
32
|
+
dependsOn?: readonly string[]
|
|
33
|
+
description?: string
|
|
34
|
+
env?: readonly EnvVar[]
|
|
35
|
+
os?: OsCommands
|
|
36
|
+
required?: boolean
|
|
37
|
+
version: string
|
|
38
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { resolve } from "node:path"
|
|
2
|
+
import { resolveOsCommand } from "../config.ts"
|
|
3
|
+
import { versionsMatch } from "../version.ts"
|
|
4
|
+
import { TTY_PATH_FALLBACK_WARN } from "../path.ts"
|
|
5
|
+
import { type Context, probeInstalled } from "../probe.ts"
|
|
6
|
+
import type { Runtime } from "../runtime.ts"
|
|
7
|
+
|
|
8
|
+
/** `--here`: this process PATH. Otherwise a new interactive TTY's PATH, falling back with one warning. */
|
|
9
|
+
function searchPath(rt: Runtime, here: boolean): string {
|
|
10
|
+
if (here) return rt.env.PATH ?? rt.env.Path ?? ""
|
|
11
|
+
const snapshot = rt.ttyPath()
|
|
12
|
+
if (snapshot.source === "process") rt.warn(`${TTY_PATH_FALLBACK_WARN} (${snapshot.reason})`)
|
|
13
|
+
return snapshot.path
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Mismatch count from the packageManager field (0 or 1). bun is the only package manager. */
|
|
17
|
+
function checkPackageManager(ctx: Context): number {
|
|
18
|
+
const bun = ctx.deps.bun
|
|
19
|
+
if (!bun) return 0
|
|
20
|
+
const { rt } = ctx
|
|
21
|
+
const pkg = JSON.parse(rt.readFile(resolve(ctx.configDir, ctx.packageJsonPath))) as { packageManager?: unknown }
|
|
22
|
+
const current = typeof pkg.packageManager === "string" ? pkg.packageManager : undefined
|
|
23
|
+
|
|
24
|
+
if (bun.version === "latest") {
|
|
25
|
+
if (current?.startsWith("bun@")) {
|
|
26
|
+
rt.log(`\n v packageManager: ${current} (latest mode)`)
|
|
27
|
+
return 0
|
|
28
|
+
}
|
|
29
|
+
rt.log(`\n ~ packageManager: ${current ?? "(missing)"} (expected bun@*)`)
|
|
30
|
+
return 1
|
|
31
|
+
}
|
|
32
|
+
const expected = `bun@${bun.version}`
|
|
33
|
+
if (current === expected) {
|
|
34
|
+
rt.log(`\n v packageManager: ${current}`)
|
|
35
|
+
return 0
|
|
36
|
+
}
|
|
37
|
+
rt.log(`\n ~ packageManager: ${current ?? "(missing)"} (expected ${expected})`)
|
|
38
|
+
return 1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function checkCommand(ctx: Context, target: string | undefined, here: boolean): number {
|
|
42
|
+
const { rt } = ctx
|
|
43
|
+
const path = searchPath(rt, here)
|
|
44
|
+
|
|
45
|
+
if (target !== undefined) {
|
|
46
|
+
const config = ctx.deps[target]
|
|
47
|
+
if (!config) {
|
|
48
|
+
rt.error(`Unknown dependency: ${target}`)
|
|
49
|
+
rt.log(`Available: ${Object.keys(ctx.deps).join(", ")}`)
|
|
50
|
+
return 1
|
|
51
|
+
}
|
|
52
|
+
if (resolveOsCommand(target, config, ctx.os) === null) {
|
|
53
|
+
rt.log(`${target} — not available on ${ctx.os}`)
|
|
54
|
+
return 0
|
|
55
|
+
}
|
|
56
|
+
const installed = probeInstalled(rt, target, config, path)
|
|
57
|
+
if (!installed) {
|
|
58
|
+
rt.log(`${target} — not installed (expected ${config.version})`)
|
|
59
|
+
return 1
|
|
60
|
+
}
|
|
61
|
+
rt.log(`${target}@${installed.version} (expected ${config.version}) ${installed.path}`)
|
|
62
|
+
return 0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
rt.log("@workerstack/system — check")
|
|
66
|
+
rt.log(`Detected OS: ${ctx.os}`)
|
|
67
|
+
rt.log("=".repeat(50))
|
|
68
|
+
|
|
69
|
+
let ok = 0
|
|
70
|
+
let mismatch = 0
|
|
71
|
+
let missing = 0
|
|
72
|
+
for (const [name, config] of Object.entries(ctx.deps)) {
|
|
73
|
+
const badge = config.required ? "[required]" : "[optional]"
|
|
74
|
+
if (resolveOsCommand(name, config, ctx.os) === null) {
|
|
75
|
+
rt.log(` - ${badge} ${name} — not available on ${ctx.os}`)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
const installed = probeInstalled(rt, name, config, path)
|
|
79
|
+
if (!installed) {
|
|
80
|
+
rt.log(` x ${badge} ${name} — not installed (expected ${config.version})`)
|
|
81
|
+
if (config.required) missing++
|
|
82
|
+
else mismatch++
|
|
83
|
+
} else if (versionsMatch(installed.version, config.version)) {
|
|
84
|
+
rt.log(` v ${badge} ${name}@${installed.version}`)
|
|
85
|
+
ok++
|
|
86
|
+
} else {
|
|
87
|
+
rt.log(` ~ ${badge} ${name}@${installed.version} (expected ${config.version}) ${installed.path}`)
|
|
88
|
+
mismatch++
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
mismatch += checkPackageManager(ctx)
|
|
92
|
+
|
|
93
|
+
rt.log(`\n${"=".repeat(50)}`)
|
|
94
|
+
rt.log(`OK: ${ok} Mismatch: ${mismatch} Missing: ${missing}`)
|
|
95
|
+
if (missing > 0) {
|
|
96
|
+
rt.log("\nRequired dependencies missing — run: system install")
|
|
97
|
+
return 1
|
|
98
|
+
}
|
|
99
|
+
rt.log(mismatch > 0 ? "\nVersion mismatches found — run: system install --upgrade" : "\nAll system dependencies OK")
|
|
100
|
+
return 0
|
|
101
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { EnvVar } from "../config.ts"
|
|
2
|
+
import {
|
|
3
|
+
applyEnvBlock,
|
|
4
|
+
type EnvLine,
|
|
5
|
+
expandPath,
|
|
6
|
+
hasEnvBlock,
|
|
7
|
+
reloadHint,
|
|
8
|
+
renderEnvBlock,
|
|
9
|
+
shellConfigPaths,
|
|
10
|
+
shellKind,
|
|
11
|
+
} from "../env.ts"
|
|
12
|
+
import type { Context } from "../probe.ts"
|
|
13
|
+
import type { Runtime } from "../runtime.ts"
|
|
14
|
+
|
|
15
|
+
/** The line `env` renders to, or null when a `detect` entry finds nothing and has no fallback. */
|
|
16
|
+
function resolveEnvLine(rt: Runtime, env: EnvVar): EnvLine | null {
|
|
17
|
+
if ("path" in env) return { path: env.path }
|
|
18
|
+
if ("value" in env) return { key: env.key, value: env.value }
|
|
19
|
+
for (const path of env.detect) {
|
|
20
|
+
const expanded = expandPath(path, rt.home, rt.env)
|
|
21
|
+
if (rt.exists(expanded)) {
|
|
22
|
+
rt.log(` Detected ${env.key} at: ${expanded}`)
|
|
23
|
+
return { key: env.key, value: path }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (env.fallback !== undefined) {
|
|
27
|
+
rt.log(` Using fallback for ${env.key}: ${env.fallback}`)
|
|
28
|
+
return { key: env.key, value: env.fallback }
|
|
29
|
+
}
|
|
30
|
+
rt.log(` Could not detect ${env.key}, no fallback specified`)
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rcFiles(rt: Runtime): { kind: ReturnType<typeof shellKind>; paths: string[] } {
|
|
35
|
+
const shell = rt.env.SHELL ?? ""
|
|
36
|
+
const kind = shellKind(rt.platform, shell)
|
|
37
|
+
return { kind, paths: shellConfigPaths(rt.home, kind, shell, rt.env) }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fileName(path: string): string {
|
|
41
|
+
return path.split(/[\\/]/).pop() as string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Write `tool`'s managed block into the shell rc. True when a block was written. */
|
|
45
|
+
export function configureEnvVars(rt: Runtime, tool: string, envVars: readonly EnvVar[]): boolean {
|
|
46
|
+
if (envVars.length === 0) return true
|
|
47
|
+
|
|
48
|
+
const { kind, paths } = rcFiles(rt)
|
|
49
|
+
const names = paths.map(fileName).join(", ")
|
|
50
|
+
rt.log(`\n Configuring environment variables in ${names}...`)
|
|
51
|
+
|
|
52
|
+
const lines = envVars.map((env) => resolveEnvLine(rt, env)).filter((line) => line !== null)
|
|
53
|
+
if (lines.length === 0) {
|
|
54
|
+
rt.log(" No environment variables to configure")
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
for (const path of paths) {
|
|
60
|
+
const existing = rt.exists(path) ? rt.readFile(path) : ""
|
|
61
|
+
if (hasEnvBlock(existing, tool)) rt.log(` Updating existing ${tool} environment config in ${fileName(path)}...`)
|
|
62
|
+
rt.writeFile(path, applyEnvBlock(existing, renderEnvBlock(tool, lines, kind), tool))
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
rt.error(` Failed to configure env vars: ${error instanceof Error ? error.message : String(error)}`)
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
rt.log(` Added to ${names}:`)
|
|
70
|
+
for (const line of lines) rt.log("path" in line ? ` PATH=${line.path}:$PATH` : ` ${line.key}=${line.value}`)
|
|
71
|
+
rt.log(`\n ${reloadHint(kind, paths[0] as string)}`)
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function envCommand(ctx: Context): number {
|
|
76
|
+
const { rt } = ctx
|
|
77
|
+
rt.log("Setting up environment variables")
|
|
78
|
+
rt.log("=".repeat(50))
|
|
79
|
+
|
|
80
|
+
const envDeps = Object.entries(ctx.deps).flatMap(([tool, config]) =>
|
|
81
|
+
config.env && config.env.length > 0 ? [[tool, config.env] as const] : [],
|
|
82
|
+
)
|
|
83
|
+
if (envDeps.length === 0) {
|
|
84
|
+
rt.log("\nNo dependencies with environment variables configured.")
|
|
85
|
+
return 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let failed = 0
|
|
89
|
+
for (const [tool, env] of envDeps) {
|
|
90
|
+
rt.log(`\n${tool}`)
|
|
91
|
+
if (!configureEnvVars(rt, tool, env)) failed++
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { kind, paths } = rcFiles(rt)
|
|
95
|
+
rt.log(`\n${"=".repeat(50)}`)
|
|
96
|
+
rt.log(`Environment variables configured in ${paths.join(", ")}`)
|
|
97
|
+
rt.log(`\n${reloadHint(kind, paths[0] as string)}`)
|
|
98
|
+
return failed > 0 ? 1 : 0
|
|
99
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { resolveOsCommand, type SystemDepConfig } from "../config.ts"
|
|
2
|
+
import { versionsMatch } from "../version.ts"
|
|
3
|
+
import { sortByDependencies } from "../graph.ts"
|
|
4
|
+
import { type Context, probeInstalled } from "../probe.ts"
|
|
5
|
+
import { expandPath } from "../env.ts"
|
|
6
|
+
import { configureEnvVars } from "./env.ts"
|
|
7
|
+
|
|
8
|
+
export interface InstallOptions {
|
|
9
|
+
dryRun: boolean
|
|
10
|
+
upgrade: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Outcome = "failed" | "installed" | "planned" | "skipped" | "unavailable"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Write the dep's env block whenever it is present, installed now or before: an already-installed tool
|
|
17
|
+
* (a CI image's rust, a setup action's bun) is otherwise missing from a new shell's PATH.
|
|
18
|
+
*/
|
|
19
|
+
function writeEnv(ctx: Context, name: string, config: SystemDepConfig): void {
|
|
20
|
+
if (config.env && config.env.length > 0) configureEnvVars(ctx.rt, name, config.env)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function installOne(ctx: Context, name: string, config: SystemDepConfig, opts: InstallOptions): Outcome {
|
|
24
|
+
const { rt } = ctx
|
|
25
|
+
const command = resolveOsCommand(name, config, ctx.os)
|
|
26
|
+
if (!command) {
|
|
27
|
+
rt.log(`${name}@${config.version} — not available on ${ctx.os}`)
|
|
28
|
+
return "unavailable"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
rt.log(`${name}@${config.version}`)
|
|
32
|
+
rt.log(` ${config.description}`)
|
|
33
|
+
|
|
34
|
+
if (opts.dryRun) {
|
|
35
|
+
rt.log(` dry-run: ${command}`)
|
|
36
|
+
return "planned"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const installed = probeInstalled(rt, name, config)
|
|
40
|
+
if (installed && versionsMatch(installed.version, config.version)) {
|
|
41
|
+
rt.log(` Already installed (${installed.version}), skipping`)
|
|
42
|
+
writeEnv(ctx, name, config)
|
|
43
|
+
return "skipped"
|
|
44
|
+
}
|
|
45
|
+
if (installed && !opts.upgrade) {
|
|
46
|
+
rt.log(` Already installed (${installed.version}), skipping (expected ${config.version}) ${installed.path}`)
|
|
47
|
+
rt.log(` run: system install ${name} --upgrade`)
|
|
48
|
+
writeEnv(ctx, name, config)
|
|
49
|
+
return "skipped"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!rt.run(command)) {
|
|
53
|
+
rt.error(`\nCommand failed: ${command}`)
|
|
54
|
+
rt.error(" Installation failed")
|
|
55
|
+
return "failed"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
rt.log(" Installed successfully")
|
|
59
|
+
writeEnv(ctx, name, config)
|
|
60
|
+
/* Installer often does not win PATH — brew/nvm/fnm still first. */
|
|
61
|
+
const after = probeInstalled(rt, name, config)
|
|
62
|
+
if (after && !versionsMatch(after.version, config.version)) {
|
|
63
|
+
rt.log(` PATH still has ${after.version} (expected ${config.version}) ${after.path}`)
|
|
64
|
+
rt.log(" Unpin/unlink that binary or put the pinned one first on PATH")
|
|
65
|
+
}
|
|
66
|
+
return "installed"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Put every dep's `{ path }` dirs first on this run's PATH, so a dep finds a prerequisite installed earlier
|
|
71
|
+
* in the same run (ruff needs uv) and the skip check sees the pinned binary before a system one.
|
|
72
|
+
*/
|
|
73
|
+
function prependDepPaths(ctx: Context): void {
|
|
74
|
+
for (const config of Object.values(ctx.deps)) {
|
|
75
|
+
for (const entry of config.env ?? []) {
|
|
76
|
+
if ("path" in entry) ctx.rt.prependPath(expandPath(entry.path, ctx.rt.home, ctx.rt.env))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function installCommand(ctx: Context, target: string | undefined, opts: InstallOptions): number {
|
|
82
|
+
const { rt } = ctx
|
|
83
|
+
prependDepPaths(ctx)
|
|
84
|
+
|
|
85
|
+
if (target !== undefined) {
|
|
86
|
+
const config = ctx.deps[target]
|
|
87
|
+
if (!config) {
|
|
88
|
+
rt.error(`Unknown dependency: ${target}`)
|
|
89
|
+
rt.log(`Available: ${Object.keys(ctx.deps).join(", ")}`)
|
|
90
|
+
return 1
|
|
91
|
+
}
|
|
92
|
+
rt.log(`Detected OS: ${ctx.os}`)
|
|
93
|
+
return installOne(ctx, target, config, opts) === "failed" ? 1 : 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
rt.log("@workerstack/system — install")
|
|
97
|
+
rt.log(`Detected OS: ${ctx.os}`)
|
|
98
|
+
rt.log("=".repeat(50))
|
|
99
|
+
|
|
100
|
+
const entries = sortByDependencies(Object.entries(ctx.deps))
|
|
101
|
+
rt.log(`\nFound ${entries.length} dependencies to install:\n`)
|
|
102
|
+
for (const [name, config] of entries) {
|
|
103
|
+
const badge = config.required ? "[required]" : "[optional]"
|
|
104
|
+
const available = resolveOsCommand(name, config, ctx.os) !== null
|
|
105
|
+
rt.log(` ${badge} ${name}@${config.version}${available ? "" : ` (not available on ${ctx.os})`}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
rt.log(`\n${"=".repeat(50)}`)
|
|
109
|
+
rt.log(opts.dryRun ? "Dry-run — commands only, nothing installed" : "Installing dependencies...")
|
|
110
|
+
rt.log("=".repeat(50))
|
|
111
|
+
|
|
112
|
+
const counts: Record<Outcome, number> = { failed: 0, installed: 0, planned: 0, skipped: 0, unavailable: 0 }
|
|
113
|
+
for (const [name, config] of entries) {
|
|
114
|
+
const outcome = installOne(ctx, name, config, opts)
|
|
115
|
+
/* an optional dep that fails does not fail the run */
|
|
116
|
+
counts[outcome === "failed" && !config.required ? "skipped" : outcome]++
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
rt.log(`\n${"=".repeat(50)}`)
|
|
120
|
+
rt.log("Installation Summary")
|
|
121
|
+
rt.log("=".repeat(50))
|
|
122
|
+
if (opts.dryRun) rt.log(`Planned: ${counts.planned}`)
|
|
123
|
+
else rt.log(`Installed: ${counts.installed}`)
|
|
124
|
+
rt.log(`Skipped: ${counts.skipped}`)
|
|
125
|
+
rt.log(`Unavailable on ${ctx.os}: ${counts.unavailable}`)
|
|
126
|
+
rt.log(`Failed: ${counts.failed}`)
|
|
127
|
+
|
|
128
|
+
if (counts.failed > 0) {
|
|
129
|
+
rt.log("\nSome required dependencies failed to install")
|
|
130
|
+
return 1
|
|
131
|
+
}
|
|
132
|
+
rt.log(opts.dryRun ? "\nDry-run complete" : "\nAll required system dependencies are ready!")
|
|
133
|
+
return 0
|
|
134
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { BuiltinDep } from "../builtins/types.ts"
|
|
2
|
+
import { OS_TARGETS } from "../config.ts"
|
|
3
|
+
import type { Runtime } from "../runtime.ts"
|
|
4
|
+
|
|
5
|
+
/** The OS targets a built-in has a command for. */
|
|
6
|
+
function availability(builtin: BuiltinDep): string {
|
|
7
|
+
const os = builtin.os({ arch: "x64", version: builtin.versions === "latest" ? "latest" : "0.0.0" })
|
|
8
|
+
return OS_TARGETS.filter((target) => {
|
|
9
|
+
const command = os[target] ?? os.all
|
|
10
|
+
return typeof command === "string"
|
|
11
|
+
}).join(" ")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Print the built-in catalog: name, version policy, where it installs, what it is. */
|
|
15
|
+
export function listCommand(rt: Runtime, catalog: Readonly<Record<string, BuiltinDep>>): number {
|
|
16
|
+
const rows = Object.entries(catalog).map(([name, builtin]) => [
|
|
17
|
+
name,
|
|
18
|
+
builtin.versions,
|
|
19
|
+
availability(builtin),
|
|
20
|
+
builtin.description,
|
|
21
|
+
])
|
|
22
|
+
const widths = [0, 1, 2].map((col) => Math.max(...rows.map((row) => (row[col] as string).length)))
|
|
23
|
+
rt.log('Built-in deps (give a version: go: "1.27.2")')
|
|
24
|
+
rt.log("")
|
|
25
|
+
for (const row of rows) {
|
|
26
|
+
rt.log(` ${row.map((cell, col) => (col < 3 ? cell.padEnd(widths[col] as number) : cell)).join(" ")}`)
|
|
27
|
+
}
|
|
28
|
+
return 0
|
|
29
|
+
}
|