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 +21 -0
- package/README.md +188 -2
- package/VERSION +1 -0
- package/bin/zyrk.js +9 -0
- package/package.json +64 -4
- package/src/cli/main.ts +383 -0
- package/src/cli/npm-delegate.ts +97 -0
- package/src/cli/parse.ts +106 -0
- package/src/config/paths.ts +119 -0
- package/src/dlx/create.ts +69 -0
- package/src/dlx/run.ts +389 -0
- package/src/errors.ts +142 -0
- package/src/extract/tarball.ts +868 -0
- package/src/fetch/all.ts +206 -0
- package/src/fetch/package.ts +181 -0
- package/src/fetch/registry.ts +615 -0
- package/src/lockfile/fingerprints.ts +74 -0
- package/src/lockfile/read.ts +198 -0
- package/src/lockfile/types.ts +70 -0
- package/src/lockfile/write.ts +196 -0
- package/src/log.ts +100 -0
- package/src/project/projection.ts +228 -0
- package/src/resolve/resolver.ts +757 -0
- package/src/resolve/spec.ts +131 -0
- package/src/store/db.ts +388 -0
- package/src/store/graph.ts +175 -0
- package/src/store/layout.ts +431 -0
- package/src/store/manifests.ts +347 -0
- package/src/store/objects.ts +229 -0
- package/src/store/realize.ts +322 -0
- package/src/store/source.ts +249 -0
- package/src/store/txn.ts +118 -0
- package/src/store/verify.ts +433 -0
- package/src/types/vendor.d.ts +120 -0
- package/src/util/fsx.ts +289 -0
- package/src/util/hash.ts +117 -0
- package/src/util/ids.ts +42 -0
- package/src/util/integrity.ts +269 -0
- package/src/util/json.ts +89 -0
- package/src/util/locks.ts +266 -0
- package/src/util/proc.ts +108 -0
- package/src/version.ts +34 -0
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
|
-
#
|
|
1
|
+
# zyrk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/kingjethro999/nexu/actions/workflows/ci.yml)
|
|
4
|
+
[](https://nodejs.org/)
|
|
5
|
+
[](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
package/package.json
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zyrk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|
package/src/cli/main.ts
ADDED
|
@@ -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
|
+
}
|