@evgeny.skarlat/cli 0.0.22-beta.7

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/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @evgeny.skarlat/cli
2
+
3
+ RenreKit CLI — lightweight plugin-driven development CLI.
4
+
5
+ This is the **meta package**. It does not contain a binary by itself; instead
6
+ it lists per-platform packages in `optionalDependencies` and npm filters those
7
+ by `os` + `cpu` so only the matching one installs.
8
+
9
+ The `renre-kit` command on your `PATH` is a small Node dispatcher that resolves
10
+ the installed platform package and execs its native binary.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install -g @evgeny.skarlat/cli
16
+ ```
17
+
18
+ ## Supported platforms
19
+
20
+ | OS | Arch | Package |
21
+ | ------- | ----- | -------------------------------- |
22
+ | Linux | x64 | `@evgeny.skarlat/cli-linux-x64` |
23
+ | Linux | arm64 | `@evgeny.skarlat/cli-linux-arm64` |
24
+ | macOS | x64 | `@evgeny.skarlat/cli-darwin-x64` |
25
+ | macOS | arm64 | `@evgeny.skarlat/cli-darwin-arm64` |
26
+ | Windows | x64 | `@evgeny.skarlat/cli-win32-x64` |
27
+
28
+ ## Architecture
29
+
30
+ See `renre-kit-architecture/adr/distribution/`:
31
+
32
+ - ADR-001 — JFrog npm registry as sole channel
33
+ - ADR-002 — Bun `--compile` for cross-platform single-file binaries
34
+ - ADR-003 — Platform-`optionalDependencies` package layout
35
+ - ADR-004 — Embed the web dashboard inside the CLI binary
36
+ - ADR-005 — `rnrk` Python lifecycle manager relegated to contributor path
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ // Cross-platform dispatcher for the @renre-kit/cli meta package (ADR-003).
3
+ //
4
+ // Resolves the per-OS / per-CPU / per-libc platform package that npm
5
+ // filtered in via `optionalDependencies`, locates its native binary, and
6
+ // execs it with the caller's argv + environment.
7
+ //
8
+ // Resolution order (first hit wins):
9
+ // 1. $RENRE_KIT_BIN_PATH — explicit override, useful for local development
10
+ // against a freshly built binary without reinstalling.
11
+ // 2. On linux: -musl variant if running on musl libc (Alpine etc.), then
12
+ // the glibc variant; on glibc the glibc variant first, then musl as a
13
+ // defensive fallback. (Older npm without `libc`-filtering may install
14
+ // both, so the dispatcher decides at runtime.)
15
+ // 3. On macOS / Windows: the single `<os>-<arch>` variant.
16
+ import { createRequire } from 'node:module';
17
+ import { spawnSync } from 'node:child_process';
18
+ import { platform as osPlatform, arch as osArch } from 'node:os';
19
+ import { existsSync } from 'node:fs';
20
+ import { dirname, join } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const require = createRequire(import.meta.url);
24
+
25
+ function platformKey() {
26
+ const p = osPlatform();
27
+ const a = osArch();
28
+ if (p === 'win32') return ['win32', a];
29
+ if (p === 'darwin') return ['darwin', a];
30
+ if (p === 'linux') return ['linux', a];
31
+ return [p, a];
32
+ }
33
+
34
+ /**
35
+ * Detect whether the running Linux host uses musl libc (Alpine and similar
36
+ * minimal distros) rather than glibc. Two heuristics:
37
+ * 1. `/etc/alpine-release` exists -> musl
38
+ * 2. `ldd --version` output contains "musl"
39
+ * Returns false on any other platform.
40
+ */
41
+ function isMuslLinux() {
42
+ if (osPlatform() !== 'linux') return false;
43
+ try {
44
+ if (existsSync('/etc/alpine-release')) return true;
45
+ } catch {
46
+ /* permission denied or similar — fall through */
47
+ }
48
+ try {
49
+ const res = spawnSync('ldd', ['--version'], { encoding: 'utf8', timeout: 1500 });
50
+ const text = (res.stdout || '') + (res.stderr || '');
51
+ if (text.toLowerCase().includes('musl')) return true;
52
+ } catch {
53
+ /* ldd missing — assume glibc */
54
+ }
55
+ return false;
56
+ }
57
+
58
+ function candidatePackageNames(os, arch) {
59
+ if (os === 'linux') {
60
+ const glibc = `@evgeny.skarlat/cli-linux-${arch}`;
61
+ const musl = `@evgeny.skarlat/cli-linux-${arch}-musl`;
62
+ return isMuslLinux() ? [musl, glibc] : [glibc, musl];
63
+ }
64
+ return [`@evgeny.skarlat/cli-${os}-${arch}`];
65
+ }
66
+
67
+ function resolvePlatformPackageDir(pkgName) {
68
+ try {
69
+ const pkgJsonPath = require.resolve(`${pkgName}/package.json`);
70
+ return dirname(pkgJsonPath);
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ function findBinary(os, arch) {
77
+ const binaryName = os === 'win32' ? 'renre-kit.exe' : 'renre-kit';
78
+ const candidates = candidatePackageNames(os, arch);
79
+ for (const pkg of candidates) {
80
+ const pkgDir = resolvePlatformPackageDir(pkg);
81
+ if (!pkgDir) continue;
82
+ const binaryPath = join(pkgDir, 'bin', binaryName);
83
+ if (existsSync(binaryPath)) {
84
+ return { binaryPath, pkg };
85
+ }
86
+ }
87
+ return { binaryPath: null, pkg: candidates[0] };
88
+ }
89
+
90
+ /**
91
+ * Resolve the on-disk path to `@github/copilot/index.js`, which the bundled
92
+ * `@github/copilot-sdk` inside the Bun binary needs to spawn the Copilot CLI.
93
+ *
94
+ * `import.meta.resolve` from inside a Bun-compiled binary (virtual fs at
95
+ * `/$bunfs/root/`) cannot see `node_modules/@github/copilot/`, so the SDK
96
+ * falls back to the `COPILOT_CLI_PATH` env var. We compute it here from the
97
+ * Node-side dispatcher, which has normal filesystem access.
98
+ *
99
+ * We use `require.resolve.paths()` to enumerate candidate node_modules dirs
100
+ * rather than `require.resolve('@github/copilot/package.json')` — the latter
101
+ * fails because `@github/copilot` ships an `exports` field that doesn't
102
+ * expose `./package.json`. This mirrors the SDK's own fallback (see
103
+ * `@github/copilot-sdk/dist/client.js:getBundledCliPath`).
104
+ *
105
+ * Returns `null` when the package is not installed — copilot is optional, so
106
+ * we don't fail the launch.
107
+ */
108
+ export function resolveCopilotCliPath(req) {
109
+ try {
110
+ const paths = req.resolve.paths('@github/copilot') ?? [];
111
+ for (const base of paths) {
112
+ const candidate = join(base, '@github', 'copilot', 'index.js');
113
+ if (existsSync(candidate)) return candidate;
114
+ }
115
+ return null;
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Locate the `node_modules` directory that contains `node-pty` and return
123
+ * the dir itself (not the package dir). The Bun-compiled binary marks
124
+ * `node-pty` as `--external`, so the native `conpty.node` (Windows) and
125
+ * `pty.node` (POSIX) bindings have to be resolved from disk at runtime.
126
+ * We surface the parent `node_modules` via `NODE_PATH` so Bun's runtime
127
+ * resolver picks up the prebuilt binding shipped by the npm package.
128
+ *
129
+ * Returns `null` when node-pty isn't installed — the terminal feature
130
+ * degrades to "unavailable" rather than crashing.
131
+ */
132
+ export function resolveNodePtyNodeModules(req) {
133
+ try {
134
+ const paths = req.resolve.paths('node-pty') ?? [];
135
+ for (const base of paths) {
136
+ if (existsSync(join(base, 'node-pty', 'package.json'))) return base;
137
+ }
138
+ return null;
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Build the child process environment. Injects `COPILOT_CLI_PATH` and
146
+ * prepends to `NODE_PATH` only when the caller hasn't set them explicitly —
147
+ * an explicit value from the user's shell or CI always wins.
148
+ */
149
+ export function buildChildEnv(parentEnv, { copilotCliPath, nodePtyNodeModules } = {}) {
150
+ const env = { ...parentEnv };
151
+ if (copilotCliPath && !env.COPILOT_CLI_PATH) {
152
+ env.COPILOT_CLI_PATH = copilotCliPath;
153
+ }
154
+ if (nodePtyNodeModules) {
155
+ const sep = process.platform === 'win32' ? ';' : ':';
156
+ env.NODE_PATH = env.NODE_PATH
157
+ ? `${nodePtyNodeModules}${sep}${env.NODE_PATH}`
158
+ : nodePtyNodeModules;
159
+ }
160
+ return env;
161
+ }
162
+
163
+ function execAndExit(binaryPath) {
164
+ const childEnv = buildChildEnv(process.env, {
165
+ copilotCliPath: resolveCopilotCliPath(require),
166
+ nodePtyNodeModules: resolveNodePtyNodeModules(require),
167
+ });
168
+ const result = spawnSync(binaryPath, process.argv.slice(2), {
169
+ stdio: 'inherit',
170
+ env: childEnv,
171
+ windowsHide: false,
172
+ });
173
+ if (result.error) {
174
+ process.stderr.write(`renre-kit: failed to launch native binary: ${result.error.message}\n`);
175
+ process.exit(1);
176
+ }
177
+ process.exit(result.status ?? 0);
178
+ }
179
+
180
+ function main() {
181
+ const [os, arch] = platformKey();
182
+
183
+ // 1. Explicit override — bypasses package resolution entirely. Useful when
184
+ // pointing at a locally-built binary during development.
185
+ const envOverride = process.env.RENRE_KIT_BIN_PATH;
186
+ if (envOverride) {
187
+ if (!existsSync(envOverride)) {
188
+ process.stderr.write(
189
+ `\nrenre-kit: RENRE_KIT_BIN_PATH points to a file that does not exist:\n ${envOverride}\n\n`,
190
+ );
191
+ process.exit(1);
192
+ }
193
+ execAndExit(envOverride);
194
+ return;
195
+ }
196
+
197
+ // 2. Resolve via optionalDependencies candidates.
198
+ const { binaryPath, pkg } = findBinary(os, arch);
199
+ if (!binaryPath) {
200
+ const candidates = candidatePackageNames(os, arch);
201
+ process.stderr.write(
202
+ `\nrenre-kit: no native binary installed for ${os}/${arch}` +
203
+ (os === 'linux' ? ` (${isMuslLinux() ? 'musl' : 'glibc'})` : '') +
204
+ `.\n` +
205
+ `Looked for: ${candidates.join(', ')}\n` +
206
+ `Reinstall with: npm install -g @evgeny.skarlat/cli\n` +
207
+ `Or set RENRE_KIT_BIN_PATH to a local binary you trust.\n\n`,
208
+ );
209
+ process.exit(1);
210
+ }
211
+
212
+ if (process.env.RENRE_KIT_DEBUG_DISPATCH) {
213
+ process.stderr.write(`[renre-kit-dispatch] launching ${pkg} -> ${binaryPath}\n`);
214
+ }
215
+
216
+ execAndExit(binaryPath);
217
+ }
218
+
219
+ // Only auto-run when invoked as a script. Tests import this module to exercise
220
+ // the helper functions without triggering the dispatch + process.exit chain.
221
+ const invokedAsScript = process.argv[1] === fileURLToPath(import.meta.url);
222
+ if (invokedAsScript) main();
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@evgeny.skarlat/cli",
3
+ "version": "0.0.22-beta.7",
4
+ "description": "RenreKit CLI — lightweight plugin-driven development CLI",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "renre-kit": "bin/renre-kit.js"
9
+ },
10
+ "files": [
11
+ "bin/renre-kit.js",
12
+ "scripts/postinstall.mjs",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "postinstall": "node scripts/postinstall.mjs"
17
+ },
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "dependencies": {
22
+ "@github/copilot": "^1.0.36"
23
+ },
24
+ "optionalDependencies": {
25
+ "node-pty": "^1.1.0",
26
+ "@evgeny.skarlat/cli-darwin-arm64": "0.0.22-beta.7",
27
+ "@evgeny.skarlat/cli-darwin-x64": "0.0.22-beta.7",
28
+ "@evgeny.skarlat/cli-linux-arm64": "0.0.22-beta.7",
29
+ "@evgeny.skarlat/cli-linux-arm64-musl": "0.0.22-beta.7",
30
+ "@evgeny.skarlat/cli-linux-x64": "0.0.22-beta.7",
31
+ "@evgeny.skarlat/cli-linux-x64-musl": "0.0.22-beta.7",
32
+ "@evgeny.skarlat/cli-win32-x64": "0.0.22-beta.7"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Postinstall verification for @renre-kit/cli.
4
+ *
5
+ * Confirms that npm picked up the right per-platform package from
6
+ * `optionalDependencies`. If it didn't, prints a clear actionable message
7
+ * and exits 0 anyway — failing the install would leave the user worse off
8
+ * (they couldn't even rerun `npm install` against a fixed registry without
9
+ * uninstalling the meta first).
10
+ *
11
+ * Detection logic mirrors bin/renre-kit.js so the same candidates are
12
+ * tried.
13
+ */
14
+ import { createRequire } from 'node:module';
15
+ import { spawnSync } from 'node:child_process';
16
+ import { platform as osPlatform, arch as osArch } from 'node:os';
17
+ import { existsSync } from 'node:fs';
18
+
19
+ const require = createRequire(import.meta.url);
20
+
21
+ function isMuslLinux() {
22
+ if (osPlatform() !== 'linux') return false;
23
+ try {
24
+ if (existsSync('/etc/alpine-release')) return true;
25
+ } catch {
26
+ /* fall through */
27
+ }
28
+ try {
29
+ const res = spawnSync('ldd', ['--version'], { encoding: 'utf8', timeout: 1500 });
30
+ const text = (res.stdout || '') + (res.stderr || '');
31
+ if (text.toLowerCase().includes('musl')) return true;
32
+ } catch {
33
+ /* ldd missing */
34
+ }
35
+ return false;
36
+ }
37
+
38
+ function expectedCandidates() {
39
+ const p = osPlatform();
40
+ const a = osArch();
41
+ if (p === 'linux') {
42
+ const glibc = `@evgeny.skarlat/cli-linux-${a}`;
43
+ const musl = `@evgeny.skarlat/cli-linux-${a}-musl`;
44
+ return isMuslLinux() ? [musl, glibc] : [glibc, musl];
45
+ }
46
+ if (p === 'darwin') return [`@evgeny.skarlat/cli-darwin-${a}`];
47
+ if (p === 'win32') return [`@evgeny.skarlat/cli-win32-${a}`];
48
+ return [`@evgeny.skarlat/cli-${p}-${a}`];
49
+ }
50
+
51
+ function canResolve(pkg) {
52
+ try {
53
+ require.resolve(`${pkg}/package.json`);
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ function main() {
61
+ if (process.env.RENRE_KIT_SKIP_POSTINSTALL_CHECK) return;
62
+
63
+ const candidates = expectedCandidates();
64
+ const resolved = candidates.find(canResolve);
65
+ if (resolved) {
66
+ if (process.env.RENRE_KIT_DEBUG_DISPATCH) {
67
+ process.stderr.write(`[renre-kit-postinstall] resolved ${resolved}\n`);
68
+ }
69
+ return;
70
+ }
71
+
72
+ const p = osPlatform();
73
+ const a = osArch();
74
+ const libc = p === 'linux' ? (isMuslLinux() ? 'musl' : 'glibc') : null;
75
+ process.stderr.write(
76
+ [
77
+ '',
78
+ `renre-kit: WARNING — no platform binary package was installed.`,
79
+ ` Detected host: ${p}/${a}${libc ? ` (${libc})` : ''}`,
80
+ ` Looked for: ${candidates.join(', ')}`,
81
+ '',
82
+ ` This usually means npm filtered out the optionalDependency for an`,
83
+ ` unsupported os/cpu/libc combination, or the registry mirror did not`,
84
+ ` carry the platform package.`,
85
+ '',
86
+ ` Fix by reinstalling once the right package is available:`,
87
+ ` npm install -g @evgeny.skarlat/cli`,
88
+ ` Or set RENRE_KIT_BIN_PATH to a binary you trust to bypass resolution.`,
89
+ '',
90
+ ].join('\n'),
91
+ );
92
+ }
93
+
94
+ try {
95
+ main();
96
+ } catch (err) {
97
+ // Never fail the install on a verification hiccup.
98
+ process.stderr.write(
99
+ `renre-kit: postinstall verification error (non-fatal): ${err?.message ?? err}\n`,
100
+ );
101
+ }