@gjsify/cli 0.4.35 → 0.4.36

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.
@@ -0,0 +1,99 @@
1
+ // Dead-token vs new-package diagnostic for `gjsify publish`.
2
+ //
3
+ // When the publish PUT returns `404 Not Found` (body `"Not Found"` or empty)
4
+ // it is ambiguous between two very different situations:
5
+ //
6
+ // 1. The `_authToken` in ~/.npmrc has been revoked / expired. The npm
7
+ // registry's 404 is its way of saying "we don't recognize this
8
+ // bearer". Easy to mistake for a missing-package error and send the
9
+ // user down the wrong path (Trusted-Publisher-bootstrap setup).
10
+ // 2. The scoped `@gjsify/<pkg>` genuinely doesn't exist on npm yet —
11
+ // the first-publish bootstrap step from AGENTS.md.
12
+ //
13
+ // `GET /-/whoami` with the SAME Authorization header disambiguates: a live
14
+ // token returns `{"username": "..."}`, a dead token returns `{}` (status 200
15
+ // in both cases — the empty body is npm's signal).
16
+ //
17
+ // `npm publish` from npm-cli handles dead tokens with `401 EOTP` + a clear
18
+ // "one-time-password or refresh the token" hint. The 404 path is npm's
19
+ // PUT-specific shape and `gjsify publish` previously just printed the
20
+ // opaque body. This helper closes the diagnostic gap.
21
+ import { whoami } from '@gjsify/npm-registry';
22
+ /**
23
+ * Probe `/-/whoami` to disambiguate the 404 cause. Best-effort: any thrown
24
+ * error / non-2xx falls through to `reason: 'unknown'` so the caller's
25
+ * generic error path runs untouched.
26
+ *
27
+ * Pure async I/O — no side effects, no process.exit, no console writes.
28
+ * The caller owns presentation (stderr vs stdout, plain vs JSON).
29
+ */
30
+ export async function diagnose404(input) {
31
+ const { packageName, version, registry, npmrc } = input;
32
+ let probe = {};
33
+ try {
34
+ probe = await whoami(registry, npmrc);
35
+ }
36
+ catch {
37
+ return { reason: 'unknown', message: formatUnknown(packageName, version) };
38
+ }
39
+ if (probe.username && probe.username.length > 0) {
40
+ return {
41
+ reason: 'live-token-404',
42
+ username: probe.username,
43
+ message: formatLiveToken404(packageName, version, probe.username),
44
+ };
45
+ }
46
+ return { reason: 'dead-token', message: formatDeadToken(packageName, version) };
47
+ }
48
+ function formatDeadToken(name, version) {
49
+ return [
50
+ `gjsify publish: ${name}@${version} — 404 Not Found`,
51
+ '',
52
+ 'The npm token in ~/.npmrc appears to be revoked or expired (the /-/whoami probe',
53
+ 'returned {} instead of {"username": "..."}). The 404 is npm\'s response to a PUT',
54
+ 'authenticated with an invalid bearer token.',
55
+ '',
56
+ 'To refresh:',
57
+ ' npm login',
58
+ ' # or, future: gjsify login (tracked as project_gjsify_login_goal)',
59
+ '',
60
+ 'Then verify before publishing:',
61
+ ' curl -s -H "Authorization: Bearer $(grep registry.npmjs.org ~/.npmrc | sed \'s|.*=||\')" \\',
62
+ ' https://registry.npmjs.org/-/whoami',
63
+ ' # Healthy: {"username":"<you>"}',
64
+ ' # Dead: {}',
65
+ ].join('\n');
66
+ }
67
+ function formatLiveToken404(name, version, username) {
68
+ return [
69
+ `gjsify publish: ${name}@${version} — 404 Not Found`,
70
+ '',
71
+ `Authenticated as: ${username}`,
72
+ '',
73
+ `The package ${name} does not yet exist on npmjs.com. For a brand-new`,
74
+ 'scoped package, this usually means the first-publish bootstrap step is needed',
75
+ '(see AGENTS.md > "New @gjsify/* package: first-publish + Trusted Publisher',
76
+ 'bootstrap"). If you\'re sure you have write access to the scope, verify with:',
77
+ ' npm access ls-packages',
78
+ ].join('\n');
79
+ }
80
+ function formatUnknown(name, version) {
81
+ return `gjsify publish: ${name}@${version} — 404 Not Found`;
82
+ }
83
+ /**
84
+ * Heuristic: is the 404 body the "dead-token-or-missing-package" shape?
85
+ * npm's PUT returns plain text `Not Found` (sometimes empty body). We trigger
86
+ * the diagnostic for both, plus for the JSON `{"error":"Not Found"}` shape
87
+ * just in case. Other 404 bodies (e.g. with a structured npm error code)
88
+ * keep the generic error path.
89
+ */
90
+ export function is404DiagnosticCandidate(body) {
91
+ const trimmed = body.trim();
92
+ if (trimmed.length === 0)
93
+ return true;
94
+ if (/^not found$/i.test(trimmed))
95
+ return true;
96
+ if (/"error"\s*:\s*"Not Found"/i.test(trimmed))
97
+ return true;
98
+ return false;
99
+ }
@@ -0,0 +1,21 @@
1
+ export interface ResolveNpmPackageOptions {
2
+ /** Anchor directory to search from (typically the caller's cwd). */
3
+ cwd?: string;
4
+ /**
5
+ * Bundle path or module URL to anchor the bundle-side createRequire
6
+ * at. Pass `import.meta.url` from the call site so the bundle's
7
+ * own node_modules chain stays reachable even when the CLI is
8
+ * invoked from an unrelated cwd.
9
+ */
10
+ bundleUrl?: string;
11
+ }
12
+ /**
13
+ * Resolve a bare npm specifier through multiple createRequire anchors.
14
+ * Returns the absolute filesystem path of the resolved module, or null
15
+ * if no anchor succeeded.
16
+ *
17
+ * Each anchor is tried in priority order — `GJSIFY_NODE_PATH` (highest)
18
+ * → caller cwd → workspace root → bundle URL → parent-dir walk from
19
+ * cwd. The first anchor whose `require.resolve()` succeeds wins.
20
+ */
21
+ export declare function resolveNpmPackage(specifier: string, opts?: ResolveNpmPackageOptions): string | null;
@@ -0,0 +1,121 @@
1
+ // Multi-anchor npm-package resolver for the GJS-bundled CLI.
2
+ //
3
+ // GJS's native ESM loader has no node_modules walker — a bare
4
+ // `await import('rolldown')` from inside the bundle throws
5
+ // `ImportError: Module not found: rolldown` even when the package is
6
+ // physically present in a node_modules above the caller's cwd.
7
+ //
8
+ // `createRequire(import.meta.url)` works when invoked under Node, and
9
+ // when invoked under GJS *with* `@gjsify/module`'s polyfill it walks the
10
+ // node_modules chain from the URL it was anchored at. But it walks
11
+ // only ONE chain — the one rooted at the anchor's parent. When the
12
+ // bundle lives at `<install>/dist/cli.gjs.mjs` and the user runs
13
+ // `gjs -m <install>/dist/cli.gjs.mjs build …` from a completely
14
+ // unrelated cwd (`/tmp/sandbox/`) whose `node_modules` is the only
15
+ // place rolldown is present, anchoring on the bundle URL misses it.
16
+ //
17
+ // This helper resolves a bare specifier against multiple anchors in
18
+ // order — the first one that resolves wins:
19
+ //
20
+ // 1. `GJSIFY_NODE_PATH` env override (colon-separated dir list,
21
+ // matches Node's NODE_PATH semantics — each entry is treated as a
22
+ // synthetic `node_modules` parent).
23
+ // 2. Caller-supplied anchor dir (typically process.cwd()).
24
+ // 3. Workspace root from `findWorkspaceRoot(anchorDir)` — finds the
25
+ // monorepo top when invoked from a sub-package directory.
26
+ // 4. Bundle path's parent chain (`import.meta.url` of THIS module).
27
+ // 5. Parent dirs of the anchor (fallback for nested-without-workspace
28
+ // setups — matches `oxc-resolve.ts`'s walker).
29
+ //
30
+ // Returns the absolute path on success (the caller usually wraps it in
31
+ // `pathToFileURL().href` before passing to dynamic `import(...)`).
32
+ // Returns null when every anchor failed.
33
+ //
34
+ // Reference: `packages/infra/cli/src/commands/tsc.ts` (workspace-root +
35
+ // cwd anchoring precedent) and `packages/infra/cli/src/utils/oxc-resolve.ts`
36
+ // (parent-dir walker precedent).
37
+ import { createRequire } from 'node:module';
38
+ import { pathToFileURL } from 'node:url';
39
+ import { join, resolve } from 'node:path';
40
+ import { findWorkspaceRoot } from './workspace-root.js';
41
+ /**
42
+ * Resolve a bare npm specifier through multiple createRequire anchors.
43
+ * Returns the absolute filesystem path of the resolved module, or null
44
+ * if no anchor succeeded.
45
+ *
46
+ * Each anchor is tried in priority order — `GJSIFY_NODE_PATH` (highest)
47
+ * → caller cwd → workspace root → bundle URL → parent-dir walk from
48
+ * cwd. The first anchor whose `require.resolve()` succeeds wins.
49
+ */
50
+ export function resolveNpmPackage(specifier, opts = {}) {
51
+ const cwd = opts.cwd ?? process.cwd();
52
+ // 1. GJSIFY_NODE_PATH env override (NODE_PATH-like) — each entry is
53
+ // treated as a directory whose own `node_modules` participates
54
+ // in the lookup. Mirrors Node's NODE_PATH semantics.
55
+ const envPath = process.env['GJSIFY_NODE_PATH'];
56
+ if (envPath) {
57
+ for (const dir of envPath.split(':').filter(Boolean)) {
58
+ const hit = tryResolveFromDir(specifier, resolve(dir));
59
+ if (hit)
60
+ return hit;
61
+ }
62
+ }
63
+ // 2. Caller cwd → walk up for nearest node_modules/<pkg>.
64
+ const fromCwd = tryResolveFromDir(specifier, cwd);
65
+ if (fromCwd)
66
+ return fromCwd;
67
+ // 3. Workspace root (monorepo top, if any).
68
+ const wsRoot = findWorkspaceRoot(cwd);
69
+ if (wsRoot && wsRoot !== cwd) {
70
+ const fromWsRoot = tryResolveFromDir(specifier, wsRoot);
71
+ if (fromWsRoot)
72
+ return fromWsRoot;
73
+ }
74
+ // 4. Bundle path's own parent chain — anchored at the bundle URL
75
+ // so the install-time node_modules layout (e.g.
76
+ // `<install>/node_modules/rolldown` next to the CLI bundle)
77
+ // is reachable even when the user runs the bundle directly from
78
+ // an unrelated cwd.
79
+ if (opts.bundleUrl) {
80
+ try {
81
+ const req = createRequire(opts.bundleUrl);
82
+ return req.resolve(specifier);
83
+ }
84
+ catch {
85
+ // Fall through.
86
+ }
87
+ }
88
+ // 5. Parent-dir walk from cwd as a last resort (nested
89
+ // package layouts without a workspace marker). Capped at 8
90
+ // levels to avoid runaway filesystem walks on pathological
91
+ // layouts. Matches the depth `oxc-resolve.ts` uses for its
92
+ // equivalent walker.
93
+ let dir = resolve(cwd, '..');
94
+ for (let i = 0; i < 8; i++) {
95
+ const hit = tryResolveFromDir(specifier, dir);
96
+ if (hit)
97
+ return hit;
98
+ const parent = resolve(dir, '..');
99
+ if (parent === dir)
100
+ break;
101
+ dir = parent;
102
+ }
103
+ return null;
104
+ }
105
+ /**
106
+ * Attempt to resolve `specifier` as if a fictional source file at
107
+ * `<dir>/__gjsify_resolve__.js` were doing the lookup. createRequire's
108
+ * algorithm walks up from the anchor's parent for `node_modules/<pkg>`
109
+ * so seeding it inside `<dir>` makes `<dir>/node_modules/<pkg>` the
110
+ * first hit.
111
+ */
112
+ function tryResolveFromDir(specifier, dir) {
113
+ try {
114
+ const sentinel = pathToFileURL(join(dir, '__gjsify_resolve__.js')).href;
115
+ const req = createRequire(sentinel);
116
+ return req.resolve(specifier);
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.4.35",
3
+ "version": "0.4.36",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -120,35 +120,46 @@
120
120
  "cli"
121
121
  ],
122
122
  "dependencies": {
123
- "@gjsify/buffer": "^0.4.35",
124
- "@gjsify/create-app": "^0.4.35",
125
- "@gjsify/node-globals": "^0.4.35",
126
- "@gjsify/node-polyfills": "^0.4.35",
127
- "@gjsify/npm-registry": "^0.4.35",
128
- "@gjsify/resolve-npm": "^0.4.35",
129
- "@gjsify/rolldown-plugin-gjsify": "^0.4.35",
130
- "@gjsify/rolldown-plugin-pnp": "^0.4.35",
131
- "@gjsify/semver": "^0.4.35",
132
- "@gjsify/tar": "^0.4.35",
133
- "@gjsify/web-polyfills": "^0.4.35",
134
- "@gjsify/workspace": "^0.4.35",
123
+ "@gjsify/buffer": "^0.4.36",
124
+ "@gjsify/create-app": "^0.4.36",
125
+ "@gjsify/node-globals": "^0.4.36",
126
+ "@gjsify/node-polyfills": "^0.4.36",
127
+ "@gjsify/npm-registry": "^0.4.36",
128
+ "@gjsify/resolve-npm": "^0.4.36",
129
+ "@gjsify/rolldown-plugin-gjsify": "^0.4.36",
130
+ "@gjsify/rolldown-plugin-pnp": "^0.4.36",
131
+ "@gjsify/semver": "^0.4.36",
132
+ "@gjsify/tar": "^0.4.36",
133
+ "@gjsify/tsc": "^0.4.36",
134
+ "@gjsify/web-polyfills": "^0.4.36",
135
+ "@gjsify/workspace": "^0.4.36",
135
136
  "cosmiconfig": "^9.0.1",
136
137
  "get-tsconfig": "^4.14.0",
137
138
  "pkg-types": "^2.3.1",
138
- "rolldown": "^1.0.0",
139
+ "rolldown": "^1.0.3",
139
140
  "yargs": "^18.0.0"
140
141
  },
141
142
  "devDependencies": {
142
- "@gjsify/unit": "^0.4.35",
143
+ "@gjsify/unit": "^0.4.36",
143
144
  "@types/yargs": "^17.0.35",
144
- "typescript": "^6.0.3"
145
+ "typescript": "^5.9.3"
145
146
  },
146
147
  "peerDependencies": {
147
- "@gjsify/rolldown-native": "^0.4.35"
148
+ "@gjsify/rolldown-native": "^0.4.36"
148
149
  },
149
150
  "peerDependenciesMeta": {
150
151
  "@gjsify/rolldown-native": {
151
152
  "optional": true
152
153
  }
153
- }
154
+ },
155
+ "license": "MIT",
156
+ "repository": {
157
+ "type": "git",
158
+ "url": "git+https://github.com/gjsify/gjsify.git",
159
+ "directory": "packages/infra/cli"
160
+ },
161
+ "bugs": {
162
+ "url": "https://github.com/gjsify/gjsify/issues"
163
+ },
164
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/cli#readme"
154
165
  }