@riducms/cli 0.1.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 +42 -0
- package/package.json +39 -0
- package/src/cli.js +10 -0
- package/src/launcher.js +147 -0
- package/src/run.js +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Haniel Ubogu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# `@riducms/cli`
|
|
2
|
+
|
|
3
|
+
This package runs the native Go CLI for the matching Ridu release:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx @riducms/cli new my-content
|
|
7
|
+
bunx @riducms/cli new my-content
|
|
8
|
+
pnpm dlx @riducms/cli new my-content
|
|
9
|
+
yarn dlx @riducms/cli new my-content
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
For the conventional project-creation entry point, use `npm create ridu@latest my-app`. The
|
|
13
|
+
`create-ridu` initializer delegates to this package with the `new` command; this package remains
|
|
14
|
+
the launcher for `ridu version`, `dev`, `generate`, migrations, and every other CLI command.
|
|
15
|
+
|
|
16
|
+
The launcher downloads and caches the matching native command for macOS, Linux, or Windows. It
|
|
17
|
+
forwards arguments, signals, and exit status to that command.
|
|
18
|
+
|
|
19
|
+
New scaffolds pin this package at the exact coordinated version. From a generated project, use:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install
|
|
23
|
+
npm run dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use the non-writing form separately in CI to verify committed contracts:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm run ridu -- generate --check
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The scaffold records npm, Bun, pnpm, or Yarn in `ridu.toml`, and the native CLI uses it for frontend
|
|
33
|
+
installs and admin/plugin commands. The package-local command avoids global version drift while
|
|
34
|
+
still using the ordinary npm registry.
|
|
35
|
+
|
|
36
|
+
Go users can install the same command directly:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
go install github.com/riducms/ridu/cmd/ridu@v0.1.0
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Set `RIDU_BINARY` to an already-installed binary path when downloads are managed centrally.
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bin": {
|
|
3
|
+
"ridu": "./src/cli.js"
|
|
4
|
+
},
|
|
5
|
+
"bugs": {
|
|
6
|
+
"url": "https://github.com/riducms/ridu/issues"
|
|
7
|
+
},
|
|
8
|
+
"description": "Cross-platform npm launcher for Ridu's native Go CLI.",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
"./run": "./src/run.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src/cli.js",
|
|
17
|
+
"src/launcher.js",
|
|
18
|
+
"src/run.js",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://riducms.com",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"name": "@riducms/cli",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"provenance": true
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"directory": "packages/cli",
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/riducms/ridu.git"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"check": "bun test",
|
|
35
|
+
"test": "bun test"
|
|
36
|
+
},
|
|
37
|
+
"type": "module",
|
|
38
|
+
"version": "0.1.0"
|
|
39
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runRidu } from "./run.js";
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
process.exitCode = await runRidu(process.argv.slice(2));
|
|
7
|
+
} catch (error) {
|
|
8
|
+
console.error(`ridu: ${error instanceof Error ? error.message : String(error)}`);
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
}
|
package/src/launcher.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
|
+
import { gunzipSync } from "node:zlib";
|
|
7
|
+
|
|
8
|
+
const repository = "https://github.com/riducms/ridu";
|
|
9
|
+
|
|
10
|
+
export function releaseTarget(platform = process.platform, architecture = process.arch) {
|
|
11
|
+
const operatingSystems = { darwin: "darwin", linux: "linux", win32: "windows" };
|
|
12
|
+
const architectures = { arm64: "arm64", x64: "amd64" };
|
|
13
|
+
const goos = operatingSystems[platform];
|
|
14
|
+
const goarch = architectures[architecture];
|
|
15
|
+
if (goos === undefined || goarch === undefined) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Ridu has no prebuilt CLI for ${platform}/${architecture}; install it with Go instead`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return { goarch, goos, windows: goos === "windows" };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function archiveName(version, target) {
|
|
24
|
+
const extension = target.windows ? "zip" : "tar.gz";
|
|
25
|
+
return `ridu_${version}_${target.goos}_${target.goarch}.${extension}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function expectedChecksum(checksums, filename) {
|
|
29
|
+
for (const line of checksums.split(/\r?\n/u)) {
|
|
30
|
+
const match = line.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/u);
|
|
31
|
+
if (match !== null && basename(match[2]) === filename) {
|
|
32
|
+
return match[1].toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Ridu release checksums do not contain ${filename}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function verifyChecksum(bytes, expected, filename) {
|
|
39
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
40
|
+
if (actual !== expected) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Ridu CLI checksum mismatch for ${filename}: expected ${expected}, got ${actual}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function extractTarGzipBinary(archive, binaryName = "ridu") {
|
|
48
|
+
const tar = gunzipSync(archive);
|
|
49
|
+
for (let offset = 0; offset + 512 <= tar.length;) {
|
|
50
|
+
const header = tar.subarray(offset, offset + 512);
|
|
51
|
+
if (header.every((byte) => byte === 0)) break;
|
|
52
|
+
const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/u, "");
|
|
53
|
+
const encodedSize = header.subarray(124, 136).toString("ascii").replace(/\0.*$/u, "").trim();
|
|
54
|
+
const size = Number.parseInt(encodedSize || "0", 8);
|
|
55
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
56
|
+
throw new Error(`invalid tar member size for ${name}`);
|
|
57
|
+
}
|
|
58
|
+
const contentOffset = offset + 512;
|
|
59
|
+
if (basename(name) === binaryName) {
|
|
60
|
+
return tar.subarray(contentOffset, contentOffset + size);
|
|
61
|
+
}
|
|
62
|
+
offset = contentOffset + Math.ceil(size / 512) * 512;
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`Ridu release archive does not contain ${binaryName}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function cacheRoot() {
|
|
68
|
+
if (process.env.RIDU_CLI_CACHE_DIR) return process.env.RIDU_CLI_CACHE_DIR;
|
|
69
|
+
if (process.platform === "win32" && process.env.LOCALAPPDATA) {
|
|
70
|
+
return join(process.env.LOCALAPPDATA, "ridu", "cli");
|
|
71
|
+
}
|
|
72
|
+
if (process.env.XDG_CACHE_HOME) return join(process.env.XDG_CACHE_HOME, "ridu", "cli");
|
|
73
|
+
return join(homedir(), ".cache", "ridu", "cli");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function download(url) {
|
|
77
|
+
const response = await fetch(url, {
|
|
78
|
+
headers: { "user-agent": "@riducms/cli" },
|
|
79
|
+
redirect: "follow",
|
|
80
|
+
});
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new Error(`download ${url}: ${response.status} ${response.statusText}`);
|
|
83
|
+
}
|
|
84
|
+
return Buffer.from(await response.arrayBuffer());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function installWindowsArchive(archive, destination) {
|
|
88
|
+
const stage = join(tmpdir(), `ridu-cli-${process.pid}-${Date.now()}`);
|
|
89
|
+
const archivePath = `${stage}.zip`;
|
|
90
|
+
mkdirSync(stage, { recursive: true });
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(archivePath, archive, { mode: 0o600 });
|
|
93
|
+
const result = spawnSync(
|
|
94
|
+
"powershell.exe",
|
|
95
|
+
[
|
|
96
|
+
"-NoProfile",
|
|
97
|
+
"-NonInteractive",
|
|
98
|
+
"-Command",
|
|
99
|
+
`Expand-Archive -LiteralPath '${archivePath.replaceAll("'", "''")}' -DestinationPath '${stage.replaceAll("'", "''")}' -Force`,
|
|
100
|
+
],
|
|
101
|
+
{ encoding: "utf8" }
|
|
102
|
+
);
|
|
103
|
+
if (result.status !== 0) {
|
|
104
|
+
const detail = result.error?.message ?? String(result.stderr ?? "").trim();
|
|
105
|
+
throw new Error(`extract Ridu CLI archive: ${detail || "PowerShell exited unsuccessfully"}`);
|
|
106
|
+
}
|
|
107
|
+
writeFileSync(destination, readFileSync(join(stage, "ridu.exe")), { mode: 0o755 });
|
|
108
|
+
} finally {
|
|
109
|
+
rmSync(stage, { force: true, recursive: true });
|
|
110
|
+
rmSync(archivePath, { force: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function ensureBinary(version) {
|
|
115
|
+
if (process.env.RIDU_BINARY) return process.env.RIDU_BINARY;
|
|
116
|
+
const target = releaseTarget();
|
|
117
|
+
const binaryName = target.windows ? "ridu.exe" : "ridu";
|
|
118
|
+
const destination = join(cacheRoot(), version, `${target.goos}-${target.goarch}`, binaryName);
|
|
119
|
+
if (existsSync(destination)) return destination;
|
|
120
|
+
|
|
121
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
122
|
+
const filename = archiveName(version, target);
|
|
123
|
+
const releaseBase =
|
|
124
|
+
process.env.RIDU_CLI_RELEASE_BASE_URL ?? `${repository}/releases/download/v${version}`;
|
|
125
|
+
const [archive, checksums] = await Promise.all([
|
|
126
|
+
download(`${releaseBase}/${filename}`),
|
|
127
|
+
download(`${releaseBase}/SHA256SUMS`),
|
|
128
|
+
]);
|
|
129
|
+
verifyChecksum(archive, expectedChecksum(checksums.toString("utf8"), filename), filename);
|
|
130
|
+
|
|
131
|
+
const temporary = `${destination}.${process.pid}.tmp`;
|
|
132
|
+
try {
|
|
133
|
+
if (target.windows) {
|
|
134
|
+
installWindowsArchive(archive, temporary);
|
|
135
|
+
} else {
|
|
136
|
+
writeFileSync(temporary, extractTarGzipBinary(archive), { mode: 0o755 });
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
renameSync(temporary, destination);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (!existsSync(destination)) throw error;
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
rmSync(temporary, { force: true });
|
|
145
|
+
}
|
|
146
|
+
return destination;
|
|
147
|
+
}
|
package/src/run.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { ensureBinary } from "./launcher.js";
|
|
7
|
+
|
|
8
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const { version } = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
|
|
10
|
+
|
|
11
|
+
export async function runRidu(arguments_) {
|
|
12
|
+
const binary = await ensureBinary(version);
|
|
13
|
+
const child = spawn(binary, arguments_, { stdio: "inherit" });
|
|
14
|
+
const signalHandlers = new Map();
|
|
15
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
16
|
+
const handler = () => child.kill(signal);
|
|
17
|
+
signalHandlers.set(signal, handler);
|
|
18
|
+
process.once(signal, handler);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return await new Promise((resolvePromise, reject) => {
|
|
22
|
+
child.once("error", reject);
|
|
23
|
+
child.once("exit", (code) => resolvePromise(code ?? 1));
|
|
24
|
+
});
|
|
25
|
+
} finally {
|
|
26
|
+
for (const [signal, handler] of signalHandlers) process.off(signal, handler);
|
|
27
|
+
}
|
|
28
|
+
}
|