@tomasko/podcomp 0.0.0-stage → 0.1.1

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.
Files changed (4) hide show
  1. package/README.md +94 -2
  2. package/bin/podcomp.js +58 -0
  3. package/lib.js +270 -0
  4. package/package.json +42 -4
package/README.md CHANGED
@@ -1,3 +1,95 @@
1
- # Temporary Holding Version
1
+ # `@tomasko/podcomp`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The npm launcher for [`podcomp`](https://github.com/tomasky/podcomp) — a
4
+ Compose Spec implementation that runs on Podman, written in MoonBit.
5
+
6
+ ```sh
7
+ npm install -g @tomasko/podcomp
8
+ podcomp up
9
+ podcomp ps
10
+ podcomp logs -f web
11
+ podcomp down
12
+ ```
13
+
14
+ `podcomp` reads the same `compose.yaml` / `compose.yml` / `docker-compose.yml`
15
+ files Docker Compose reads, merges and interpolates them, and drives `podman`
16
+ directly. There is no daemon, no Python runtime and no background state: one
17
+ native binary.
18
+
19
+ ## What this package contains
20
+
21
+ A small launcher, not the binary itself. On the first `podcomp` run it
22
+ downloads the prebuilt native binary for your platform from the GitHub release
23
+ and caches it. Nothing is downloaded at install time.
24
+
25
+ | Platform | Target |
26
+ | --- | --- |
27
+ | Linux x86-64 | `podcomp-linux-x86_64` (static, musl) |
28
+ | Linux arm64 | `podcomp-linux-aarch64` (static, musl) |
29
+ | macOS Apple silicon | `podcomp-darwin-aarch64` |
30
+
31
+ The Linux builds are linked statically against musl, so they run on any
32
+ distribution regardless of its glibc version.
33
+
34
+ ## Why there is no `postinstall`
35
+
36
+ This package deliberately declares **no install script**. Since npm 12,
37
+ dependency lifecycle scripts are blocked unless the user opts in; and pnpm 12
38
+ fails a *project* install outright with `ERR_PNPM_IGNORED_BUILDS` when a
39
+ dependency declares one:
40
+
41
+ ```console
42
+ $ pnpm add @tomasko/podcomp
43
+ Error: ERR_PNPM_IGNORED_BUILDS
44
+ × adding a new package
45
+ ╰─▶ Ignored build scripts: @tomasko/podcomp@0.1.1
46
+ ```
47
+
48
+ Downloading lazily from `bin/podcomp.js` sidesteps all of it: it works under
49
+ npm, pnpm, yarn, bun, `npx`, and `--ignore-scripts`, because it is an ordinary
50
+ command rather than an install hook.
51
+
52
+ ## Cache location
53
+
54
+ The downloaded binary is kept outside the package, so reinstalling or clearing
55
+ `node_modules` never triggers a second download:
56
+
57
+ | Platform | Path |
58
+ | --- | --- |
59
+ | Linux | `$XDG_CACHE_HOME/podcomp` or `~/.cache/podcomp` |
60
+ | macOS | `~/Library/Caches/podcomp` |
61
+
62
+ ## Environment variables
63
+
64
+ | Variable | Effect |
65
+ | --- | --- |
66
+ | `PODCOMP_BINARY` | Run this binary and never download anything |
67
+ | `PODCOMP_CACHE_DIR` | Where to keep the downloaded binary |
68
+ | `PODCOMP_VERSION` | Release version to fetch (default: this package's version) |
69
+ | `PODCOMP_REPO` | `owner/repo` to download from (default: `tomasky/podcomp`) |
70
+ | `PODCOMP_DOWNLOAD_BASE` | Full base URL, overriding `PODCOMP_REPO`/`PODCOMP_VERSION` |
71
+ | `PODCOMP_FORCE_DOWNLOAD` | Re-download even if the cached binary exists |
72
+ | `PODCOMP_SKIP_DOWNLOAD` | Never download; fail if the binary is absent |
73
+ | `PODCOMP_NO_VERIFY` | Skip SHA-256 verification of the download |
74
+
75
+ `PODCOMP_DOWNLOAD_BASE` and `PODCOMP_REPO` are there for forks, private mirrors
76
+ and air-gapped installs:
77
+
78
+ ```sh
79
+ PODCOMP_DOWNLOAD_BASE=https://internal.example.com/podcomp npm install -g @tomasko/podcomp
80
+ ```
81
+
82
+ ## Other ways to install
83
+
84
+ ```sh
85
+ # Installer script (no Node.js required)
86
+ curl -LsSf https://raw.githubusercontent.com/tomasky/podcomp/main/install.sh | bash
87
+
88
+ # Build from source
89
+ git clone https://github.com/tomasky/podcomp && cd podcomp
90
+ moon build --target native --release
91
+ ```
92
+
93
+ ## License
94
+
95
+ Apache-2.0
package/bin/podcomp.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // Launcher for the native `podcomp` binary.
3
+ //
4
+ // The package ships no install script, so this is where the binary is fetched:
5
+ // on the first run, straight into the user cache. Arguments, stdio, signals and
6
+ // the exit code are all forwarded to the real binary.
7
+ "use strict";
8
+
9
+ const { spawn } = require("child_process");
10
+ const { ensureBinary } = require("../lib.js");
11
+
12
+ const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
13
+
14
+ async function main() {
15
+ let binary;
16
+ try {
17
+ binary = await ensureBinary({
18
+ log: (line) => process.stderr.write(`${line}\n`),
19
+ });
20
+ } catch (err) {
21
+ process.stderr.write(`podcomp: ${err.message}\n`);
22
+ process.exit(1);
23
+ }
24
+
25
+ const child = spawn(binary, process.argv.slice(2), { stdio: "inherit" });
26
+
27
+ // Forward signals so Ctrl-C reaches the CLI rather than killing only the
28
+ // wrapper and orphaning the child.
29
+ for (const signal of SIGNALS) {
30
+ process.on(signal, () => {
31
+ try {
32
+ child.kill(signal);
33
+ } catch {
34
+ /* already gone */
35
+ }
36
+ });
37
+ }
38
+
39
+ child.on("error", (err) => {
40
+ process.stderr.write(`podcomp: failed to launch ${binary}: ${err.message}\n`);
41
+ process.exit(1);
42
+ });
43
+
44
+ child.on("exit", (code, signal) => {
45
+ if (signal) {
46
+ // Mirror the child's death by signal instead of inventing an exit code.
47
+ process.removeAllListeners(signal);
48
+ process.kill(process.pid, signal);
49
+ return;
50
+ }
51
+ process.exit(code === null ? 1 : code);
52
+ });
53
+ }
54
+
55
+ main().catch((err) => {
56
+ process.stderr.write(`podcomp: ${err && err.stack ? err.stack : err}\n`);
57
+ process.exit(1);
58
+ });
package/lib.js ADDED
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+
3
+ // Shared logic for the npm launcher: work out which prebuilt `podcomp` binary
4
+ // belongs on this machine, and fetch it from GitHub Releases on demand.
5
+ //
6
+ // podcomp deliberately ships **no install script**. As of npm 12 dependency
7
+ // lifecycle scripts are blocked unless the user opts in, and pnpm 12 fails a
8
+ // *project* install outright with ERR_PNPM_IGNORED_BUILDS when a dependency
9
+ // declares one. Downloading on first run works under every package manager,
10
+ // under `--ignore-scripts`, and under `npx`, so that is what this does.
11
+ //
12
+ // Overrides (all optional):
13
+ // PODCOMP_REPO owner/repo (default: tomasky/podcomp)
14
+ // PODCOMP_VERSION release version (default: this package's version)
15
+ // PODCOMP_DOWNLOAD_BASE full base URL (overrides PODCOMP_REPO/PODCOMP_VERSION)
16
+ // PODCOMP_CACHE_DIR where the binary is kept
17
+ // PODCOMP_BINARY run this binary and never download
18
+ // PODCOMP_FORCE_DOWNLOAD re-download even if the binary is already present
19
+ // PODCOMP_SKIP_DOWNLOAD never download; fail if the binary is absent
20
+ // PODCOMP_NO_VERIFY skip SHA-256 verification
21
+
22
+ const crypto = require("crypto");
23
+ const fs = require("fs");
24
+ const os = require("os");
25
+ const path = require("path");
26
+ const https = require("https");
27
+ const http = require("http");
28
+
29
+ const pkg = require("./package.json");
30
+
31
+ // Node's `platform-arch` -> the release target name. Intel macOS is absent on
32
+ // purpose: the MoonBit toolchain no longer ships a darwin-x86_64 target.
33
+ const TARGETS = {
34
+ "linux-x64": "linux-x86_64",
35
+ "linux-arm64": "linux-aarch64",
36
+ "darwin-arm64": "darwin-aarch64",
37
+ };
38
+
39
+ const SUPPORTED = "linux-x86_64, linux-aarch64 and darwin-aarch64";
40
+
41
+ function targetName() {
42
+ return TARGETS[`${process.platform}-${process.arch}`];
43
+ }
44
+
45
+ function repoSlug() {
46
+ if (process.env.PODCOMP_REPO) return process.env.PODCOMP_REPO;
47
+ const url =
48
+ typeof pkg.repository === "string"
49
+ ? pkg.repository
50
+ : (pkg.repository && pkg.repository.url) || "";
51
+ const m = url.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
52
+ return m ? `${m[1]}/${m[2]}` : "tomasky/podcomp";
53
+ }
54
+
55
+ function version() {
56
+ return (process.env.PODCOMP_VERSION || pkg.version).replace(/^v/, "");
57
+ }
58
+
59
+ function assetName(target) {
60
+ return `podcomp-${target}`;
61
+ }
62
+
63
+ // Both URLs are tried in order: the pinned version first, then whatever is
64
+ // latest. The fallback keeps the launcher useful when a release was re-cut
65
+ // under a different tag, or when package.json was bumped ahead of the tag.
66
+ function downloadUrls(target) {
67
+ const asset = assetName(target);
68
+ if (process.env.PODCOMP_DOWNLOAD_BASE) {
69
+ const base = process.env.PODCOMP_DOWNLOAD_BASE.replace(/\/+$/, "");
70
+ return [`${base}/${asset}`];
71
+ }
72
+ const repo = repoSlug();
73
+ return [
74
+ `https://github.com/${repo}/releases/download/v${version()}/${asset}`,
75
+ `https://github.com/${repo}/releases/latest/download/${asset}`,
76
+ ];
77
+ }
78
+
79
+ function cacheDir() {
80
+ if (process.env.PODCOMP_CACHE_DIR) return process.env.PODCOMP_CACHE_DIR;
81
+ const home = os.homedir();
82
+ if (process.platform === "darwin") {
83
+ return path.join(home, "Library", "Caches", "podcomp");
84
+ }
85
+ const base = process.env.XDG_CACHE_HOME || path.join(home, ".cache");
86
+ return path.join(base, "podcomp");
87
+ }
88
+
89
+ function binaryPath(target) {
90
+ return path.join(cacheDir(), `podcomp-${version()}-${target}`);
91
+ }
92
+
93
+ function isRunnable(file) {
94
+ try {
95
+ fs.accessSync(file, fs.constants.X_OK);
96
+ return true;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ // --- HTTP ------------------------------------------------------------------
103
+
104
+ function fetch(url, redirects) {
105
+ return new Promise((resolve, reject) => {
106
+ if (redirects > 10) {
107
+ reject(new Error("too many redirects"));
108
+ return;
109
+ }
110
+ const client = url.startsWith("http:") ? http : https;
111
+ const req = client.get(
112
+ url,
113
+ { headers: { "user-agent": `podcomp-npm/${pkg.version}` } },
114
+ (res) => {
115
+ const { statusCode, headers } = res;
116
+ if (statusCode >= 300 && statusCode < 400 && headers.location) {
117
+ res.resume();
118
+ resolve(fetch(new URL(headers.location, url).toString(), redirects + 1));
119
+ return;
120
+ }
121
+ if (statusCode !== 200) {
122
+ res.resume();
123
+ const err = new Error(`GET ${url} -> HTTP ${statusCode}`);
124
+ err.statusCode = statusCode;
125
+ reject(err);
126
+ return;
127
+ }
128
+ resolve(res);
129
+ }
130
+ );
131
+ req.on("error", reject);
132
+ req.setTimeout(120000, () => req.destroy(new Error(`timed out fetching ${url}`)));
133
+ });
134
+ }
135
+
136
+ async function fetchBuffer(url) {
137
+ const res = await fetch(url, 0);
138
+ const chunks = [];
139
+ for await (const chunk of res) chunks.push(chunk);
140
+ return Buffer.concat(chunks);
141
+ }
142
+
143
+ async function fetchText(url) {
144
+ return (await fetchBuffer(url)).toString("utf8");
145
+ }
146
+
147
+ // --- install ---------------------------------------------------------------
148
+
149
+ // Download to a sibling temp file and rename into place, so a crashed or
150
+ // concurrent run can never leave a half-written binary at `dest`.
151
+ async function downloadTo(url, dest) {
152
+ const res = await fetch(url, 0);
153
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
154
+ const tmp = `${dest}.${process.pid}.${Date.now()}.part`;
155
+ try {
156
+ await new Promise((resolve, reject) => {
157
+ const out = fs.createWriteStream(tmp, { mode: 0o755 });
158
+ res.pipe(out);
159
+ out.on("finish", () => out.close(resolve));
160
+ out.on("error", reject);
161
+ res.on("error", reject);
162
+ });
163
+ const size = fs.statSync(tmp).size;
164
+ if (size === 0) throw new Error(`${url} returned an empty file`);
165
+ fs.chmodSync(tmp, 0o755);
166
+ fs.renameSync(tmp, dest);
167
+ } catch (err) {
168
+ fs.rmSync(tmp, { force: true });
169
+ throw err;
170
+ }
171
+ }
172
+
173
+ // Verify against the `<asset>.sha256` published next to the binary. A missing
174
+ // checksum is a warning, not a failure, so older releases keep working.
175
+ async function verify(dest, url, log) {
176
+ if (process.env.PODCOMP_NO_VERIFY) return;
177
+ let published;
178
+ try {
179
+ published = (await fetchText(`${url}.sha256`)).trim();
180
+ } catch {
181
+ log(`podcomp: warning: no checksum published for ${path.basename(url)}`);
182
+ return;
183
+ }
184
+ const expected = (published.split(/\s+/)[0] || "").toLowerCase();
185
+ if (!/^[0-9a-f]{64}$/.test(expected)) {
186
+ log("podcomp: warning: published checksum is not a SHA-256 digest");
187
+ return;
188
+ }
189
+ const actual = crypto.createHash("sha256").update(fs.readFileSync(dest)).digest("hex");
190
+ if (actual !== expected) {
191
+ fs.rmSync(dest, { force: true });
192
+ throw new Error(
193
+ `checksum mismatch for ${path.basename(url)}\n` +
194
+ ` expected ${expected}\n actual ${actual}`
195
+ );
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Resolve to a runnable `podcomp` binary, downloading it when it is missing.
201
+ */
202
+ async function ensureBinary({ force = false, log = () => {} } = {}) {
203
+ if (process.env.PODCOMP_BINARY) {
204
+ if (!fs.existsSync(process.env.PODCOMP_BINARY)) {
205
+ throw new Error(
206
+ `PODCOMP_BINARY points at ${process.env.PODCOMP_BINARY}, which does not exist`
207
+ );
208
+ }
209
+ return process.env.PODCOMP_BINARY;
210
+ }
211
+
212
+ const target = targetName();
213
+ if (!target) {
214
+ throw new Error(
215
+ `podcomp does not ship a binary for ${process.platform}/${process.arch}; ` +
216
+ `supported: ${SUPPORTED}. Build from source instead: ` +
217
+ `https://github.com/${repoSlug()}#build-from-source`
218
+ );
219
+ }
220
+
221
+ const dest = binaryPath(target);
222
+ const wanted = force || process.env.PODCOMP_FORCE_DOWNLOAD;
223
+ if (!wanted && isRunnable(dest)) return dest;
224
+
225
+ if (process.env.PODCOMP_SKIP_DOWNLOAD) {
226
+ if (isRunnable(dest)) return dest;
227
+ throw new Error(
228
+ `PODCOMP_SKIP_DOWNLOAD is set but no podcomp binary is installed at ${dest}`
229
+ );
230
+ }
231
+
232
+ const urls = downloadUrls(target);
233
+ let lastError;
234
+ for (const url of urls) {
235
+ try {
236
+ log(`podcomp: downloading ${url}`);
237
+ await downloadTo(url, dest);
238
+ await verify(dest, url, log);
239
+ log(`podcomp: installed ${dest}`);
240
+ return dest;
241
+ } catch (err) {
242
+ lastError = err;
243
+ log(`podcomp: ${err.message}`);
244
+ }
245
+ }
246
+
247
+ throw new Error(
248
+ `could not download the podcomp binary for ${target}.\n` +
249
+ `Tried:\n${urls.map((u) => ` ${u}`).join("\n")}\n` +
250
+ `Last error: ${lastError && lastError.message}\n\n` +
251
+ `Install it another way instead:\n` +
252
+ ` curl -LsSf https://raw.githubusercontent.com/${repoSlug()}/main/install.sh | bash\n` +
253
+ `or set PODCOMP_DOWNLOAD_BASE to a mirror.`
254
+ );
255
+ }
256
+
257
+ module.exports = {
258
+ pkg,
259
+ TARGETS,
260
+ SUPPORTED,
261
+ targetName,
262
+ repoSlug,
263
+ version,
264
+ assetName,
265
+ downloadUrls,
266
+ cacheDir,
267
+ binaryPath,
268
+ isRunnable,
269
+ ensureBinary,
270
+ };
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@tomasko/podcomp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Run Compose files with Podman , a single native binary, no daemon, no Python.",
5
+ "license": "Apache-2.0",
6
+ "bin": {
7
+ "podcomp": "bin/podcomp.js"
8
+ },
9
+ "files": [
10
+ "bin/podcomp.js",
11
+ "lib.js",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "os": [
18
+ "darwin",
19
+ "linux"
20
+ ],
21
+ "cpu": [
22
+ "x64",
23
+ "arm64"
24
+ ],
25
+ "scripts": {
26
+ "test": "node --test"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/tomasky/podcomp.git"
31
+ },
32
+ "homepage": "https://github.com/tomasky/podcomp",
33
+ "bugs": "https://github.com/tomasky/podcomp/issues",
34
+ "keywords": [
35
+ "podman",
36
+ "docker-compose",
37
+ "compose",
38
+ "containers",
39
+ "orchestration",
40
+ "cli",
41
+ "moonbit"
42
+ ],
43
+ "preferGlobal": true
44
+ }