@highstate/cli 0.15.0 → 0.16.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.
@@ -0,0 +1,156 @@
1
+ import { readFile } from "node:fs/promises"
2
+ import { Command, Option } from "clipanion"
3
+ import { detectPackageManager } from "nypm"
4
+ import semver from "semver"
5
+ import {
6
+ applyOverrides,
7
+ buildOverrides,
8
+ fetchManifest,
9
+ getDependencyRange,
10
+ getProjectPlatformVersion,
11
+ logger,
12
+ resolveVersionBundle,
13
+ } from "../shared"
14
+
15
+ export class UpdateCommand extends Command {
16
+ static paths = [["update"]]
17
+
18
+ static usage = Command.Usage({
19
+ description: "Updates version overrides in an existing Highstate project.",
20
+ })
21
+
22
+ platformVersion = Option.String("--platform-version", {
23
+ description: "The Highstate platform version to set.",
24
+ })
25
+
26
+ stdlibVersion = Option.String("--stdlib-version", {
27
+ description: "The Highstate standard library version to set.",
28
+ })
29
+
30
+ platformOnly = Option.Boolean("--platform", false, {
31
+ description: "Update only platform versions.",
32
+ })
33
+
34
+ stdlibOnly = Option.Boolean("--stdlib", false, {
35
+ description: "Update only standard library versions.",
36
+ })
37
+
38
+ install = Option.Boolean("--install", true, {
39
+ description: "Install dependencies after updating overrides.",
40
+ })
41
+
42
+ async execute(): Promise<void> {
43
+ const projectRoot = process.cwd()
44
+
45
+ const packageManager = await resolveProjectPackageManager(projectRoot)
46
+
47
+ if (this.platformOnly && this.stdlibOnly) {
48
+ throw new Error('Flags "--platform" and "--stdlib" cannot be used together')
49
+ }
50
+
51
+ const updatePlatform = this.platformOnly || !this.stdlibOnly
52
+ const updateStdlib = this.stdlibOnly || !this.platformOnly
53
+
54
+ if (this.stdlibOnly) {
55
+ const currentPlatformVersion = await getProjectPlatformVersion(projectRoot, {
56
+ packageManager,
57
+ })
58
+ if (!currentPlatformVersion) {
59
+ throw new Error('Current platform version is not set in overrides for "@highstate/pulumi"')
60
+ }
61
+
62
+ const targetStdlibVersion = (this.stdlibVersion ?? "").trim()
63
+ if (targetStdlibVersion.length === 0) {
64
+ throw new Error('Flag "--stdlib-version" must be provided when using "--stdlib"')
65
+ }
66
+
67
+ const stdlibManifest = await fetchManifest("@highstate/library", targetStdlibVersion)
68
+ const supportedPlatformRange = getDependencyRange(stdlibManifest, "@highstate/pulumi")
69
+ if (!supportedPlatformRange) {
70
+ throw new Error(
71
+ `Unable to infer "@highstate/pulumi" version from "@highstate/library@${targetStdlibVersion}"`,
72
+ )
73
+ }
74
+
75
+ const validPlatform = semver.valid(currentPlatformVersion)
76
+ if (!validPlatform) {
77
+ throw new Error(
78
+ `Current platform version is not a valid semver "${currentPlatformVersion}"`,
79
+ )
80
+ }
81
+
82
+ const ok = semver.satisfies(validPlatform, supportedPlatformRange, {
83
+ includePrerelease: true,
84
+ })
85
+ if (!ok) {
86
+ throw new Error(
87
+ `Current platform version "${currentPlatformVersion}" does not satisfy requirement "${supportedPlatformRange}"`,
88
+ )
89
+ }
90
+ }
91
+
92
+ const bundle = await resolveVersionBundle({
93
+ platformVersion: updatePlatform ? this.platformVersion : undefined,
94
+ stdlibVersion: updateStdlib ? this.stdlibVersion : undefined,
95
+ })
96
+
97
+ const overrides = buildOverrides(bundle)
98
+ await applyOverrides({
99
+ projectRoot,
100
+ packageManager,
101
+ overrides,
102
+ })
103
+
104
+ logger.info(
105
+ "updated overrides: platform=%s stdlib=%s pulumi=%s",
106
+ bundle.platformVersion,
107
+ bundle.stdlibVersion,
108
+ bundle.pulumiVersion,
109
+ )
110
+
111
+ if (this.install) {
112
+ const { installDependencies } = await import("nypm")
113
+
114
+ logger.info("installing dependencies using %s...", packageManager)
115
+
116
+ await installDependencies({
117
+ cwd: projectRoot,
118
+ packageManager,
119
+ silent: false,
120
+ })
121
+ }
122
+
123
+ logger.info("update completed successfully")
124
+ }
125
+ }
126
+
127
+ async function resolveProjectPackageManager(projectRoot: string) {
128
+ const detected = await detectPackageManager(projectRoot)
129
+ if (!detected?.name) {
130
+ throw new Error("Unable to detect package manager for this project")
131
+ }
132
+
133
+ if (detected.name === "bun") {
134
+ throw new Error('Package manager "bun" is not supported')
135
+ }
136
+
137
+ if (detected.name === "deno") {
138
+ throw new Error('Package manager "deno" is not supported')
139
+ }
140
+
141
+ if (detected.name !== "npm" && detected.name !== "pnpm" && detected.name !== "yarn") {
142
+ throw new Error(`Unsupported package manager: "${detected.name}"`)
143
+ }
144
+
145
+ await assertPackageJsonExists(projectRoot)
146
+
147
+ return detected.name
148
+ }
149
+
150
+ async function assertPackageJsonExists(projectRoot: string): Promise<void> {
151
+ try {
152
+ await readFile(`${projectRoot}/package.json`, "utf8")
153
+ } catch {
154
+ throw new Error(`File "package.json" not found in "${projectRoot}"`)
155
+ }
156
+ }
package/src/main.ts CHANGED
@@ -1,33 +1,36 @@
1
1
  import { Builtins, Cli } from "clipanion"
