zyrk 0.0.0-stage → 0.2.3

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 King Jethro (kingjethro999)
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 CHANGED
@@ -1,3 +1,189 @@
1
- # Temporary Holding Version
1
+ # zyrk
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
+ [![CI](https://github.com/kingjethro999/nexu/actions/workflows/ci.yml/badge.svg)](https://github.com/kingjethro999/nexu/actions/workflows/ci.yml)
4
+ [![Node.js](https://img.shields.io/badge/Node.js-24%2B-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ An npm-compatible package manager built around **one verified package store per
8
+ OS user**, with dependency-aware package instances linked into projects.
9
+
10
+ **Pre-release and under active construction.** The current CLI can fetch and run
11
+ one-off tools with `dlx` and `create`. Ordinary project installation, dependency
12
+ editing, garbage collection, repair, and deployment are still being built. Check
13
+ `zyrk --help` for the implemented command surface.
14
+
15
+ ## Why zyrk exists
16
+
17
+ A developer often works on several Node.js projects with overlapping
18
+ dependencies. zyrk's intended default is to verify package sources once, keep
19
+ them in a per-user store, and assemble each project's dependency graph through
20
+ links. Different versions and peer dependency contexts remain separate.
21
+
22
+ Application code keeps using ordinary imports. There is no custom Node.js
23
+ loader, privileged daemon, or manual mount setup in the intended development
24
+ model.
25
+
26
+ Sharing files is an established approach: pnpm also uses hard links. zyrk makes
27
+ the shared store, exact dependency context, mutation rules, and cleanup contract
28
+ central to its design. Storage savings, speed, and framework compatibility will
29
+ be reported only after reproducible measurements and compatibility tests.
30
+
31
+ ## Try the current CLI
32
+
33
+ Once the `zyrk` package is published, install it with `npm install -g zyrk`,
34
+ then run `zyrk --help`.
35
+
36
+ Requires **Node.js 24 or newer**. The source runs directly with Node's native
37
+ TypeScript support; no compilation step or native toolchain is required to
38
+ install zyrk itself.
39
+
40
+ From a checkout:
41
+
42
+ ```bash
43
+ git clone https://github.com/kingjethro999/nexu.git zyrk
44
+ cd zyrk
45
+ npm ci
46
+ node bin/zyrk.js --help
47
+ node bin/zyrk.js --version
48
+ ```
49
+
50
+ For an npm-distributed build, use the exact package name listed on its npm page
51
+ and check that its repository points to `kingjethro999/nexu`. The executable is
52
+ `zyrk`. The existing [npm publishing action](.github/workflows/npm-publish.yml)
53
+ uses the repository’s `NPM_TOKEN` secret when a GitHub release is created.
54
+
55
+ ### Run a one-off tool
56
+
57
+ ```bash
58
+ zyrk dlx <package>[@version] [args...]
59
+ zyrk create <initializer>[@version] [args...]
60
+ ```
61
+
62
+ When running from source, replace `zyrk` with `node bin/zyrk.js`.
63
+
64
+ `dlx` resolves the requested package and its dependencies, verifies downloaded
65
+ archives, creates a dependency-aware tool graph, and launches the selected
66
+ JavaScript bin entry with the supplied arguments. Its temporary projection is
67
+ removed afterward; verified sources and shared realizations remain in the
68
+ store.
69
+
70
+ `create` uses the same execution path and maps initializer names to their
71
+ `create-*` packages. For example, `vite` maps to `create-vite`, while a scoped
72
+ initializer stays in its scope. Compatibility depends on the selected tool:
73
+ closures declaring install lifecycle scripts are currently blocked, and native
74
+ or shell executable bins are not supported by the JavaScript-only launcher.
75
+
76
+ These commands run the tool's code with your account's permissions. The tool
77
+ working directory is your current directory. A temporary tool projection is
78
+ not a sandbox.
79
+
80
+ ### Current command coverage
81
+
82
+ | Surface | Current behavior |
83
+ | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
84
+ | `--help`, `--version` | Show the CLI surface and package version. |
85
+ | `dlx`, `create` | Resolve, fetch, verify archives, and run temporary tool environments. |
86
+ | `publish`, `unpublish`, `access`, `deprecate`, `dist-tag`, `owner` | Explicitly delegate to your npm CLI. |
87
+ | `token`, `adduser`, `login`, `logout`, `whoami`, `ping`, `pack` | Explicitly delegate to your npm CLI. |
88
+ | `install`, `add`, `remove`, `update`, `run`, `exec` | Report `ZYRK_NOT_IMPLEMENTED`. |
89
+ | `store`, `projects`, `repair`, `import`, `deploy`, `docker` | Report `ZYRK_NOT_IMPLEMENTED`. |
90
+ | `list`, `why`, `doctor`, `approve-builds`, `rebuild`, `audit`, `patch`, `patch-commit` | Report `ZYRK_NOT_IMPLEMENTED`. |
91
+
92
+ Native commands that support `--json` return a versioned result envelope.
93
+ `dlx` and `create` reject that option because the tool owns stdout. Delegated
94
+ commands preserve npm's own output format.
95
+
96
+ ## Storage, isolation, and trust
97
+
98
+ On Linux, the default store is:
99
+
100
+ ```text
101
+ ${XDG_DATA_HOME:-$HOME/.local/share}/zyrk/store/v1
102
+ ```
103
+
104
+ Set `ZYRK_STORE_DIR` deliberately to choose another store location. The store
105
+ must satisfy ownership and permission checks. Linux local filesystems are the
106
+ initial target; platform and framework support remain gated on their own tests.
107
+
108
+ The design separates three things:
109
+
110
+ - **Verified source:** archive integrity is checked before extraction and source
111
+ publication. Extraction rejects unsafe paths and links and normalizes modes.
112
+ - **Dependency context:** a package instance records its selected dependency
113
+ wiring, so incompatible versions and peer contexts are not merged.
114
+ - **Mutable work:** the planned build/runtime mutation model uses private
115
+ project generations. Writable build outputs must not change shared source.
116
+
117
+ Read-only modes help prevent accidental writes; they are not a security boundary
118
+ against code running as the same OS user. Shared stores should be used only by
119
+ mutually trusted processes. Do not share them across unrelated CI trust domains.
120
+
121
+ Native tool commands currently use the public npm registry. Private registry
122
+ configuration and credential support are not implemented; do not use these
123
+ commands for internal tools that depend on private registry routing.
124
+
125
+ ## Global installs and npm delegation
126
+
127
+ zyrk never installs packages globally and never forwards a global-install
128
+ request to npm:
129
+
130
+ ```bash
131
+ zyrk install --global <package>
132
+ # ZYRK_GLOBAL_INSTALL_UNSUPPORTED
133
+ ```
134
+
135
+ Use npm directly when you want a global package. Installing the zyrk executable
136
+ with npm is separate from asking zyrk to install another package globally.
137
+
138
+ Registry write and account operations use your npm installation. zyrk prints a
139
+ disclosure, inherits stdio, and returns the child status. npm uses its own
140
+ registry configuration and authentication:
141
+
142
+ ```bash
143
+ zyrk publish --tag beta
144
+ zyrk pack --ignore-scripts
145
+ ```
146
+
147
+ npm options may come before or after the command: zyrk forwards every
148
+ non-zyrk token verbatim, so protective flags such as `--dry-run` always
149
+ reach npm. A bare value before the command that cannot be the command
150
+ itself fails before npm launches — use the `--flag=value` form or place
151
+ the command first. npm may run package lifecycle scripts according to its
152
+ own options and configuration.
153
+
154
+ ## Develop and contribute
155
+
156
+ ```bash
157
+ npm ci
158
+ npm run setup:hooks
159
+ npm run typecheck
160
+ npm test
161
+ npm run check
162
+ npm run format:check
163
+ npm run format
164
+ ```
165
+
166
+ The commit hook runs the full check gate, increments the version in
167
+ `package.json`, `package-lock.json`, and `VERSION`, and creates a matching version
168
+ tag after the commit. Keep changes focused and never bypass the hooks.
169
+
170
+ CI checks pushed commits and pull requests. The separate
171
+ [npm publishing action](.github/workflows/npm-publish.yml) is triggered when a
172
+ GitHub release is created; ordinary commits do not trigger that action.
173
+
174
+ The contributor guide is [AGENTS.md](AGENTS.md). Bug reports and reproducible
175
+ compatibility cases belong in
176
+ [GitHub issues](https://github.com/kingjethro999/nexu/issues).
177
+
178
+ ## Naming and local state
179
+
180
+ The project was renamed from snpm to **zyrk**. The shared-store philosophy,
181
+ version isolation, mutation policy, and security requirements remain the same.
182
+ Current CLI/configuration uses `zyrk` / `ZYRK`, and lock metadata uses the `zyrk`
183
+ namespace. Pre-rename stores and lock metadata are not migrated automatically.
184
+ Keep existing data until an explicit migration is supported.
185
+
186
+ ## License
187
+
188
+ [MIT](LICENSE). If you want to support development,
189
+ [sponsor the maintainer](https://github.com/sponsors/kingjethro999).
package/VERSION ADDED
@@ -0,0 +1 @@
1
+ v0.2.3
package/bin/zyrk.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "../src/cli/main.ts";
3
+
4
+ process.exitCode = await runCli({
5
+ argv: process.argv.slice(2),
6
+ env: process.env,
7
+ stdout: process.stdout,
8
+ stderr: process.stderr,
9
+ });
package/package.json CHANGED
@@ -1,6 +1,66 @@
1
1
  {
2
2
  "name": "zyrk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.3",
4
+ "description": "Storage-efficient Node package manager: one verified store per user, shared across projects through links.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=24.0.0"
9
+ },
10
+ "bin": {
11
+ "zyrk": "bin/zyrk.js"
12
+ },
13
+ "scripts": {
14
+ "typecheck": "tsc -p tsconfig.json",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
17
+ "check": "node scripts/check.mjs",
18
+ "format:check": "prettier --check .",
19
+ "format": "prettier --write .",
20
+ "version:bump": "node scripts/bump-version.js",
21
+ "setup:hooks": "git config core.hooksPath .githooks"
22
+ },
23
+ "dependencies": {
24
+ "npm-package-arg": "^14.0.0",
25
+ "npm-registry-fetch": "^20.0.1",
26
+ "semver": "^7.8.5",
27
+ "ssri": "^14.0.0",
28
+ "tar": "^7.5.22"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^24.19.2",
32
+ "@types/semver": "^7.8.0",
33
+ "prettier": "3.9.10",
34
+ "typescript": "^7.0.2",
35
+ "vitest": "^5.0.5"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/kingjethro999/nexu.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/kingjethro999/nexu/issues"
43
+ },
44
+ "homepage": "https://github.com/kingjethro999/nexu#readme",
45
+ "keywords": [
46
+ "package-manager",
47
+ "npm",
48
+ "nodejs",
49
+ "typescript",
50
+ "dependency-management",
51
+ "shared-store",
52
+ "content-addressable-storage",
53
+ "cli"
54
+ ],
55
+ "files": [
56
+ "bin/",
57
+ "src/",
58
+ "VERSION",
59
+ "LICENSE",
60
+ "README.md"
61
+ ],
62
+ "publishConfig": {
63
+ "access": "public",
64
+ "registry": "https://registry.npmjs.org/"
65
+ }
66
+ }
@@ -0,0 +1,383 @@
1
+ /**
2
+ * zyrk command dispatch (SPEC 15).
3
+ *
4
+ * This is the roadmap skeleton, not the whole implementation: `dlx` and
5
+ * `create` run temporary tool environments; `--help`, `--version`, the
6
+ * global-install rejection, and explicit npm delegation do their jobs.
7
+ * Every other native dependency command fails with ZYRK_NOT_IMPLEMENTED
8
+ * rather than pretending to work; unknown commands fail with ZYRK_USAGE
9
+ * and the help text.
10
+ * Nothing here launches a subprocess: rejection happens before resolution,
11
+ * downloads, lifecycle execution, project/store mutation, or any child
12
+ * process (SPEC 15.3.1).
13
+ */
14
+
15
+ import { readFileSync } from "node:fs";
16
+ import * as path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { runCreate } from "../dlx/create.ts";
19
+ import { runDlx } from "../dlx/run.ts";
20
+ import { exitCodeFor, ZyrkError, toZyrkError } from "../errors.ts";
21
+ import { DEFAULT_REGISTRY, RegistryClient } from "../fetch/registry.ts";
22
+ import { resolveStoreRoot } from "../config/paths.ts";
23
+ import { Store } from "../store/layout.ts";
24
+ import {
25
+ delegateToNpm,
26
+ delegatedHelpLines,
27
+ isDelegatedCommand,
28
+ } from "./npm-delegate.ts";
29
+ import { isGlobalInstallRequest, parseArgs, type ParsedArgs } from "./parse.ts";
30
+
31
+ export const CLI_JSON_SCHEMA_VERSION = 1;
32
+
33
+ /** Spec-defined dependency commands (SPEC 15.1-15.2, fetch, docker). */
34
+ const NATIVE_COMMANDS: ReadonlyArray<readonly [string, string]> = [
35
+ ["install, i", "Install the locked or newly resolved graph."],
36
+ ["fetch", "Fetch and verify a locked source closure."],
37
+ ["add", "Add dependencies transactionally."],
38
+ ["remove", "Remove direct dependencies and refresh the closure."],
39
+ ["update", "Update within allowed ranges."],
40
+ ["run", "Run a project script with local binary paths."],
41
+ ["exec", "Run an installed executable (never fetches)."],
42
+ ["dlx", "Explicitly fetch and run a temporary tool environment."],
43
+ ["create", "Scaffold from a create-* starter package."],
44
+ ["list, why", "Show the installed graph / why a dependency is selected."],
45
+ ["doctor", "Diagnose filesystem, links, policy, install-state issues."],
46
+ ["approve-builds, rebuild", "Review script permissions / rebuild."],
47
+ ["audit", "Report vulnerabilities under the configured privacy policy."],
48
+ ["patch, patch-commit", "Create a private edit area / emit a patch."],
49
+ ["store", "Inspect, verify, collect, and retain the store."],
50
+ ["projects", "List registrations / drop a retention root."],
51
+ ["repair", "Reconstruct the project from verified locked inputs."],
52
+ ["import", "Convert supported npm lock information."],
53
+ ["deploy", "Create self-contained production output."],
54
+ ["docker", "Generate, build, and start the Docker setup."],
55
+ ];
56
+
57
+ const KNOWN_COMMANDS = new Set<string>();
58
+ for (const [names] of NATIVE_COMMANDS) {
59
+ for (const name of names.split(", ")) KNOWN_COMMANDS.add(name);
60
+ }
61
+
62
+ /** Commands with a real implementation behind runCli. */
63
+ const IMPLEMENTED_COMMANDS = new Set(["dlx", "create"]);
64
+
65
+ export function helpText(version: string): string {
66
+ const lines = [
67
+ `zyrk v${version} — one verified store per user, shared across projects through links.`,
68
+ "",
69
+ "Usage: zyrk <command> [args] [--json]",
70
+ "",
71
+ "Dependency commands:",
72
+ ];
73
+ for (const [names, description] of NATIVE_COMMANDS) {
74
+ const done = names
75
+ .split(", ")
76
+ .some((name) => IMPLEMENTED_COMMANDS.has(name));
77
+ lines.push(
78
+ ` ${names.padEnd(24, " ")}${description}${done ? "" : " (not implemented yet)"}`,
79
+ );
80
+ }
81
+ lines.push("", "Registry publishing and account commands (via your npm):");
82
+ lines.push(...delegatedHelpLines());
83
+ lines.push(
84
+ "",
85
+ "zyrk never installs packages globally and never runs npm to do it.",
86
+ "Need a global package? Run npm directly: npm install --global <package-spec>",
87
+ );
88
+ return `${lines.join("\n")}\n`;
89
+ }
90
+
91
+ export type CliOutput = {
92
+ exitCode: number;
93
+ stdout: string;
94
+ stderr: string;
95
+ };
96
+
97
+ function ok(stdout: string, parsed: ParsedArgs): CliOutput {
98
+ if (!parsed.json) return { exitCode: 0, stdout, stderr: "" };
99
+ return {
100
+ exitCode: 0,
101
+ stdout: `${JSON.stringify({ schemaVersion: CLI_JSON_SCHEMA_VERSION, status: "ok" })}\n`,
102
+ stderr: "",
103
+ };
104
+ }
105
+
106
+ function fail(
107
+ error: ZyrkError,
108
+ parsed: ParsedArgs,
109
+ stdoutHint = "",
110
+ ): CliOutput {
111
+ const human =
112
+ `zyrk: error ${error.code}: ${error.message}` +
113
+ (error.remedy === undefined ? "" : `\n${error.remedy}`);
114
+ if (!parsed.json) {
115
+ return {
116
+ exitCode: exitCodeFor(error),
117
+ stdout: stdoutHint,
118
+ stderr: `${human}\n`,
119
+ };
120
+ }
121
+ const body: Record<string, unknown> = {
122
+ schemaVersion: CLI_JSON_SCHEMA_VERSION,
123
+ status: "error",
124
+ code: error.code,
125
+ message: error.message,
126
+ };
127
+ if (error.remedy !== undefined) body.remedy = error.remedy;
128
+ return {
129
+ exitCode: exitCodeFor(error),
130
+ stdout: `${JSON.stringify(body)}\n`,
131
+ stderr: "",
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Echo a command token back safely: drop control characters, cap length,
137
+ * and strip URL userinfo so error output cannot leak credentials or forge
138
+ * terminal sequences. Full structured output redaction is S03; this covers
139
+ * the one place CLI dispatch interpolates an arbitrary token today.
140
+ */
141
+ /**
142
+ * Drop C0 control characters and DEL without regex escapes.
143
+ */
144
+ function stripControlChars(token: string): string {
145
+ let out = "";
146
+ for (const ch of token) {
147
+ const code = ch.codePointAt(0);
148
+ if (code !== undefined && code >= 32 && code !== 127) out += ch;
149
+ }
150
+ return out;
151
+ }
152
+
153
+ function sanitizeCommandToken(token: string): string {
154
+ const oneLine = stripControlChars(token);
155
+ const capped = oneLine.length > 64 ? `${oneLine.slice(0, 64)}…` : oneLine;
156
+ return capped.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^@/]*@)?/, "$1");
157
+ }
158
+
159
+ function globalInstallError(command: string): ZyrkError {
160
+ return new ZyrkError(
161
+ "ZYRK_GLOBAL_INSTALL_UNSUPPORTED",
162
+ `zyrk does not install packages globally (requested: zyrk ${command} with global-install mode).`,
163
+ {
164
+ detail: { command },
165
+ remedy:
166
+ "Shared storage is not global installation. Run npm directly, for example: " +
167
+ "npm install --global <package-spec>. zyrk will not run npm for you.",
168
+ },
169
+ );
170
+ }
171
+
172
+ /**
173
+ * Pure dispatch: no filesystem, network, or subprocess effects. Returns the
174
+ * exact bytes for stdout/stderr and the process exit code.
175
+ */
176
+ export function dispatch(
177
+ parsed: ParsedArgs,
178
+ context: { version: string; env: NodeJS.ProcessEnv },
179
+ ): CliOutput {
180
+ if (parsed.version) {
181
+ return ok(`${context.version}\n`, parsed);
182
+ }
183
+ if (parsed.help || parsed.command === undefined) {
184
+ return ok(helpText(context.version), parsed);
185
+ }
186
+ const command = parsed.command;
187
+ if (isGlobalInstallRequest(command, parsed.commandArgs, context.env)) {
188
+ return fail(globalInstallError(command), parsed);
189
+ }
190
+ if (!KNOWN_COMMANDS.has(command)) {
191
+ const shown = sanitizeCommandToken(command);
192
+ const placementHint =
193
+ command.includes("://") || parsed.argsBeforeCommand.length > 0
194
+ ? " If this was an option value, use the --flag=value form or place the command first."
195
+ : "";
196
+ return fail(
197
+ new ZyrkError("ZYRK_USAGE", `unknown command: ${shown}.`, {
198
+ detail: { command: shown },
199
+ remedy: `Run \`zyrk --help\` for the documented command list.${placementHint}`,
200
+ }),
201
+ parsed,
202
+ helpText(context.version),
203
+ );
204
+ }
205
+ if (!IMPLEMENTED_COMMANDS.has(command)) {
206
+ return fail(
207
+ new ZyrkError(
208
+ "ZYRK_NOT_IMPLEMENTED",
209
+ `zyrk ${command} is not implemented yet.`,
210
+ {
211
+ detail: { command },
212
+ remedy:
213
+ "Only documented, implemented commands are advertised; this one is on the roadmap.",
214
+ },
215
+ ),
216
+ parsed,
217
+ );
218
+ }
219
+ throw new ZyrkError(
220
+ "ZYRK_INTERNAL",
221
+ `implemented command ${command} bypassed runCli.`,
222
+ {
223
+ detail: { command },
224
+ remedy: "report this unreachable dispatch failure",
225
+ },
226
+ );
227
+ }
228
+
229
+ function readCliVersion(): string {
230
+ try {
231
+ const root = path.resolve(
232
+ path.dirname(fileURLToPath(import.meta.url)),
233
+ "../..",
234
+ );
235
+ const pkg = JSON.parse(
236
+ readFileSync(path.join(root, "package.json"), "utf8"),
237
+ ) as {
238
+ version?: unknown;
239
+ };
240
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
241
+ } catch {
242
+ return "unknown";
243
+ }
244
+ }
245
+
246
+ export type ToolEnvironment = {
247
+ store: Store;
248
+ registry: RegistryClient;
249
+ };
250
+
251
+ export type RunCliOptions = {
252
+ argv: readonly string[];
253
+ env: NodeJS.ProcessEnv;
254
+ stdout: { write(chunk: string): unknown };
255
+ stderr: { write(chunk: string): unknown };
256
+ readVersion?: () => string;
257
+ cwd?: string;
258
+ tools?: () => Promise<ToolEnvironment>;
259
+ };
260
+
261
+ /** Parse, dispatch, delegate, or run a tool, and emit. The only impure seam. */
262
+ export async function runCli(options: RunCliOptions): Promise<number> {
263
+ const parsed = parseArgs(options.argv);
264
+ const version = (options.readVersion ?? readCliVersion)();
265
+ const emit = (result: CliOutput): number => {
266
+ if (result.stdout !== "") options.stdout.write(result.stdout);
267
+ if (result.stderr !== "") options.stderr.write(result.stderr);
268
+ return result.exitCode;
269
+ };
270
+ // Delegation bypasses dispatch: zyrk interprets no command arguments
271
+ // itself, adds no JSON envelope, and forwards every non-zyrk token
272
+ // verbatim — before the command as well as after it. A leading
273
+ // protective flag such as `--dry-run` belongs to npm; dropping it would
274
+ // silently change what runs (S05). The only tokens zyrk consumes are its
275
+ // own `--json`/`--help`/`--version`, and `--json` is passed on so npm
276
+ // still sees exactly what it was given.
277
+ if (
278
+ !parsed.help &&
279
+ !parsed.version &&
280
+ parsed.command !== undefined &&
281
+ isDelegatedCommand(parsed.command)
282
+ ) {
283
+ try {
284
+ const forwarded = [...parsed.argsBeforeCommand];
285
+ if (parsed.json && !parsed.argsAfterCommand.includes("--json")) {
286
+ forwarded.push("--json");
287
+ }
288
+ forwarded.push(...parsed.argsAfterCommand);
289
+ return await delegateToNpm({
290
+ command: parsed.command,
291
+ args: forwarded,
292
+ env: options.env,
293
+ stderr: options.stderr,
294
+ });
295
+ } catch (error) {
296
+ return emit(
297
+ fail(
298
+ toZyrkError(error, "zyrk failed while delegating to npm."),
299
+ parsed,
300
+ ),
301
+ );
302
+ }
303
+ }
304
+ if (
305
+ !parsed.help &&
306
+ !parsed.version &&
307
+ (parsed.command === "dlx" || parsed.command === "create")
308
+ ) {
309
+ try {
310
+ return await runToolCommand(parsed.command, parsed, options, version);
311
+ } catch (error) {
312
+ return emit(
313
+ fail(toZyrkError(error, `zyrk ${parsed.command} failed.`), parsed),
314
+ );
315
+ }
316
+ }
317
+ return emit(dispatch(parsed, { version, env: options.env }));
318
+ }
319
+
320
+ async function defaultTools(
321
+ env: NodeJS.ProcessEnv,
322
+ version: string,
323
+ ): Promise<ToolEnvironment> {
324
+ const store = await Store.open({
325
+ root: resolveStoreRoot({ env }).root,
326
+ create: true,
327
+ });
328
+ const registry = new RegistryClient({
329
+ registry: DEFAULT_REGISTRY,
330
+ userAgent: `zyrk/${version}`,
331
+ });
332
+ return { store, registry };
333
+ }
334
+
335
+ async function runToolCommand(
336
+ command: "dlx" | "create",
337
+ parsed: ParsedArgs,
338
+ options: RunCliOptions,
339
+ version: string,
340
+ ): Promise<number> {
341
+ if (parsed.json) {
342
+ throw new ZyrkError(
343
+ "ZYRK_USAGE",
344
+ `zyrk ${command} does not support --json; the tool owns stdout.`,
345
+ {
346
+ detail: { command },
347
+ remedy: `run zyrk ${command} without --json.`,
348
+ },
349
+ );
350
+ }
351
+ const tail = parsed.commandArgs;
352
+ const first = tail[0];
353
+ if (first === undefined) {
354
+ throw new ZyrkError(
355
+ "ZYRK_USAGE",
356
+ command === "dlx"
357
+ ? "zyrk dlx needs a tool specifier."
358
+ : "zyrk create needs an initializer name.",
359
+ {
360
+ detail: { command },
361
+ remedy:
362
+ command === "dlx"
363
+ ? "use zyrk dlx <package>[@version] [args...]."
364
+ : "use zyrk create <name>[@version] [args...].",
365
+ },
366
+ );
367
+ }
368
+ const tools =
369
+ options.tools === undefined
370
+ ? await defaultTools(options.env, version)
371
+ : await options.tools();
372
+ const base = {
373
+ store: tools.store,
374
+ registry: tools.registry,
375
+ args: tail.slice(1),
376
+ cwd: options.cwd ?? process.cwd(),
377
+ env: options.env,
378
+ };
379
+ if (command === "dlx") {
380
+ return (await runDlx({ ...base, spec: first })).exitCode;
381
+ }
382
+ return (await runCreate({ ...base, input: first })).exitCode;
383
+ }