@haakco/hostingctl 0.0.0-stage → 1.0.310

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,142 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The published `hostingctl` command.
4
+ *
5
+ * npm is the delivery truck, not the product: the package carries one self-contained executable per
6
+ * platform, published as its own package, and this launcher runs the one that matches the machine it was
7
+ * installed on. The JavaScript bundle in `dist/` is the fallback for a platform with no published
8
+ * binary, so an install always produces a working command.
9
+ *
10
+ * The launcher adds no output of its own and interprets no flags of its own: the command it runs
11
+ * answers, and its exit status is the command's own 0, 1 or 2.
12
+ */
13
+ import { spawnSync } from "node:child_process";
14
+ import { existsSync } from "node:fs";
15
+ import { createRequire } from "node:module";
16
+ import { constants as osConstants } from "node:os";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import { targets } from "./platforms.js";
20
+
21
+ const here = dirname(fileURLToPath(import.meta.url));
22
+ const require = createRequire(import.meta.url);
23
+ const argv = process.argv.slice(2);
24
+
25
+ /**
26
+ * This launcher's own path, which is the installation the command belongs to.
27
+ *
28
+ * A native-backed install runs the platform package's executable, so the path that executable sees is the
29
+ * **platform package's** — and detection would then read the copy as `@haakco/hostingctl-linux-x64` and
30
+ * update *that*, leaving the parent the client installed untouched. Handing the launcher's own path down as
31
+ * `HOSTINGCTL_ENTRY` keeps the identity of the package the client actually installed, for both the native
32
+ * and the bundle path. A platform package run on its own has no launcher, so it keeps its own identity.
33
+ */
34
+ const launcher = fileURLToPath(import.meta.url);
35
+
36
+ /** The platform package for this machine, or undefined when this platform has no published binary. */
37
+ function targetFor(platform, arch) {
38
+ return targets.find((one) => one.platform === platform && one.arch === arch);
39
+ }
40
+
41
+ /**
42
+ * The executable inside this machine's platform package, when that package is installed.
43
+ *
44
+ * The package is resolved through the launcher's own installation rather than a path guess, so a global
45
+ * install, a local install and an `npx` cache all find the same package. A package that is not installed,
46
+ * or one without the executable in it, is not a binary: the caller falls back to the JavaScript bundle.
47
+ */
48
+ function binaryFor(target) {
49
+ if (target === undefined) return undefined;
50
+
51
+ let manifest;
52
+ try {
53
+ manifest = require.resolve(`${target.package}/package.json`);
54
+ } catch {
55
+ return undefined;
56
+ }
57
+
58
+ const binary = join(dirname(manifest), target.executable);
59
+ return existsSync(binary) ? binary : undefined;
60
+ }
61
+
62
+ /**
63
+ * The command's own exit status.
64
+ *
65
+ * A command killed by a signal has no status of its own, and the shell's own convention for that is 128
66
+ * plus the signal's number, so a Ctrl+C still reads as an interruption rather than as success.
67
+ */
68
+ function exitCodeOf(result) {
69
+ if (result.status !== null) return result.status;
70
+ if (result.signal !== null && result.signal !== undefined) {
71
+ return 128 + (osConstants.signals[result.signal] ?? 0);
72
+ }
73
+
74
+ return 2;
75
+ }
76
+
77
+ /**
78
+ * Run the resolved command and hand back its result.
79
+ *
80
+ * The child's output is inherited, which is what makes the bytes on the terminal and the exit status the
81
+ * command's own. `--version` is the one case that is captured, because the launch path is reported
82
+ * underneath it; the child's own text is still written out unchanged.
83
+ */
84
+ function run(command, args, capture) {
85
+ const result = spawnSync(command, args, {
86
+ stdio: capture ? ["inherit", "pipe", "inherit"] : "inherit",
87
+ encoding: "utf8",
88
+ env: { ...process.env, HOSTINGCTL_ENTRY: launcher },
89
+ });
90
+
91
+ if (capture && typeof result.stdout === "string" && result.stdout !== "") {
92
+ process.stdout.write(result.stdout);
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ const target = targetFor(process.platform, process.arch);
99
+ const forced = process.env.HOSTINGCTL_FORCE_JS === "1";
100
+ const bundle = join(here, "..", "dist", "main.js");
101
+
102
+ // The version is the one answer the launcher adds, because only the launcher knows which of the two
103
+ // paths answered it. `--json` is left alone: a machine-readable answer stays machine-readable.
104
+ const reportsPath = argv.includes("--version") && !argv.includes("--json");
105
+
106
+ let result;
107
+ let path;
108
+
109
+ if (!forced) {
110
+ const binary = binaryFor(target);
111
+ if (binary !== undefined) {
112
+ result = run(binary, argv, reportsPath);
113
+ path = `native binary (${target.id})`;
114
+ if (result.error !== undefined) {
115
+ process.stderr.write(
116
+ `hostingctl: the published binary could not be run (${result.error.message}); using the JavaScript bundle.\n`,
117
+ );
118
+ result = undefined;
119
+ }
120
+ }
121
+ }
122
+
123
+ if (result === undefined) {
124
+ if (!existsSync(bundle)) {
125
+ process.stderr.write(
126
+ "hostingctl: this installation carries neither a native binary nor the JavaScript bundle.\n",
127
+ );
128
+ process.exitCode = 2;
129
+ } else {
130
+ result = run(process.execPath, [bundle, ...argv], reportsPath);
131
+ path =
132
+ forced === true
133
+ ? "JavaScript bundle (HOSTINGCTL_FORCE_JS=1)"
134
+ : `JavaScript bundle (no published binary for ${process.platform}-${process.arch})`;
135
+ }
136
+ }
137
+
138
+ if (reportsPath === true && result !== undefined) {
139
+ process.stdout.write(`Launch path: ${path}\n`);
140
+ }
141
+
142
+ process.exitCode = result === undefined ? 2 : exitCodeOf(result);
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The published platform packages, and the machine each one answers for.
3
+ *
4
+ * This table is the one owner of the five platform names. The launcher resolves the package for the
5
+ * machine it runs on, `scripts/ci/build-cli-binaries.sh` compiles and stages them, and its `check` mode
6
+ * refuses a manifest or an optional dependency that names a package this table does not. A rename
7
+ * therefore happens here and nowhere else.
8
+ *
9
+ * `id` is the target's name in this repository and in a release. `platform` and `arch` are Node's own
10
+ * values for the machine the package answers for, and are what npm's `os` and `cpu` fields need.
11
+ * `bunTarget` is the compile target Bun names it by.
12
+ */
13
+ export const targets = [
14
+ {
15
+ id: "darwin-arm64",
16
+ package: "@haakco/hostingctl-darwin-arm64",
17
+ platform: "darwin",
18
+ arch: "arm64",
19
+ bunTarget: "bun-darwin-arm64",
20
+ executable: "hostingctl",
21
+ },
22
+ {
23
+ id: "darwin-x64",
24
+ package: "@haakco/hostingctl-darwin-x64",
25
+ platform: "darwin",
26
+ arch: "x64",
27
+ bunTarget: "bun-darwin-x64",
28
+ executable: "hostingctl",
29
+ },
30
+ {
31
+ id: "linux-arm64",
32
+ package: "@haakco/hostingctl-linux-arm64",
33
+ platform: "linux",
34
+ arch: "arm64",
35
+ bunTarget: "bun-linux-arm64",
36
+ executable: "hostingctl",
37
+ },
38
+ {
39
+ id: "linux-x64",
40
+ package: "@haakco/hostingctl-linux-x64",
41
+ platform: "linux",
42
+ arch: "x64",
43
+ bunTarget: "bun-linux-x64",
44
+ executable: "hostingctl",
45
+ },
46
+ {
47
+ id: "windows-x64",
48
+ package: "@haakco/hostingctl-windows-x64",
49
+ platform: "win32",
50
+ arch: "x64",
51
+ bunTarget: "bun-windows-x64",
52
+ executable: "hostingctl.exe",
53
+ },
54
+ ];
55
+
56
+ /**
57
+ * The metadata every published package carries, apart from its own name, version and target.
58
+ *
59
+ * The main package and the five platform packages are two surfaces of one product, so they carry one
60
+ * set of answers: the same licence, the same repository, the same documentation site. The distribution
61
+ * check compares the main package's own fields with these, so the two cannot drift apart.
62
+ */
63
+ export const packageFields = {
64
+ license: "UNLICENSED",
65
+ homepage: "https://hosting.haak.co/docs/cli",
66
+ bugs: { url: "https://github.com/haakco/hosting/issues" },
67
+ author: { name: "HaakCo", url: "https://haak.co" },
68
+ publishConfig: { access: "public", registry: "https://registry.npmjs.org/" },
69
+ repository: { type: "git", url: "git+https://github.com/haakco/hosting.git" },
70
+ };