2
- import { version } from "../package.json"
3
- import { BackendIdentityCommand } from "./commands/backend/identity"
4
- import { BackendUnlockMethodAddCommand } from "./commands/backend/unlock-method/add"
5
- import { BackendUnlockMethodDeleteCommand } from "./commands/backend/unlock-method/delete"
6
- import { BackendUnlockMethodListCommand } from "./commands/backend/unlock-method/list"
7
- import { BuildCommand } from "./commands/build"
8
- import { DesignerCommand } from "./commands/designer"
9
- import { InitCommand } from "./commands/init"
10
2
  import {
11
- CreateCommand as PackageCreateCommand,
12
- ListCommand as PackageListCommand,
13
- RemoveCommand as PackageRemoveCommand,
14
- UpdateReferencesCommand,
15
- } from "./commands/package"
3
+ BackendIdentityCommand,
4
+ BackendUnlockMethodAddCommand,
5
+ BackendUnlockMethodDeleteCommand,
6
+ BackendUnlockMethodListCommand,
7
+ BuildCommand,
8
+ DesignerCommand,
9
+ InitCommand,
10
+ PackageCreateCommand,
11
+ PackageListCommand,
12
+ PackageRemoveCommand,
13
+ PackageUpdateReferencesCommand,
14
+ UpdateCommand,
15
+ } from "./commands"
16
+
17
+ // const { version } = await import("@highstate/cli/package.json")
16
18
 
17
19
  const cli = new Cli({
18
20
  binaryName: "highstate",
19
21
  binaryLabel: "Highstate",
20
- binaryVersion: version,
22
+ // binaryVersion: version,
21
23
  })
22
24
 
23
25
  cli.register(BuildCommand)
24
26
  cli.register(DesignerCommand)
25
27
  cli.register(InitCommand)
28
+ cli.register(UpdateCommand)
26
29
  cli.register(BackendIdentityCommand)
27
30
  cli.register(BackendUnlockMethodListCommand)
28
31
  cli.register(BackendUnlockMethodAddCommand)
29
32
  cli.register(BackendUnlockMethodDeleteCommand)
