@gjsify/cli 0.25.1 → 0.26.1
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/dist/cli.gjs.mjs +171 -171
- package/lib/cli-app.js +7 -10
- package/lib/commands/affected.js +160 -17
- package/lib/commands/check.js +25 -8
- package/lib/commands/create.js +3 -1
- package/lib/commands/debug.js +3 -1
- package/lib/commands/flatpak/check.js +4 -3
- package/lib/commands/flatpak/diff.js +3 -1
- package/lib/commands/flatpak/sources.js +4 -7
- package/lib/commands/foreach.js +25 -7
- package/lib/commands/generate-installer.js +2 -4
- package/lib/commands/install.js +15 -7
- package/lib/commands/login.js +3 -1
- package/lib/commands/onboard.js +3 -6
- package/lib/commands/publish.js +8 -11
- package/lib/commands/self-update.js +4 -8
- package/lib/commands/showcase.d.ts +16 -0
- package/lib/commands/showcase.js +44 -24
- package/lib/commands/storybook.js +16 -2
- package/lib/commands/system-check.js +1 -2
- package/lib/commands/test.js +6 -2
- package/lib/commands/trust.js +12 -7
- package/lib/commands/tsc.js +3 -1
- package/lib/commands/uninstall.js +1 -2
- package/lib/commands/whoami.js +6 -3
- package/lib/utils/build-cache.js +7 -2
- package/lib/utils/dir-link.d.ts +58 -0
- package/lib/utils/dir-link.js +126 -0
- package/lib/utils/dlx-cache.d.ts +30 -3
- package/lib/utils/dlx-cache.js +63 -10
- package/lib/utils/install-backend-native.js +1 -1
- package/lib/utils/install-lock.js +5 -7
- package/lib/utils/install-packument-cache.d.ts +23 -0
- package/lib/utils/install-packument-cache.js +45 -10
- package/lib/utils/install-tarball-cache.js +5 -8
- package/lib/utils/oxc-resolve.js +1 -1
- package/lib/utils/prompt.js +6 -1
- package/lib/utils/runtimes.d.ts +18 -0
- package/lib/utils/runtimes.js +20 -0
- package/lib/utils/stdin.js +6 -7
- package/package.json +17 -17
package/lib/commands/onboard.js
CHANGED
|
@@ -116,8 +116,7 @@ export const onboardCommand = {
|
|
|
116
116
|
process.stdout.write(`${JSON.stringify({ ok: false, error: 'auth', message: msg })}\n`);
|
|
117
117
|
else
|
|
118
118
|
console.error(msg);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
return;
|
|
119
|
+
return process.exit(1);
|
|
121
120
|
}
|
|
122
121
|
// Login may have written a fresh token — reload so the probes/ops use it.
|
|
123
122
|
npmrc = await loadNpmrc(cwd);
|
|
@@ -136,8 +135,7 @@ export const onboardCommand = {
|
|
|
136
135
|
process.stdout.write(`${JSON.stringify({ ok: false, error: 'repository', message: msg })}\n`);
|
|
137
136
|
else
|
|
138
137
|
console.error(msg);
|
|
139
|
-
process.exit(1);
|
|
140
|
-
return;
|
|
138
|
+
return process.exit(1);
|
|
141
139
|
}
|
|
142
140
|
const workflow = normalizeWorkflowFile(args.workflow);
|
|
143
141
|
const environment = args.environment;
|
|
@@ -155,8 +153,7 @@ export const onboardCommand = {
|
|
|
155
153
|
process.stdout.write(`${JSON.stringify({ ok: false, error: 'no-packages', message: msg })}\n`);
|
|
156
154
|
else
|
|
157
155
|
console.error(msg);
|
|
158
|
-
process.exit(1);
|
|
159
|
-
return;
|
|
156
|
+
return process.exit(1);
|
|
160
157
|
}
|
|
161
158
|
log(`gjsify onboard: ${selected.length} package(s) | repo=${repository} workflow=${workflow}`);
|
|
162
159
|
if (dryRun)
|
package/lib/commands/publish.js
CHANGED
|
@@ -125,7 +125,9 @@ export const publishCommand = {
|
|
|
125
125
|
const rawPkg = JSON.parse(readFileSync(rawPkgPath, 'utf-8'));
|
|
126
126
|
if (typeof rawPkg.name !== 'string') {
|
|
127
127
|
process.stderr.write(`gjsify publish --check-trusted: ${rawPkgPath} has no \`name\` field\n`);
|
|
128
|
-
process.exit(
|
|
128
|
+
// `return` — a bare `process.exit()` is deferred under GJS and
|
|
129
|
+
// the check would continue on a nameless manifest.
|
|
130
|
+
return process.exit(2);
|
|
129
131
|
}
|
|
130
132
|
if (rawPkg.private === true) {
|
|
131
133
|
const out = { ok: true, action: 'check-trusted', name: rawPkg.name, skipped: 'private' };
|
|
@@ -432,13 +434,11 @@ function reportPublishOutcome(outcome, asJson) {
|
|
|
432
434
|
}
|
|
433
435
|
case 'otp-required': {
|
|
434
436
|
console.error(`gjsify publish: npm requires a 2FA one-time code — re-run with \`--otp <code>\``);
|
|
435
|
-
process.exit(1);
|
|
436
|
-
return;
|
|
437
|
+
return process.exit(1);
|
|
437
438
|
}
|
|
438
439
|
case 'oidc-failed': {
|
|
439
440
|
handleOidcFailure(outcome.error, outcome.name, asJson);
|
|
440
|
-
process.exit(1);
|
|
441
|
-
return;
|
|
441
|
+
return process.exit(1);
|
|
442
442
|
}
|
|
443
443
|
case 'oidc-no-token': {
|
|
444
444
|
const msg = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
|
|
@@ -446,8 +446,7 @@ function reportPublishOutcome(outcome, asJson) {
|
|
|
446
446
|
if (asJson) {
|
|
447
447
|
process.stdout.write(`${JSON.stringify({ ok: false, name: outcome.name, version: outcome.version, error: 'oidc-failed-no-token' }, null, 2)}\n`);
|
|
448
448
|
}
|
|
449
|
-
process.exit(1);
|
|
450
|
-
return;
|
|
449
|
+
return process.exit(1);
|
|
451
450
|
}
|
|
452
451
|
case 'diagnostic': {
|
|
453
452
|
if (asJson) {
|
|
@@ -463,14 +462,12 @@ function reportPublishOutcome(outcome, asJson) {
|
|
|
463
462
|
else {
|
|
464
463
|
process.stderr.write(`${outcome.diag.message}\n`);
|
|
465
464
|
}
|
|
466
|
-
process.exit(1);
|
|
467
|
-
return;
|
|
465
|
+
return process.exit(1);
|
|
468
466
|
}
|
|
469
467
|
case 'error': {
|
|
470
468
|
console.error(`gjsify publish: ${outcome.name}@${outcome.version} — ${outcome.status} ${outcome.statusText}`);
|
|
471
469
|
console.error(outcome.text);
|
|
472
|
-
process.exit(1);
|
|
473
|
-
return;
|
|
470
|
+
return process.exit(1);
|
|
474
471
|
}
|
|
475
472
|
}
|
|
476
473
|
}
|
|
@@ -65,15 +65,13 @@ export const selfUpdateCommand = {
|
|
|
65
65
|
catch (err) {
|
|
66
66
|
const msg = err instanceof Error ? err.message : String(err);
|
|
67
67
|
console.error(`Failed to fetch packument: ${msg}`);
|
|
68
|
-
process.exit(1);
|
|
69
|
-
return;
|
|
68
|
+
return process.exit(1);
|
|
70
69
|
}
|
|
71
70
|
const target = resolveTag(packument, args.tag);
|
|
72
71
|
if (!target) {
|
|
73
72
|
console.error(`Unknown dist-tag '${args.tag}' on ${PACKAGE_NAME}. ` +
|
|
74
73
|
`Known tags: ${Object.keys(packument['dist-tags'] ?? {}).join(', ') || '(none)'}`);
|
|
75
|
-
process.exit(1);
|
|
76
|
-
return;
|
|
74
|
+
return process.exit(1);
|
|
77
75
|
}
|
|
78
76
|
console.log(`Latest matching --tag ${args.tag}: v${target}`);
|
|
79
77
|
// A version match is NOT sufficient to declare "up to date": a global
|
|
@@ -103,8 +101,7 @@ export const selfUpdateCommand = {
|
|
|
103
101
|
}
|
|
104
102
|
if (args.check) {
|
|
105
103
|
console.log(currentVersion ? `Update available: v${currentVersion} → v${target}` : `Install required: → v${target}`);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
return;
|
|
104
|
+
return process.exit(1);
|
|
108
105
|
}
|
|
109
106
|
// `@gjsify/cli` ships two runtimes from one package: the Node `bin`
|
|
110
107
|
// (`lib/index.js`, which imports its deps from `node_modules`) and the
|
|
@@ -141,8 +138,7 @@ export const selfUpdateCommand = {
|
|
|
141
138
|
const msg = err instanceof Error ? err.message : String(err);
|
|
142
139
|
console.error(`self-update: install failed — ${msg}`);
|
|
143
140
|
console.error(`Re-run \`gjsify self-update\` to retry.`);
|
|
144
|
-
process.exit(1);
|
|
145
|
-
return;
|
|
141
|
+
return process.exit(1);
|
|
146
142
|
}
|
|
147
143
|
// Lay down (or repair) the GJS-native engine bridges in lockstep with
|
|
148
144
|
// the bundle — same set + best-effort handling as the global installer.
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import type { Command } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pinned invocations for the three runners that cache an unpinned bin.
|
|
4
|
+
*
|
|
5
|
+
* The deno line carries TWO flags because deno stacks two independent refusals,
|
|
6
|
+
* and dropping either one leaves the hint broken in exactly the window where it
|
|
7
|
+
* is reached most: `--reload` busts the `DENO_DIR` copy, and `--min-dep-age 0`
|
|
8
|
+
* waives deno's supply-chain policy of refusing any version published within
|
|
9
|
+
* the last 24 h (`minimumDependencyAge`, default `24h`). Without the second
|
|
10
|
+
* flag `@latest` silently resolves to the newest release OLDER THAN A DAY — so
|
|
11
|
+
* a user who hits a bug, is told to run `@latest`, and does, gets the SAME
|
|
12
|
+
* pre-fix binary back and a stale error message with it. Measured twice on one
|
|
13
|
+
* day against 0.25.1 (minutes old) falling back to 0.24.1. `@latest` is kept
|
|
14
|
+
* rather than a baked version so the hint cannot go stale itself; the age
|
|
15
|
+
* waiver is what makes `@latest` mean latest.
|
|
16
|
+
*/
|
|
17
|
+
export declare const PIN_HINT: string;
|
|
2
18
|
interface ShowcaseOptions {
|
|
3
19
|
name?: string;
|
|
4
20
|
json: boolean;
|
package/lib/commands/showcase.js
CHANGED
|
@@ -7,7 +7,7 @@ import { ensurePkgDir } from './dlx.js';
|
|
|
7
7
|
import { parseSpec } from '../utils/parse-spec.js';
|
|
8
8
|
import { resolveNodeEntry } from '../utils/resolve-gjs-entry.js';
|
|
9
9
|
import { runRuntimeBundle } from '../utils/run-node.js';
|
|
10
|
-
import { EXAMPLE_RUNTIMES, isExampleRuntime, readDeclaredRuntimes, checkRuntimeSupported, defaultExampleRuntime, } from '../utils/runtimes.js';
|
|
10
|
+
import { EXAMPLE_RUNTIMES, isExampleRuntime, readDeclaredRuntimes, checkRuntimeSupported, defaultExampleRuntime, requiresGjsSystemDeps, } from '../utils/runtimes.js';
|
|
11
11
|
function readCliVersion() {
|
|
12
12
|
try {
|
|
13
13
|
const pkgUrl = new URL('../../package.json', import.meta.url);
|
|
@@ -34,11 +34,25 @@ function readCliVersion() {
|
|
|
34
34
|
function runningAs(cliVersion) {
|
|
35
35
|
return cliVersion ? `[gjsify ${cliVersion}] ` : '';
|
|
36
36
|
}
|
|
37
|
-
/**
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Pinned invocations for the three runners that cache an unpinned bin.
|
|
39
|
+
*
|
|
40
|
+
* The deno line carries TWO flags because deno stacks two independent refusals,
|
|
41
|
+
* and dropping either one leaves the hint broken in exactly the window where it
|
|
42
|
+
* is reached most: `--reload` busts the `DENO_DIR` copy, and `--min-dep-age 0`
|
|
43
|
+
* waives deno's supply-chain policy of refusing any version published within
|
|
44
|
+
* the last 24 h (`minimumDependencyAge`, default `24h`). Without the second
|
|
45
|
+
* flag `@latest` silently resolves to the newest release OLDER THAN A DAY — so
|
|
46
|
+
* a user who hits a bug, is told to run `@latest`, and does, gets the SAME
|
|
47
|
+
* pre-fix binary back and a stale error message with it. Measured twice on one
|
|
48
|
+
* day against 0.25.1 (minutes old) falling back to 0.24.1. `@latest` is kept
|
|
49
|
+
* rather than a baked version so the hint cannot go stale itself; the age
|
|
50
|
+
* waiver is what makes `@latest` mean latest.
|
|
51
|
+
*/
|
|
52
|
+
export const PIN_HINT = 'A package runner reuses a CACHED copy of an unpinned bin. Force the current one:\n' +
|
|
39
53
|
' npx @gjsify/cli@latest showcase <name>\n' +
|
|
40
54
|
' bunx @gjsify/cli@latest showcase <name>\n' +
|
|
41
|
-
' deno run -A --reload npm:@gjsify/cli@latest showcase <name>';
|
|
55
|
+
' deno run -A --reload --min-dep-age 0 npm:@gjsify/cli@latest showcase <name>';
|
|
42
56
|
export const showcaseCommand = {
|
|
43
57
|
command: 'showcase [name]',
|
|
44
58
|
description: 'List or run curated gjsify showcase applications.',
|
|
@@ -119,33 +133,39 @@ export const showcaseCommand = {
|
|
|
119
133
|
// `m_should_exit` core dump. Returning halts the handler here.
|
|
120
134
|
return process.exit(1);
|
|
121
135
|
}
|
|
136
|
+
// Resolve the runtime BEFORE the system-dependency gate: that gate is a
|
|
137
|
+
// question ABOUT the runtime, and asking it first gated every showcase
|
|
138
|
+
// on a gjs binary (see `requiresGjsSystemDeps`).
|
|
139
|
+
const runtime = (args.runtime ?? defaultExampleRuntime());
|
|
140
|
+
if (!isExampleRuntime(runtime)) {
|
|
141
|
+
console.error(`Unknown --runtime "${runtime}" (expected: ${EXAMPLE_RUNTIMES.join(', ')}).`);
|
|
142
|
+
// `return` — bare `process.exit` falls through under GJS (see above).
|
|
143
|
+
return process.exit(1);
|
|
144
|
+
}
|
|
122
145
|
// System dependency check before delegating — only system libs (gjs,
|
|
123
|
-
// gtk4, …)
|
|
146
|
+
// gtk4, …), and only for a runtime that consumes the `--app gjs`
|
|
147
|
+
// bundle. The showcase's npm deps (incl. `@gjsify/webgl` with the
|
|
124
148
|
// gwebgl Vala prebuild) are fetched by `gjsify dlx` into the npm
|
|
125
149
|
// cache, and `runGjsBundle()` picks the prebuild up from the bundle
|
|
126
150
|
// dir via `detectNativePackages()`. Pre-flight-checking npm deps
|
|
127
151
|
// here would fail for `npx @gjsify/cli showcase` (no project
|
|
128
152
|
// node_modules, CLI doesn't dep on the showcase libs).
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
if (requiresGjsSystemDeps(runtime)) {
|
|
154
|
+
const results = runMinimalChecks();
|
|
155
|
+
const missingHard = results.filter((r) => !r.found && r.severity === 'required');
|
|
156
|
+
if (missingHard.length > 0) {
|
|
157
|
+
console.error('Missing system dependencies:\n');
|
|
158
|
+
for (const dep of missingHard) {
|
|
159
|
+
console.error(` ✗ ${dep.name}`);
|
|
160
|
+
}
|
|
161
|
+
const pm = detectPackageManager();
|
|
162
|
+
const cmd = buildInstallCommand(pm, missingHard);
|
|
163
|
+
if (cmd) {
|
|
164
|
+
console.error(`\nInstall with:\n ${cmd}`);
|
|
165
|
+
}
|
|
166
|
+
// `return` — bare `process.exit` falls through under GJS (see above).
|
|
167
|
+
return process.exit(1);
|
|
140
168
|
}
|
|
141
|
-
// `return` — bare `process.exit` falls through under GJS (see above).
|
|
142
|
-
return process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
const runtime = (args.runtime ?? defaultExampleRuntime());
|
|
145
|
-
if (!isExampleRuntime(runtime)) {
|
|
146
|
-
console.error(`Unknown --runtime "${runtime}" (expected: ${EXAMPLE_RUNTIMES.join(', ')}).`);
|
|
147
|
-
// `return` — bare `process.exit` falls through under GJS (see above).
|
|
148
|
-
return process.exit(1);
|
|
149
169
|
}
|
|
150
170
|
const cliVersion = readCliVersion();
|
|
151
171
|
// Non-gjs runtime: resolve the showcase's `--app node` bundle and run it
|
|
@@ -136,8 +136,22 @@ export const storybookCommand = {
|
|
|
136
136
|
};
|
|
137
137
|
const initialCount = writeEntry();
|
|
138
138
|
if (initialCount === 0) {
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
// `return process.exit()`: a bare `process.exit()` does NOT halt
|
|
140
|
+
// synchronously under GJS (no atexit, the GLib loop is still
|
|
141
|
+
// armed), so execution fell through to the "Found 0 story file(s)"
|
|
142
|
+
// log and on into the build — and the deferred exit never carried
|
|
143
|
+
// its code, so `gjsify storybook` in a directory with no stories
|
|
144
|
+
// printed BOTH messages and exited 0. Measured: exit 1 under bun,
|
|
145
|
+
// exit 0 under gjs, same command. Same trap as `showcase.ts`.
|
|
146
|
+
console.error(`No *.story.ts files found under ${relative(cwd, storiesDir) || storiesDir}\n\n` +
|
|
147
|
+
` \`gjsify storybook\` renders the stories of the CURRENT project: it discovers\n` +
|
|
148
|
+
` \`*.story.ts\` files and launches a component browser over them. Point it at a\n` +
|
|
149
|
+
` different directory with --stories <dir> (or \`gjsify.storybook.stories\` in\n` +
|
|
150
|
+
` package.json) if yours live elsewhere.\n\n` +
|
|
151
|
+
` To SEE a storybook without authoring one first, run the curated Libadwaita\n` +
|
|
152
|
+
` component browser — it is a showcase, so it needs no project at all:\n\n` +
|
|
153
|
+
` gjsify showcase adwaita-storybook`);
|
|
154
|
+
return process.exit(1);
|
|
141
155
|
}
|
|
142
156
|
console.log(`Found ${initialCount} story file(s) under ${relative(cwd, storiesDir) || '.'} (runtime: ${runtime})`);
|
|
143
157
|
// In-process build — runtime-agnostic (Node uses npm rolldown, GJS uses
|
|
@@ -18,8 +18,7 @@ export const systemCheckCommand = {
|
|
|
18
18
|
if (args.json) {
|
|
19
19
|
console.log(JSON.stringify({ packageManager: pm, deps: results }, null, 2));
|
|
20
20
|
// Only required deps influence the exit code.
|
|
21
|
-
process.exit(missingRequired.length > 0 ? 1 : 0);
|
|
22
|
-
return;
|
|
21
|
+
return process.exit(missingRequired.length > 0 ? 1 : 0);
|
|
23
22
|
}
|
|
24
23
|
console.log('System dependency check\n');
|
|
25
24
|
const required = results.filter((r) => r.severity === 'required');
|
package/lib/commands/test.js
CHANGED
|
@@ -60,7 +60,9 @@ export const testCommand = {
|
|
|
60
60
|
console.error(`[gjsify test] no test entry at ${relative(cwd, entry)} — ` +
|
|
61
61
|
`add an \`src/test.mts\` that aggregates your \`@gjsify/unit\` suites, ` +
|
|
62
62
|
`or set \`gjsify.test.entry\` in package.json.`);
|
|
63
|
-
process.exit(
|
|
63
|
+
// `return` — a bare `process.exit()` is deferred under GJS and the
|
|
64
|
+
// handler would try to build the missing entry anyway.
|
|
65
|
+
return process.exit(1);
|
|
64
66
|
}
|
|
65
67
|
const requested = args.runtime === 'gjs'
|
|
66
68
|
? ['gjs']
|
|
@@ -124,7 +126,9 @@ export const testCommand = {
|
|
|
124
126
|
console.log(`[gjsify test] ${summary}`);
|
|
125
127
|
const anyFailed = results.some((r) => !r.ok);
|
|
126
128
|
if (anyFailed) {
|
|
127
|
-
|
|
129
|
+
// `return` — the deferred GJS exit otherwise fell through to the
|
|
130
|
+
// success `process.exit(0)` below, clobbering the failure code.
|
|
131
|
+
return process.exit(1);
|
|
128
132
|
}
|
|
129
133
|
// Explicit success exit: under GJS the spawn-armed main loop would
|
|
130
134
|
// otherwise park this process after the summary. (runGjsBundle
|
package/lib/commands/trust.js
CHANGED
|
@@ -79,7 +79,9 @@ export const trustCommand = {
|
|
|
79
79
|
const npmrc = await loadNpmrc(cwd);
|
|
80
80
|
if (!hasAnyCredential(npmrc)) {
|
|
81
81
|
console.error('gjsify trust: no npm token configured. Run `gjsify login` first.');
|
|
82
|
-
process.exit(
|
|
82
|
+
// `return` — a bare `process.exit()` is deferred under GJS and the
|
|
83
|
+
// handler would keep going without credentials.
|
|
84
|
+
return process.exit(1);
|
|
83
85
|
}
|
|
84
86
|
// Pre-flight: a present-but-DEAD token would otherwise surface as a
|
|
85
87
|
// confusing per-package `401 "You must be logged in to publish
|
|
@@ -97,13 +99,13 @@ export const trustCommand = {
|
|
|
97
99
|
console.error(`gjsify trust: npm auth check failed (${msg}).\n` +
|
|
98
100
|
'Your ~/.npmrc token is dead or revoked — refresh it with `npm login`, then re-run.\n' +
|
|
99
101
|
'(`gjsify trust` uses the ~/.npmrc bearer token directly — unlike `npm trust`, which logs in via the browser.)');
|
|
100
|
-
process.exit(1);
|
|
101
|
-
return;
|
|
102
|
+
return process.exit(1);
|
|
102
103
|
}
|
|
103
104
|
if (!who.username) {
|
|
104
105
|
console.error('gjsify trust: your npm token appears dead or revoked (registry returned an empty whoami).\n' +
|
|
105
106
|
'Refresh it with `npm login`, then re-run.');
|
|
106
|
-
|
|
107
|
+
// `return` — see the deferred-exit note on the guard above.
|
|
108
|
+
return process.exit(1);
|
|
107
109
|
}
|
|
108
110
|
console.log(`gjsify trust: authenticated as ${who.username}`);
|
|
109
111
|
}
|
|
@@ -117,11 +119,13 @@ export const trustCommand = {
|
|
|
117
119
|
if (!repository) {
|
|
118
120
|
console.error('gjsify trust: could not determine the GitHub repository.\n' +
|
|
119
121
|
'Pass --repository owner/repo (no `origin` remote to infer from).');
|
|
120
|
-
|
|
122
|
+
// `return` — see the deferred-exit note on the guard above.
|
|
123
|
+
return process.exit(1);
|
|
121
124
|
}
|
|
122
125
|
if (!validateRepository(repository)) {
|
|
123
126
|
console.error(`gjsify trust: --repository must be "owner/repo", got "${repository}".`);
|
|
124
|
-
|
|
127
|
+
// `return` — see the deferred-exit note on the guard above.
|
|
128
|
+
return process.exit(1);
|
|
125
129
|
}
|
|
126
130
|
const workflow = normalizeWorkflowFile(args.workflow);
|
|
127
131
|
const environment = args.environment;
|
|
@@ -148,7 +152,8 @@ export const trustCommand = {
|
|
|
148
152
|
}).filter((ws) => ws.name);
|
|
149
153
|
if (selected.length === 0) {
|
|
150
154
|
console.error('gjsify trust: no publishable packages matched.');
|
|
151
|
-
|
|
155
|
+
// `return` — see the deferred-exit note on the guard above.
|
|
156
|
+
return process.exit(1);
|
|
152
157
|
}
|
|
153
158
|
console.log(`gjsify trust: ${selected.length} package(s) | repo=${repository} workflow=${workflow}`);
|
|
154
159
|
if (dryRun)
|
package/lib/commands/tsc.js
CHANGED
|
@@ -104,7 +104,9 @@ export const tscCommand = {
|
|
|
104
104
|
if (!bundlePath && !nodeTscPath) {
|
|
105
105
|
console.error('gjsify tsc: neither @gjsify/tsc (GJS bundle) nor npm `typescript` (Node) is installed.');
|
|
106
106
|
console.error(' Install with: gjsify install --save-dev @gjsify/tsc (or add `typescript`).');
|
|
107
|
-
process.exit(
|
|
107
|
+
// `return` — a bare `process.exit()` is deferred under GJS and the
|
|
108
|
+
// handler would try to spawn a compiler that is not there.
|
|
109
|
+
return process.exit(1);
|
|
108
110
|
}
|
|
109
111
|
// Mirror `gjsify run`'s native-env composition so any future
|
|
110
112
|
// typescript-the-bundle code path that reaches a native bridge
|
|
@@ -46,8 +46,7 @@ export const uninstallCommand = {
|
|
|
46
46
|
if (!args.global) {
|
|
47
47
|
console.error('gjsify uninstall currently only supports --global. ' +
|
|
48
48
|
'For project-local removal, edit package.json + re-run `gjsify install`.');
|
|
49
|
-
process.exit(1);
|
|
50
|
-
return;
|
|
49
|
+
return process.exit(1);
|
|
51
50
|
}
|
|
52
51
|
const layout = defaultGlobalLayout();
|
|
53
52
|
const dryRun = args['dry-run'] ?? false;
|
package/lib/commands/whoami.js
CHANGED
|
@@ -69,7 +69,9 @@ export const whoamiCommand = {
|
|
|
69
69
|
'Then verify: gjsify whoami',
|
|
70
70
|
].join('\n') + '\n');
|
|
71
71
|
}
|
|
72
|
-
process.exit(
|
|
72
|
+
// `return` — a bare `process.exit()` is deferred under GJS and the
|
|
73
|
+
// handler would probe the registry without a token.
|
|
74
|
+
return process.exit(1);
|
|
73
75
|
}
|
|
74
76
|
let result;
|
|
75
77
|
try {
|
|
@@ -85,8 +87,9 @@ export const whoamiCommand = {
|
|
|
85
87
|
process.stderr.write(`gjsify whoami: ${message}\n`);
|
|
86
88
|
process.stderr.write(`Registry: ${registryClean}\n`);
|
|
87
89
|
}
|
|
88
|
-
process.exit
|
|
89
|
-
|
|
90
|
+
// `return process.exit` also keeps the type narrower happy: the
|
|
91
|
+
// never-typed call marks this branch as terminated.
|
|
92
|
+
return process.exit(1);
|
|
90
93
|
}
|
|
91
94
|
if (result.username && result.username.length > 0) {
|
|
92
95
|
// Branch 1 — live token.
|
package/lib/utils/build-cache.js
CHANGED
|
@@ -49,10 +49,11 @@
|
|
|
49
49
|
// or a permission error degrade to "run the script uncached" — a cache
|
|
50
50
|
// hiccup must never break a build.
|
|
51
51
|
import { createHash } from 'node:crypto';
|
|
52
|
-
import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, readSync, renameSync, rmSync,
|
|
52
|
+
import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, readSync, renameSync, rmSync, copyFileSync, writeFileSync, } from 'node:fs';
|
|
53
53
|
import { dirname, join } from 'node:path';
|
|
54
54
|
import { fileURLToPath } from 'node:url';
|
|
55
55
|
import { buildDependencyGraph } from '@gjsify/workspace';
|
|
56
|
+
import { replicateLinkSync } from './dir-link.js';
|
|
56
57
|
/**
|
|
57
58
|
* Bump to invalidate every existing cache entry (schema/semantic changes).
|
|
58
59
|
*
|
|
@@ -417,7 +418,11 @@ function copyDirRecursive(from, to) {
|
|
|
417
418
|
const dst = join(to, entry);
|
|
418
419
|
const st = lstatSync(src);
|
|
419
420
|
if (st.isSymbolicLink()) {
|
|
420
|
-
symlinkSync(readlinkSync(src), dst)
|
|
421
|
+
// Not `symlinkSync(readlinkSync(src), dst)`: replicating a link to a
|
|
422
|
+
// DIRECTORY verbatim needs a junction on Windows, and the target has
|
|
423
|
+
// to be resolved for it. See `dir-link.ts` — the one definition of
|
|
424
|
+
// that rule, added after the same mistake shipped in `dlx-cache.ts`.
|
|
425
|
+
replicateLinkSync(src, dst);
|
|
421
426
|
}
|
|
422
427
|
else if (st.isDirectory()) {
|
|
423
428
|
copyDirRecursive(src, dst);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The link type a directory link must use on this platform.
|
|
3
|
+
*
|
|
4
|
+
* `undefined` on POSIX is deliberate: Node's default is correct there, and
|
|
5
|
+
* passing a type would be noise.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DIR_LINK_TYPE: 'junction' | undefined;
|
|
8
|
+
/** True when directory links on this platform are NTFS junctions. */
|
|
9
|
+
export declare const dirLinksAreJunctions: () => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Spell the `target` argument for a link at `linkPath` pointing at `absTarget`.
|
|
12
|
+
*
|
|
13
|
+
* Absolute on Windows because a junction demands it (gotcha 1 above); relative
|
|
14
|
+
* on POSIX so the tree stays movable (gotcha 2).
|
|
15
|
+
*
|
|
16
|
+
* `linkType` is a PARAMETER, defaulting to the host's, for the same reason
|
|
17
|
+
* `detect-native-packages.ts` takes `platform`/`arch`: the Windows branch is
|
|
18
|
+
* then unit-testable from a Linux host, which is the only place anyone runs the
|
|
19
|
+
* tests. A branch nobody can exercise is how the `'dir'` bug survived to a
|
|
20
|
+
* release.
|
|
21
|
+
*
|
|
22
|
+
* @param linkPath where the link itself will live
|
|
23
|
+
* @param absTarget the directory it should point at, as an ABSOLUTE path
|
|
24
|
+
* @param linkType override the host's link type (tests only)
|
|
25
|
+
*/
|
|
26
|
+
export declare function dirLinkTarget(linkPath: string, absTarget: string, linkType?: 'junction' | undefined): string;
|
|
27
|
+
/**
|
|
28
|
+
* Link `linkPath` → `absTarget`, choosing symlink-vs-junction and the target
|
|
29
|
+
* spelling for the host.
|
|
30
|
+
*
|
|
31
|
+
* `absTarget` must be absolute; passing a relative path is the mistake gotcha 1
|
|
32
|
+
* describes, and it would only misbehave on Windows, so it is rejected here on
|
|
33
|
+
* every platform rather than left to surface on the one host nobody tests on.
|
|
34
|
+
*/
|
|
35
|
+
export declare function linkDirSync(linkPath: string, absTarget: string): void;
|
|
36
|
+
/** Promise-returning twin of {@link linkDirSync}. */
|
|
37
|
+
export declare function linkDir(linkPath: string, absTarget: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Recreate an EXISTING link at a new path, preserving what it points at.
|
|
40
|
+
*
|
|
41
|
+
* A different job from {@link linkDirSync}, and it cannot reuse it: the source
|
|
42
|
+
* link's target is whatever it already is — frequently relative, deliberately so
|
|
43
|
+
* — while a junction demands an absolute one. Copying the target verbatim is
|
|
44
|
+
* therefore right on POSIX and wrong on Windows the moment the target is a
|
|
45
|
+
* directory.
|
|
46
|
+
*
|
|
47
|
+
* So the target's KIND decides: a link to a directory becomes a junction with
|
|
48
|
+
* the target resolved against the source link's own directory (the resolution
|
|
49
|
+
* Node will not do for you — see gotcha 1 above); anything else stays a plain
|
|
50
|
+
* file symlink, which Windows allows unprivileged.
|
|
51
|
+
*
|
|
52
|
+
* A dangling source link keeps the verbatim target: it points nowhere either
|
|
53
|
+
* way, and guessing a kind for it would turn a faithful copy into a fabrication.
|
|
54
|
+
*
|
|
55
|
+
* Primitive fs ops only — this runs under the GJS facade as well as Node, which
|
|
56
|
+
* is why there is no `fs.cp`/`opendir` here.
|
|
57
|
+
*/
|
|
58
|
+
export declare function replicateLinkSync(srcLink: string, dstLink: string): void;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Creating a link to a DIRECTORY, portably.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS FILE EXISTS. Windows cannot create a directory *symlink* without
|
|
4
|
+
// elevation or Developer Mode — the call fails with `EPERM` — but any user can
|
|
5
|
+
// create an NTFS **junction**, which is why npm, yarn and pnpm all use junctions
|
|
6
|
+
// for directory links. `commands/install.ts` learned that and encoded it, twice,
|
|
7
|
+
// as a local constant for its workspace links. Nothing shared it, so the next
|
|
8
|
+
// site to link a directory got it wrong: `dlx-cache.ts` linked with `'dir'` and
|
|
9
|
+
// every non-elevated Windows user hit
|
|
10
|
+
//
|
|
11
|
+
// Could not resolve showcase "canvas2d-fireworks": EPERM: operation not
|
|
12
|
+
// permitted, symlink '…\<prepare-dir>' -> '…\pkg.tmp-…'
|
|
13
|
+
//
|
|
14
|
+
// on a plain `npx @gjsify/cli@latest showcase …`. The knowledge existed in the
|
|
15
|
+
// repo and the bug shipped anyway, which is the argument for a single callable
|
|
16
|
+
// definition rather than a convention.
|
|
17
|
+
//
|
|
18
|
+
// THE TWO GOTCHAS, both load-bearing:
|
|
19
|
+
//
|
|
20
|
+
// 1. **A junction target must be ABSOLUTE.** Node normalises a `'junction'`
|
|
21
|
+
// target with `path.resolve()` — against the process CWD, NOT against the
|
|
22
|
+
// link's own directory. A relative target that looks right produces a
|
|
23
|
+
// junction pointing somewhere else entirely, and only on Windows.
|
|
24
|
+
// 2. **POSIX targets should stay RELATIVE** where the caller wants a movable
|
|
25
|
+
// tree: a relative symlink survives the whole tree being moved or mounted
|
|
26
|
+
// elsewhere. So the target spelling is per-platform, not one string.
|
|
27
|
+
//
|
|
28
|
+
// Autodetection is NOT a substitute. Node picks `'file'` or `'dir'` when the
|
|
29
|
+
// type argument is omitted — never `'junction'` — so an omitted type is the
|
|
30
|
+
// failing case, not the safe default.
|
|
31
|
+
import { readlinkSync, statSync, symlinkSync } from 'node:fs';
|
|
32
|
+
import { symlink } from 'node:fs/promises';
|
|
33
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
34
|
+
/**
|
|
35
|
+
* The link type a directory link must use on this platform.
|
|
36
|
+
*
|
|
37
|
+
* `undefined` on POSIX is deliberate: Node's default is correct there, and
|
|
38
|
+
* passing a type would be noise.
|
|
39
|
+
*/
|
|
40
|
+
export const DIR_LINK_TYPE = process.platform === 'win32' ? 'junction' : undefined;
|
|
41
|
+
/** True when directory links on this platform are NTFS junctions. */
|
|
42
|
+
export const dirLinksAreJunctions = () => DIR_LINK_TYPE === 'junction';
|
|
43
|
+
/**
|
|
44
|
+
* Spell the `target` argument for a link at `linkPath` pointing at `absTarget`.
|
|
45
|
+
*
|
|
46
|
+
* Absolute on Windows because a junction demands it (gotcha 1 above); relative
|
|
47
|
+
* on POSIX so the tree stays movable (gotcha 2).
|
|
48
|
+
*
|
|
49
|
+
* `linkType` is a PARAMETER, defaulting to the host's, for the same reason
|
|
50
|
+
* `detect-native-packages.ts` takes `platform`/`arch`: the Windows branch is
|
|
51
|
+
* then unit-testable from a Linux host, which is the only place anyone runs the
|
|
52
|
+
* tests. A branch nobody can exercise is how the `'dir'` bug survived to a
|
|
53
|
+
* release.
|
|
54
|
+
*
|
|
55
|
+
* @param linkPath where the link itself will live
|
|
56
|
+
* @param absTarget the directory it should point at, as an ABSOLUTE path
|
|
57
|
+
* @param linkType override the host's link type (tests only)
|
|
58
|
+
*/
|
|
59
|
+
export function dirLinkTarget(linkPath, absTarget, linkType = DIR_LINK_TYPE) {
|
|
60
|
+
return linkType === 'junction' ? resolve(absTarget) : relative(dirname(linkPath), absTarget);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Link `linkPath` → `absTarget`, choosing symlink-vs-junction and the target
|
|
64
|
+
* spelling for the host.
|
|
65
|
+
*
|
|
66
|
+
* `absTarget` must be absolute; passing a relative path is the mistake gotcha 1
|
|
67
|
+
* describes, and it would only misbehave on Windows, so it is rejected here on
|
|
68
|
+
* every platform rather than left to surface on the one host nobody tests on.
|
|
69
|
+
*/
|
|
70
|
+
export function linkDirSync(linkPath, absTarget) {
|
|
71
|
+
assertAbsolute(absTarget);
|
|
72
|
+
symlinkSync(dirLinkTarget(linkPath, absTarget), linkPath, DIR_LINK_TYPE);
|
|
73
|
+
}
|
|
74
|
+
/** Promise-returning twin of {@link linkDirSync}. */
|
|
75
|
+
export async function linkDir(linkPath, absTarget) {
|
|
76
|
+
assertAbsolute(absTarget);
|
|
77
|
+
await symlink(dirLinkTarget(linkPath, absTarget), linkPath, DIR_LINK_TYPE);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Recreate an EXISTING link at a new path, preserving what it points at.
|
|
81
|
+
*
|
|
82
|
+
* A different job from {@link linkDirSync}, and it cannot reuse it: the source
|
|
83
|
+
* link's target is whatever it already is — frequently relative, deliberately so
|
|
84
|
+
* — while a junction demands an absolute one. Copying the target verbatim is
|
|
85
|
+
* therefore right on POSIX and wrong on Windows the moment the target is a
|
|
86
|
+
* directory.
|
|
87
|
+
*
|
|
88
|
+
* So the target's KIND decides: a link to a directory becomes a junction with
|
|
89
|
+
* the target resolved against the source link's own directory (the resolution
|
|
90
|
+
* Node will not do for you — see gotcha 1 above); anything else stays a plain
|
|
91
|
+
* file symlink, which Windows allows unprivileged.
|
|
92
|
+
*
|
|
93
|
+
* A dangling source link keeps the verbatim target: it points nowhere either
|
|
94
|
+
* way, and guessing a kind for it would turn a faithful copy into a fabrication.
|
|
95
|
+
*
|
|
96
|
+
* Primitive fs ops only — this runs under the GJS facade as well as Node, which
|
|
97
|
+
* is why there is no `fs.cp`/`opendir` here.
|
|
98
|
+
*/
|
|
99
|
+
export function replicateLinkSync(srcLink, dstLink) {
|
|
100
|
+
const rawTarget = readlinkSync(srcLink);
|
|
101
|
+
if (!dirLinksAreJunctions()) {
|
|
102
|
+
symlinkSync(rawTarget, dstLink);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const resolvedTarget = resolve(dirname(srcLink), rawTarget);
|
|
106
|
+
let targetIsDir;
|
|
107
|
+
try {
|
|
108
|
+
targetIsDir = statSync(resolvedTarget).isDirectory();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Dangling — copy it as-is rather than inventing a kind for it.
|
|
112
|
+
symlinkSync(rawTarget, dstLink);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (targetIsDir)
|
|
116
|
+
symlinkSync(resolvedTarget, dstLink, 'junction');
|
|
117
|
+
else
|
|
118
|
+
symlinkSync(rawTarget, dstLink, 'file');
|
|
119
|
+
}
|
|
120
|
+
function assertAbsolute(absTarget) {
|
|
121
|
+
if (resolve(absTarget) !== absTarget) {
|
|
122
|
+
throw new TypeError(`linkDir: target must be an absolute path, got ${JSON.stringify(absTarget)}. ` +
|
|
123
|
+
'Node resolves a Windows junction target against the process CWD rather than the ' +
|
|
124
|
+
"link's own directory, so a relative target silently points somewhere else there.");
|
|
125
|
+
}
|
|
126
|
+
}
|