@dynomate/cli 0.0.10

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 ADDED
@@ -0,0 +1,7 @@
1
+ Dynomate CLI Proprietary License Notice
2
+
3
+ Copyright (c) 2026 Dynomate. All rights reserved.
4
+
5
+ This software is proprietary and confidential. Use is permitted only under the
6
+ applicable Dynomate terms. No permission is granted to redistribute, modify,
7
+ sublicense, or reverse engineer this software.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @dynomate/cli
2
+
3
+ This package installs the native Dynomate command-line client. Test releases use
4
+ the npm `next` tag:
5
+
6
+ ```bash
7
+ npm install --global @dynomate/cli@next
8
+ dynomate-cli --version
9
+ ```
10
+
11
+ Run the same install command again to upgrade. The package exposes only
12
+ `dynomate-cli`; `dynomate` is reserved for the desktop application.
13
+
14
+ ## Supported platforms
15
+
16
+ - macOS 11 or newer on Apple Silicon (`arm64`)
17
+ - macOS 11 or newer on Intel (`x64`)
18
+ - 64-bit GNU/Linux (`x64`) with glibc 2.35 or newer (Ubuntu 22.04 baseline)
19
+ - 64-bit Windows (`x64`)
20
+
21
+ Installation downloads the archive for the current platform from the immutable
22
+ release at
23
+ `https://releases.dynomate.io/cli/dynomate-cli-v<version>/`. The installer reads
24
+ that release's `SHA256SUMS`, verifies the archive before extracting it, and then
25
+ atomically installs only the `dynomate-cli` executable.
26
+
27
+ Install scripts are required. If npm is run with `--ignore-scripts`, the native
28
+ executable is not downloaded and the launcher exits with instructions to
29
+ reinstall normally. The installer also needs HTTPS access to
30
+ `releases.dynomate.io`.
31
+
32
+ If install scripts cannot run, download the archive for your platform and the
33
+ [SHA256SUMS](https://releases.dynomate.io/cli/dynomate-cli-v0.0.10/SHA256SUMS)
34
+ manifest directly, verify it, and invoke `dynomate-cli` (`dynomate-cli.exe` on
35
+ Windows) yourself:
36
+
37
+ - [macOS Apple Silicon](https://releases.dynomate.io/cli/dynomate-cli-v0.0.10/dynomate-cli-v0.0.10-aarch64-apple-darwin.tar.gz)
38
+ - [macOS Intel](https://releases.dynomate.io/cli/dynomate-cli-v0.0.10/dynomate-cli-v0.0.10-x86_64-apple-darwin.tar.gz)
39
+ - [GNU/Linux x64](https://releases.dynomate.io/cli/dynomate-cli-v0.0.10/dynomate-cli-v0.0.10-x86_64-unknown-linux-gnu.tar.gz)
40
+ - [Windows x64](https://releases.dynomate.io/cli/dynomate-cli-v0.0.10/dynomate-cli-v0.0.10-x86_64-pc-windows-msvc.zip)
41
+
42
+ ## License
43
+
44
+ Dynomate CLI is proprietary software. See `LICENSE` in this package.
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { launch } from "../lib/launcher.js";
4
+
5
+ try {
6
+ const { code, signal } = await launch();
7
+ if (signal) {
8
+ process.removeAllListeners(signal);
9
+ process.kill(process.pid, signal);
10
+ } else {
11
+ process.exitCode = code ?? 1;
12
+ }
13
+ } catch (error) {
14
+ console.error(`dynomate-cli: ${error.message}`);
15
+ process.exitCode = 1;
16
+ }
package/lib/archive.js ADDED
@@ -0,0 +1,117 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { gunzipSync, inflateRawSync } from "node:zlib";
3
+
4
+ function basename(path) {
5
+ return path.replaceAll("\\", "/").split("/").filter(Boolean).at(-1);
6
+ }
7
+
8
+ function tarString(buffer, start, length) {
9
+ return buffer.subarray(start, start + length).toString("utf8").replace(/\0.*$/, "").trim();
10
+ }
11
+
12
+ function oneExecutable(matches, executableName) {
13
+ if (matches.length === 0) throw new Error(`Archive does not contain ${executableName}`);
14
+ if (matches.length > 1) throw new Error(`Archive contains more than one ${executableName}`);
15
+ if (matches[0].length === 0) throw new Error(`Archive contains an empty ${executableName}`);
16
+ return matches[0];
17
+ }
18
+
19
+ function extractTarGz(buffer, executableName) {
20
+ const tar = gunzipSync(buffer);
21
+ const matches = [];
22
+ for (let offset = 0; offset + 512 <= tar.length;) {
23
+ const header = tar.subarray(offset, offset + 512);
24
+ if (header.every((byte) => byte === 0)) break;
25
+ const checksumText = tarString(header, 148, 8).replace(/^0+/, "") || "0";
26
+ if (!/^[0-7]+$/.test(checksumText)) throw new Error("Archive contains an invalid tar header checksum");
27
+ const expectedChecksum = Number.parseInt(checksumText, 8);
28
+ let actualChecksum = 0;
29
+ for (let index = 0; index < 512; index += 1) {
30
+ actualChecksum += index >= 148 && index < 156 ? 32 : header[index];
31
+ }
32
+ if (actualChecksum !== expectedChecksum) throw new Error("Archive contains a corrupt tar header");
33
+ const prefix = tarString(header, 345, 155);
34
+ const name = `${prefix}${prefix ? "/" : ""}${tarString(header, 0, 100)}`;
35
+ const sizeText = tarString(header, 124, 12).replace(/^0+/, "") || "0";
36
+ if (!/^[0-7]+$/.test(sizeText)) throw new Error("Archive contains an invalid tar entry size");
37
+ const size = Number.parseInt(sizeText, 8);
38
+ const type = header[156];
39
+ const dataStart = offset + 512;
40
+ const dataEnd = dataStart + size;
41
+ if (!Number.isSafeInteger(size) || dataEnd > tar.length) throw new Error("Archive contains a truncated tar entry");
42
+ if (basename(name) === executableName) {
43
+ if (type !== 0 && type !== 48) throw new Error(`Archive entry ${name} is not a regular file`);
44
+ matches.push(Buffer.from(tar.subarray(dataStart, dataEnd)));
45
+ }
46
+ offset = dataStart + Math.ceil(size / 512) * 512;
47
+ }
48
+ return oneExecutable(matches, executableName);
49
+ }
50
+
51
+ function crc32(buffer) {
52
+ let crc = 0xffffffff;
53
+ for (const byte of buffer) {
54
+ crc ^= byte;
55
+ for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
56
+ }
57
+ return (crc ^ 0xffffffff) >>> 0;
58
+ }
59
+
60
+ function extractZip(buffer, executableName) {
61
+ let eocd = -1;
62
+ for (let offset = buffer.length - 22; offset >= Math.max(0, buffer.length - 65_557); offset -= 1) {
63
+ if (buffer.readUInt32LE(offset) === 0x06054b50) { eocd = offset; break; }
64
+ }
65
+ if (eocd < 0) throw new Error("Archive is not a valid ZIP file");
66
+ const entries = buffer.readUInt16LE(eocd + 10);
67
+ let offset = buffer.readUInt32LE(eocd + 16);
68
+ const matches = [];
69
+ for (let index = 0; index < entries; index += 1) {
70
+ if (offset + 46 > buffer.length || buffer.readUInt32LE(offset) !== 0x02014b50) throw new Error("ZIP central directory is invalid");
71
+ const flags = buffer.readUInt16LE(offset + 8);
72
+ const method = buffer.readUInt16LE(offset + 10);
73
+ const expectedCrc = buffer.readUInt32LE(offset + 16);
74
+ const compressedSize = buffer.readUInt32LE(offset + 20);
75
+ const uncompressedSize = buffer.readUInt32LE(offset + 24);
76
+ const nameLength = buffer.readUInt16LE(offset + 28);
77
+ const extraLength = buffer.readUInt16LE(offset + 30);
78
+ const commentLength = buffer.readUInt16LE(offset + 32);
79
+ const localOffset = buffer.readUInt32LE(offset + 42);
80
+ const nameEnd = offset + 46 + nameLength;
81
+ if (nameEnd > buffer.length) throw new Error("ZIP central directory is truncated");
82
+ const name = buffer.subarray(offset + 46, nameEnd).toString("utf8");
83
+ if (basename(name) === executableName) {
84
+ if (flags & 1) throw new Error("Encrypted ZIP entries are not supported");
85
+ if (compressedSize === 0xffffffff || uncompressedSize === 0xffffffff || localOffset === 0xffffffff) throw new Error("ZIP64 archives are not supported");
86
+ if (localOffset + 30 > buffer.length || buffer.readUInt32LE(localOffset) !== 0x04034b50) throw new Error("ZIP local header is invalid");
87
+ const localNameLength = buffer.readUInt16LE(localOffset + 26);
88
+ const localExtraLength = buffer.readUInt16LE(localOffset + 28);
89
+ const start = localOffset + 30 + localNameLength + localExtraLength;
90
+ const end = start + compressedSize;
91
+ if (end > buffer.length) throw new Error("Archive contains a truncated ZIP entry");
92
+ const compressed = buffer.subarray(start, end);
93
+ const data = method === 0 ? Buffer.from(compressed) : method === 8 ? inflateRawSync(compressed) : null;
94
+ if (!data) throw new Error(`Unsupported ZIP compression method: ${method}`);
95
+ if (data.length !== uncompressedSize || crc32(data) !== expectedCrc) throw new Error(`ZIP entry ${name} failed integrity validation`);
96
+ matches.push(data);
97
+ }
98
+ offset += 46 + nameLength + extraLength + commentLength;
99
+ }
100
+ return oneExecutable(matches, executableName);
101
+ }
102
+
103
+ export async function extractExecutable(archivePath, format, executableName, destination) {
104
+ const archive = await readFile(archivePath);
105
+ let executable;
106
+ try {
107
+ executable = format === "tar.gz"
108
+ ? extractTarGz(archive, executableName)
109
+ : format === "zip"
110
+ ? extractZip(archive, executableName)
111
+ : null;
112
+ } catch (error) {
113
+ throw new Error(`Could not extract ${executableName}: ${error.message}`, { cause: error });
114
+ }
115
+ if (!executable) throw new Error(`Unsupported archive format: ${format}`);
116
+ await writeFile(destination, executable, { flag: "wx", mode: 0o755 });
117
+ }
package/lib/install.js ADDED
@@ -0,0 +1,54 @@
1
+ import { chmod, mkdir, mkdtemp, rename, rm, stat } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { extractExecutable } from "./archive.js";
4
+ import { downloadFile, downloadText } from "./network.js";
5
+ import { nativeBinaryPath } from "./paths.js";
6
+ import { parseChecksumManifest, releaseDescriptor } from "./release.js";
7
+
8
+ async function exists(path) {
9
+ try {
10
+ await stat(path);
11
+ return true;
12
+ } catch (error) {
13
+ if (error.code === "ENOENT") return false;
14
+ throw error;
15
+ }
16
+ }
17
+
18
+ export async function installNative(options = {}) {
19
+ const descriptor = releaseDescriptor(options);
20
+ const destination = options.destination || nativeBinaryPath(descriptor);
21
+ const parent = dirname(destination);
22
+ await mkdir(parent, { recursive: true });
23
+ const temporaryDirectory = await mkdtemp(join(parent, ".install-"));
24
+ const archivePath = join(temporaryDirectory, descriptor.archiveName);
25
+ const stagedBinary = join(temporaryDirectory, descriptor.executable);
26
+ const backup = `${destination}.previous-${process.pid}`;
27
+ let movedExisting = false;
28
+ try {
29
+ const manifest = await downloadText(descriptor.checksumUrl);
30
+ const expectedHash = parseChecksumManifest(manifest, descriptor.archiveName);
31
+ const actualHash = await downloadFile(descriptor.archiveUrl, archivePath);
32
+ if (actualHash !== expectedHash) {
33
+ throw new Error(`Checksum mismatch for ${descriptor.archiveName}: expected ${expectedHash}, received ${actualHash}`);
34
+ }
35
+ await extractExecutable(archivePath, descriptor.format, descriptor.executable, stagedBinary);
36
+ if (descriptor.platform !== "win32") await chmod(stagedBinary, 0o755);
37
+ if (await exists(destination)) {
38
+ await rm(backup, { force: true });
39
+ await rename(destination, backup);
40
+ movedExisting = true;
41
+ }
42
+ try {
43
+ await rename(stagedBinary, destination);
44
+ } catch (error) {
45
+ if (movedExisting) await rename(backup, destination);
46
+ movedExisting = false;
47
+ throw error;
48
+ }
49
+ if (movedExisting) await rm(backup, { force: true });
50
+ return { destination, descriptor, sha256: actualHash };
51
+ } finally {
52
+ await rm(temporaryDirectory, { recursive: true, force: true });
53
+ }
54
+ }
@@ -0,0 +1,43 @@
1
+ import { spawn } from "node:child_process";
2
+ import { access } from "node:fs/promises";
3
+ import { constants } from "node:fs";
4
+ import { nativeBinaryPath } from "./paths.js";
5
+
6
+ export async function ensureNativeBinary(binary = nativeBinaryPath()) {
7
+ try {
8
+ await access(binary, process.platform === "win32" ? constants.F_OK : constants.X_OK);
9
+ } catch {
10
+ throw new Error(
11
+ "The native Dynomate CLI is not installed. Reinstall @dynomate/cli without --ignore-scripts, or use a direct archive from https://releases.dynomate.io/cli/.",
12
+ );
13
+ }
14
+ return binary;
15
+ }
16
+
17
+ export function spawnNative(binary, args, options = {}) {
18
+ const child = spawn(binary, args, {
19
+ stdio: "inherit",
20
+ windowsHide: false,
21
+ ...options,
22
+ });
23
+ const forwardedSignals = process.platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
24
+ const handlers = new Map();
25
+ for (const signal of forwardedSignals) {
26
+ const handler = () => {
27
+ if (!child.killed) child.kill(signal);
28
+ };
29
+ handlers.set(signal, handler);
30
+ process.on(signal, handler);
31
+ }
32
+ return new Promise((resolve, reject) => {
33
+ child.once("error", reject);
34
+ child.once("close", (code, signal) => resolve({ code, signal }));
35
+ }).finally(() => {
36
+ for (const [signal, handler] of handlers) process.off(signal, handler);
37
+ });
38
+ }
39
+
40
+ export async function launch(args = process.argv.slice(2), options = {}) {
41
+ const binary = await ensureNativeBinary(options.binary);
42
+ return spawnNative(binary, args, options.spawnOptions);
43
+ }
package/lib/network.js ADDED
@@ -0,0 +1,61 @@
1
+ import { createWriteStream } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { get as httpGet } from "node:http";
4
+ import { get as httpsGet } from "node:https";
5
+ import { pipeline } from "node:stream/promises";
6
+
7
+ const MAX_REDIRECTS = 5;
8
+ const MAX_MANIFEST_BYTES = 1024 * 1024;
9
+ const REQUEST_TIMEOUT_MS = 30_000;
10
+
11
+ function open(url, redirects = 0) {
12
+ return new Promise((resolve, reject) => {
13
+ const parsed = new URL(url);
14
+ const get = parsed.protocol === "https:" ? httpsGet : parsed.protocol === "http:" ? httpGet : null;
15
+ if (!get) return reject(new Error(`Unsupported download protocol: ${parsed.protocol}`));
16
+ const request = get(parsed, { headers: { "user-agent": "@dynomate/cli installer" } }, (response) => {
17
+ const status = response.statusCode ?? 0;
18
+ if (status >= 300 && status < 400 && response.headers.location) {
19
+ response.resume();
20
+ if (redirects >= MAX_REDIRECTS) return reject(new Error(`Too many redirects while downloading ${url}`));
21
+ const redirected = new URL(response.headers.location, parsed);
22
+ if (parsed.protocol === "https:" && redirected.protocol !== "https:") {
23
+ return reject(new Error(`Refusing insecure redirect while downloading ${url}`));
24
+ }
25
+ return resolve(open(redirected.href, redirects + 1));
26
+ }
27
+ if (status !== 200) {
28
+ response.resume();
29
+ return reject(new Error(`Download failed with HTTP ${status}: ${url}`));
30
+ }
31
+ resolve(response);
32
+ });
33
+ request.setTimeout(REQUEST_TIMEOUT_MS, () => {
34
+ request.destroy(new Error(`Download timed out after ${REQUEST_TIMEOUT_MS} ms: ${url}`));
35
+ });
36
+ request.on("error", (error) => reject(new Error(`Download failed for ${url}: ${error.message}`, { cause: error })));
37
+ });
38
+ }
39
+
40
+ export async function downloadText(url) {
41
+ const response = await open(url);
42
+ const chunks = [];
43
+ let length = 0;
44
+ for await (const chunk of response) {
45
+ length += chunk.length;
46
+ if (length > MAX_MANIFEST_BYTES) {
47
+ response.destroy();
48
+ throw new Error(`Checksum manifest is larger than ${MAX_MANIFEST_BYTES} bytes`);
49
+ }
50
+ chunks.push(chunk);
51
+ }
52
+ return Buffer.concat(chunks).toString("utf8");
53
+ }
54
+
55
+ export async function downloadFile(url, destination) {
56
+ const response = await open(url);
57
+ const hash = createHash("sha256");
58
+ response.on("data", (chunk) => hash.update(chunk));
59
+ await pipeline(response, createWriteStream(destination, { flags: "wx", mode: 0o600 }));
60
+ return hash.digest("hex");
61
+ }
package/lib/paths.js ADDED
@@ -0,0 +1,10 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { dirname, join } from "node:path";
3
+ import { PACKAGE_VERSION } from "./release.js";
4
+
5
+ export const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
6
+
7
+ export function nativeBinaryPath({ platform = process.platform, version = PACKAGE_VERSION } = {}) {
8
+ const extension = platform === "win32" ? ".exe" : "";
9
+ return join(PACKAGE_ROOT, "native", version, `dynomate-cli${extension}`);
10
+ }
package/lib/release.js ADDED
@@ -0,0 +1,65 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
4
+
5
+ export const PACKAGE_VERSION = packageJson.version;
6
+ export const RELEASE_TAG = `dynomate-cli-v${PACKAGE_VERSION}`;
7
+ export const DEFAULT_RELEASE_BASE_URL = "https://releases.dynomate.io/cli";
8
+
9
+ const TARGETS = new Map([
10
+ ["darwin-arm64", { triple: "aarch64-apple-darwin", format: "tar.gz", executable: "dynomate-cli" }],
11
+ ["darwin-x64", { triple: "x86_64-apple-darwin", format: "tar.gz", executable: "dynomate-cli" }],
12
+ ["linux-x64", { triple: "x86_64-unknown-linux-gnu", format: "tar.gz", executable: "dynomate-cli" }],
13
+ ["win32-x64", { triple: "x86_64-pc-windows-msvc", format: "zip", executable: "dynomate-cli.exe" }],
14
+ ]);
15
+
16
+ export function selectTarget(platform = process.platform, arch = process.arch) {
17
+ const target = TARGETS.get(`${platform}-${arch}`);
18
+ if (!target) {
19
+ throw new Error(
20
+ `Unsupported platform: ${platform}/${arch}. Supported targets are macOS arm64/x64, Linux x64, and Windows x64.`,
21
+ );
22
+ }
23
+ return { ...target, platform, arch };
24
+ }
25
+
26
+ export function releaseDescriptor({
27
+ platform = process.platform,
28
+ arch = process.arch,
29
+ baseUrl = process.env.DYNOMATE_CLI_RELEASE_BASE_URL || DEFAULT_RELEASE_BASE_URL,
30
+ version = PACKAGE_VERSION,
31
+ } = {}) {
32
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
33
+ throw new Error(`Invalid package version: ${version}`);
34
+ }
35
+ const target = selectTarget(platform, arch);
36
+ const tag = `dynomate-cli-v${version}`;
37
+ const archiveName = `${tag}-${target.triple}.${target.format}`;
38
+ const releaseUrl = `${baseUrl.replace(/\/$/, "")}/${tag}`;
39
+ return {
40
+ ...target,
41
+ version,
42
+ tag,
43
+ archiveName,
44
+ archiveUrl: `${releaseUrl}/${archiveName}`,
45
+ checksumUrl: `${releaseUrl}/SHA256SUMS`,
46
+ };
47
+ }
48
+
49
+ export function parseChecksumManifest(text, archiveName) {
50
+ const matches = [];
51
+ for (const line of text.split(/\r?\n/)) {
52
+ if (!line.trim()) continue;
53
+ const match = /^([a-fA-F0-9]{64})\s+\*?(.+?)\s*$/.exec(line);
54
+ if (!match) throw new Error("Invalid SHA256SUMS manifest");
55
+ if (match[2] === archiveName) matches.push(match[1].toLowerCase());
56
+ }
57
+ if (matches.length !== 1) {
58
+ throw new Error(
59
+ matches.length === 0
60
+ ? `SHA256SUMS does not contain ${archiveName}`
61
+ : `SHA256SUMS contains duplicate entries for ${archiveName}`,
62
+ );
63
+ }
64
+ return matches[0];
65
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@dynomate/cli",
3
+ "version": "0.0.10",
4
+ "description": "Installer and launcher for the native Dynomate CLI",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Bagio-labs/hammer.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "homepage": "https://dynomate.io",
13
+ "bugs": {
14
+ "url": "https://github.com/Bagio-labs/hammer/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "tag": "next"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "bin": {
24
+ "dynomate-cli": "bin/dynomate-cli.js"
25
+ },
26
+ "files": [
27
+ "bin/",
28
+ "lib/",
29
+ "scripts/",
30
+ "LICENSE",
31
+ "README.md"
32
+ ],
33
+ "scripts": {
34
+ "postinstall": "node scripts/install.js",
35
+ "test": "node --test",
36
+ "pack:check": "npm pack --dry-run"
37
+ }
38
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { installNative } from "../lib/install.js";
4
+
5
+ try {
6
+ const { descriptor } = await installNative();
7
+ console.log(`Installed dynomate-cli ${descriptor.version} for ${descriptor.triple}`);
8
+ } catch (error) {
9
+ console.error(`Failed to install dynomate-cli: ${error.message}`);
10
+ process.exitCode = 1;
11
+ }