30
- cli.register(UpdateReferencesCommand)
33
+ cli.register(PackageUpdateReferencesCommand)
31
34
  cli.register(PackageListCommand)
32
35
  cli.register(PackageCreateCommand)
33
36
  cli.register(PackageRemoveCommand)
@@ -55,18 +55,25 @@ export function extractEntryPoints(packageJson: PackageJson): Record<string, Ent
55
55
  throw new Error(`Export "${key}" must be a string or an object in package.json`)
56
56
  }
57
57
 
58
- if (!distPath.startsWith("./dist/")) {
58
+ const isJsonExport = distPath.endsWith(".json")
59
+ const isJsExport = distPath.endsWith(".js")
60
+
61
+ if (!isJsonExport && !isJsExport) {
59
62
  throw new Error(
60
- `The default value of export "${key}" must start with "./dist/" in package.json, got "${distPath}"`,
63
+ `The default value of export "${key}" must end with ".js" or ".json" in package.json, got "${distPath}"`,
61
64
  )
62
65
  }
63
66
 
64
- if (!distPath.endsWith(".js")) {
67
+ if (isJsExport && !distPath.startsWith("./dist/")) {
65
68
  throw new Error(
66
- `The default value of export "${key}" must end with ".js" in package.json, got "${distPath}"`,
69
+ `The default value of export "${key}" must start with "./dist/" when exporting ".js" in package.json, got "${distPath}"`,
67
70
  )
68
71
  }
69
72
 
73
+ if (isJsonExport) {
74
+ continue
75
+ }
76
+
70
77
  const targetName = distPath.slice(7).slice(0, -3)
71
78
 
72
79
  result[targetName] = {
@@ -2,8 +2,16 @@ export * from "./bin-transformer"
2
2
  export * from "./entry-points"
3
3
  export * from "./generator"
4
4
  export * from "./logger"
5
+ export * from "./npm-registry"
6
+ export * from "./overrides"
7
+ export * from "./package-json"
8
+ export * from "./pnpm-workspace"
9
+ export * from "./project-versions"
10
+ export * from "./pulumi-cli"
5
11
  export * from "./schema-transformer"
6
12
  export * from "./schemas"
7
13
  export * from "./services"
8
14
  export * from "./source-hash-calculator"
15
+ export * from "./version-bundle"
16
+ export * from "./version-sets"
9
17
  export * from "./workspace"
@@ -0,0 +1,67 @@
1
+ export type NpmRegistryManifest = {
2
+ name?: string
3
+ version?: string
4
+ peerDependencies?: Record<string, string>
5
+ dependencies?: Record<string, string>
6
+ optionalDependencies?: Record<string, string>
7
+ }
8
+
9
+ export type NpmRegistryPackument = {
10
+ "dist-tags"?: {
11
+ latest?: string
12
+ }
13
+ versions?: Record<string, NpmRegistryManifest>
14
+ }
15
+
16
+ export async function fetchNpmPackument(packageName: string): Promise<NpmRegistryPackument> {
17
+ const encoded = encodeURIComponent(packageName)
18
+ const url = `https://registry.npmjs.org/${encoded}`
19
+
20
+ const response = await fetch(url)
21
+ if (!response.ok) {
22
+ throw new Error(
23
+ `Failed to fetch package "${packageName}" from NPM registry (HTTP ${response.status})`,
24
+ )
25
+ }
26
+
27
+ return (await response.json()) as NpmRegistryPackument
28
+ }
29
+
30
+ export async function fetchLatestVersion(packageName: string): Promise<string> {
31
+ const packument = await fetchNpmPackument(packageName)
32
+ const latest = packument["dist-tags"]?.latest
33
+ if (!latest) {
34
+ throw new Error(
35
+ `NPM registry response for package "${packageName}" does not include "dist-tags.latest"`,
36
+ )
37
+ }
38
+
39
+ return latest
40
+ }
41
+
42
+ export async function fetchManifest(
43
+ packageName: string,
44
+ version: string,
45
+ ): Promise<NpmRegistryManifest> {
46
+ const packument = await fetchNpmPackument(packageName)
47
+ const manifest = packument.versions?.[version]
48
+ if (!manifest) {
49
+ throw new Error(
50
+ `NPM registry response for package "${packageName}" does not include version "${version}"`,
51
+ )
52
+ }
53
+
54
+ return manifest
55
+ }
56
+
57
+ export function getDependencyRange(
58
+ manifest: NpmRegistryManifest,
59
+ dependencyName: string,
60
+ ): string | null {
61
+ return (
62
+ manifest.peerDependencies?.[dependencyName] ??
63
+ manifest.dependencies?.[dependencyName] ??
64
+ manifest.optionalDependencies?.[dependencyName] ??
65
+ null
66
+ )
67
+ }
@@ -0,0 +1,73 @@
1
+ import type { PackageManagerName } from "nypm"
2
+ import type { VersionBundle } from "./version-bundle"
3
+ import { access } from "node:fs/promises"
4
+ import { readPackageJSON, resolvePackageJSON } from "pkg-types"
5
+ import { writeJsonFile } from "./package-json"
6
+ import { readPnpmWorkspace, resolvePnpmWorkspacePath, writePnpmWorkspace } from "./pnpm-workspace"
7
+ import { PLATFORM_PACKAGES, PULUMI_PACKAGES, STDLIB_PACKAGES } from "./version-sets"
8
+
9
+ export type Overrides = Record<string, string>
10
+
11
+ export function buildOverrides(bundle: VersionBundle): Overrides {
12
+ const platform = Object.fromEntries(PLATFORM_PACKAGES.map(name => [name, bundle.platformVersion]))
13
+ const stdlib = Object.fromEntries(STDLIB_PACKAGES.map(name => [name, bundle.stdlibVersion]))
14
+ const pulumi = Object.fromEntries(PULUMI_PACKAGES.map(name => [name, bundle.pulumiVersion]))
15
+
16
+ const merged: Overrides = {
17
+ ...platform,
18
+ ...stdlib,
19
+ ...pulumi,
20
+ }
21
+
22
+ return merged
23
+ }
24
+
25
+ export type ApplyOverridesArgs = {
26
+ packageManager: PackageManagerName
27
+ overrides: Overrides
28
+ projectRoot: string
29
+ }
30
+
31
+ export async function applyOverrides(args: ApplyOverridesArgs): Promise<void> {
32
+ const { packageManager, overrides, projectRoot } = args
33
+
34
+ if (packageManager === "pnpm") {
35
+ const pnpmWorkspacePath = resolvePnpmWorkspacePath(projectRoot)
36
+
37
+ try {
38
+ await access(pnpmWorkspacePath)
39
+ } catch {
40
+ throw new Error(`PNPM workspace file is missing: "${pnpmWorkspacePath}"`)
41
+ }
42
+
43
+ const workspace = await readPnpmWorkspace(pnpmWorkspacePath)
44
+ const nextWorkspace = {
45
+ ...workspace,
46
+ overrides,
47
+ }
48
+
49
+ await writePnpmWorkspace(pnpmWorkspacePath, nextWorkspace)
50
+ return
51
+ }
52
+
53
+ const packageJsonPath = await resolvePackageJSON(projectRoot)
54
+ const packageJson = await readPackageJSON(projectRoot)
55
+
56
+ if (packageManager === "npm") {
57
+ await writeJsonFile(packageJsonPath, {
58
+ ...packageJson,
59
+ overrides,
60
+ })
61
+ return
62
+ }
63
+
64
+ if (packageManager === "yarn") {
65
+ await writeJsonFile(packageJsonPath, {
66
+ ...packageJson,
67
+ resolutions: overrides,
68
+ })
69
+ return
70
+ }
71
+
72
+ await writeJsonFile(packageJsonPath, packageJson)
73
+ }
@@ -0,0 +1,6 @@
1
+ import { writeFile } from "node:fs/promises"
2
+
3
+ export async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
4
+ const contents = `${JSON.stringify(value, null, 2)}\n`
5
+ await writeFile(filePath, contents, "utf8")
6
+ }
@@ -0,0 +1,32 @@
1
+ import { readFile, writeFile } from "node:fs/promises"
2
+ import { resolve } from "node:path"
3
+ import { parse, stringify } from "yaml"
4
+
5
+ export type PnpmWorkspace = {
6
+ packages?: string[]
7
+ overrides?: Record<string, string>
8
+ }
9
+
10
+ export function resolvePnpmWorkspacePath(projectRoot: string): string {
11
+ return resolve(projectRoot, "pnpm-workspace.yaml")
12
+ }
13
+
14
+ export async function readPnpmWorkspace(filePath: string): Promise<PnpmWorkspace> {
15
+ const raw = await readFile(filePath, "utf8")
16
+ const parsed = parse(raw)
17
+
18
+ if (typeof parsed !== "object" || parsed === null) {
19
+ return {}
20
+ }
21
+
22
+ return parsed as PnpmWorkspace
23
+ }
24
+
25
+ export async function writePnpmWorkspace(
26
+ filePath: string,
27
+ workspace: PnpmWorkspace,
28
+ ): Promise<void> {
29
+ const raw = stringify(workspace)
30
+
31
+ await writeFile(filePath, raw, "utf8")
32
+ }
@@ -0,0 +1,54 @@
1
+ import type { PackageManagerName } from "nypm"
2
+ import type { PackageJson } from "pkg-types"
3
+ import { readFile } from "node:fs/promises"
4
+ import { resolvePackageJSON } from "pkg-types"
5
+ import { readPnpmWorkspace, resolvePnpmWorkspacePath } from "./pnpm-workspace"
6
+
7
+ export async function getProjectOverrideVersion(
8
+ projectRoot: string,
9
+ args: { packageManager: PackageManagerName; packageName: string },
10
+ ): Promise<string | null> {
11
+ const { packageManager, packageName } = args
12
+
13
+ if (packageManager === "pnpm") {
14
+ const path = resolvePnpmWorkspacePath(projectRoot)
15
+ const workspace = await readPnpmWorkspace(path)
16
+ return workspace.overrides?.[packageName] ?? null
17
+ }
18
+
19
+ const packageJsonPath = await resolvePackageJSON(projectRoot)
20
+ const rawPackageJson = await readFile(packageJsonPath, "utf8")
21
+ const packageJson = JSON.parse(rawPackageJson) as PackageJson
22
+
23
+ if (packageManager === "npm") {
24
+ const overrides = packageJson.overrides as Record<string, string> | undefined
25
+ return overrides?.[packageName] ?? null
26
+ }
27
+
28
+ if (packageManager === "yarn") {
29
+ const resolutions = packageJson.resolutions as Record<string, string> | undefined
30
+ return resolutions?.[packageName] ?? null
31
+ }
32
+
33
+ return null
34
+ }
35
+
36
+ export async function getProjectPlatformVersion(
37
+ projectRoot: string,
38
+ args: { packageManager: PackageManagerName },
39
+ ): Promise<string | null> {
40
+ return await getProjectOverrideVersion(projectRoot, {
41
+ packageManager: args.packageManager,
42
+ packageName: "@highstate/pulumi",
43
+ })
44
+ }
45
+
46
+ export async function getProjectPulumiSdkVersion(
47
+ projectRoot: string,
48
+ args: { packageManager: PackageManagerName },
49
+ ): Promise<string | null> {
50
+ return await getProjectOverrideVersion(projectRoot, {
51
+ packageManager: args.packageManager,
52
+ packageName: "@pulumi/pulumi",
53
+ })
54
+ }
@@ -0,0 +1,21 @@
1
+ import { execFile } from "node:child_process"
2
+ import { promisify } from "node:util"
3
+
4
+ const execFileAsync = promisify(execFile)
5
+
6
+ export async function getPulumiCliVersion(cwd: string): Promise<string | null> {
7
+ try {
8
+ const { stdout } = await execFileAsync("pulumi", ["version"], {
9
+ cwd,
10
+ })
11
+
12
+ const raw = stdout.trim()
13
+ if (raw.length === 0) {
14
+ return null
15
+ }
16
+
17
+ return raw.startsWith("v") ? raw.slice(1) : raw
18
+ } catch {
19
+ return null
20
+ }
21
+ }
@@ -0,0 +1,51 @@
1
+ import { fetchLatestVersion, fetchManifest, getDependencyRange } from "./npm-registry"
2
+
3
+ export type VersionBundle = {
4
+ platformVersion: string
5
+ stdlibVersion: string
6
+ pulumiVersion: string
7
+ }
8
+
9
+ export type ResolveVersionBundleArgs = {
10
+ platformVersion?: string
11
+ stdlibVersion?: string
12
+ }
13
+
14
+ const platformSourcePackage = "@highstate/pulumi"
15
+ const stdlibSourcePackage = "@highstate/library"
16
+
17
+ export async function resolveVersionBundle(args: ResolveVersionBundleArgs): Promise<VersionBundle> {
18
+ const platformVersion = normalizeProvidedVersion(args.platformVersion, "platform")
19
+ const stdlibVersion = normalizeProvidedVersion(args.stdlibVersion, "stdlib")
20
+
21
+ const resolvedPlatformVersion =
22
+ platformVersion ?? (await fetchLatestVersion(platformSourcePackage))
23
+ const resolvedStdlibVersion = stdlibVersion ?? (await fetchLatestVersion(stdlibSourcePackage))
24
+
25
+ const platformManifest = await fetchManifest(platformSourcePackage, resolvedPlatformVersion)
26
+ const inferredPulumi = getDependencyRange(platformManifest, "@pulumi/pulumi")
27
+ if (!inferredPulumi) {
28
+ throw new Error(
29
+ `Unable to infer "@pulumi/pulumi" version from "${platformSourcePackage}@${resolvedPlatformVersion}"`,
30
+ )
31
+ }
32
+
33
+ return {
34
+ platformVersion: resolvedPlatformVersion,
35
+ stdlibVersion: resolvedStdlibVersion,
36
+ pulumiVersion: inferredPulumi,
37
+ }
38
+ }
39
+
40
+ function normalizeProvidedVersion(value: string | undefined, label: string): string | undefined {
41
+ if (value === undefined) {
42
+ return undefined
43
+ }
44
+
45
+ const trimmed = value.trim()
46
+ if (trimmed.length === 0) {
47
+ throw new Error(`Version flag "${label}" must not be empty`)
48
+ }
49
+
50
+ return trimmed
51
+ }
@@ -0,0 +1,36 @@
1
+ export const PLATFORM_PACKAGES = [
2
+ "@highstate/api",
3
+ "@highstate/backend",
4
+ "@highstate/backend-api",
5
+ "@highstate/cli",
6
+ "@highstate/contract",
7
+ "@highstate/designer",
8
+ "@highstate/pulumi",
9
+ "@highstate/worker-sdk",
10
+ ]
11
+
12
+ export const STDLIB_PACKAGES = [
13
+ "@highstate/cilium",
14
+ "@highstate/cloudflare",
15
+ "@highstate/common",
16
+ "@highstate/distributions",
17
+ "@highstate/git",
18
+ "@highstate/k3s",
19
+ "@highstate/k8s",
20
+ "@highstate/k8s.apps",
21
+ "@highstate/k8s.game-servers",
22
+ "@highstate/k8s.monitor-worker",
23
+ "@highstate/k8s.obfuscators",
24
+ "@highstate/library",
25
+ "@highstate/mullvad",
26
+ "@highstate/nixos",
27
+ "@highstate/proxmox",
28
+ "@highstate/restic",
29
+ "@highstate/sops",
30
+ "@highstate/talos",
31
+ "@highstate/timeweb",
32
+ "@highstate/wireguard",
33
+ "@highstate/yandex",
34
+ ]
35
+
36
+ export const PULUMI_PACKAGES = ["@pulumi/pulumi"]
@@ -1,4 +0,0 @@
1
- export { CreateCommand } from "./create"
2
- export { ListCommand } from "./list"
3
- export { RemoveCommand } from "./remove"
4
- export { UpdateReferencesCommand } from "./update-references"