@gjsify/cli 0.40.0 → 0.41.0
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 +123 -119
- package/lib/cli-app.js +2 -1
- package/lib/commands/create.d.ts +3 -0
- package/lib/commands/create.js +49 -6
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/install.d.ts +1 -0
- package/lib/commands/install.js +32 -1
- package/lib/commands/prune.d.ts +11 -0
- package/lib/commands/prune.js +84 -0
- package/lib/commands/self-update.d.ts +1 -0
- package/lib/commands/self-update.js +55 -0
- package/lib/templates/install.mjs.tmpl +8 -3
- package/lib/utils/check-system-deps.d.ts +2 -2
- package/lib/utils/check-system-deps.js +43 -6
- package/lib/utils/install-provenance.d.ts +44 -0
- package/lib/utils/install-provenance.js +106 -0
- package/lib/utils/prune-prefix.d.ts +83 -0
- package/lib/utils/prune-prefix.js +333 -0
- package/lib/utils/ship/depends.js +19 -0
- package/package.json +26 -18
package/lib/cli-app.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import yargs from 'yargs';
|
|
19
19
|
import { classifyCliFailure } from './cli-fail.js';
|
|
20
20
|
import { cliVersion } from './utils/publish-headers.js';
|
|
21
|
-
import { buildCommand as build, clearCommand as clear, copyCommand as copy, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
|
|
21
|
+
import { buildCommand as build, clearCommand as clear, copyCommand as copy, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
|
|
22
22
|
import { APP_NAME } from './constants.js';
|
|
23
23
|
import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
24
24
|
import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
|
|
@@ -120,6 +120,7 @@ export async function runCli(argv) {
|
|
|
120
120
|
.command(selfUpdate.command, selfUpdate.description, selfUpdate.builder, selfUpdate.handler)
|
|
121
121
|
.command(generateInstaller.command, generateInstaller.description, generateInstaller.builder, generateInstaller.handler)
|
|
122
122
|
.command(uninstall.command, uninstall.description, uninstall.builder, uninstall.handler)
|
|
123
|
+
.command(prune.command, prune.description, prune.builder, prune.handler)
|
|
123
124
|
.command(upgrade.command, upgrade.description, upgrade.builder, upgrade.handler)
|
|
124
125
|
.command(format.command, format.description, format.builder, format.handler)
|
|
125
126
|
.command(lint.command, lint.description, lint.builder, lint.handler)
|
package/lib/commands/create.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import type { PackageManager } from '@gjsify/create-app';
|
|
1
2
|
import type { Command } from '../types/index.js';
|
|
2
3
|
interface CreateOptions {
|
|
3
4
|
'project-name': string;
|
|
4
5
|
template?: string;
|
|
6
|
+
runtime?: string;
|
|
7
|
+
'package-manager'?: PackageManager;
|
|
5
8
|
force: boolean;
|
|
6
9
|
install: boolean;
|
|
7
10
|
}
|
package/lib/commands/create.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { createProject, discoverTemplates } from '@gjsify/create-app';
|
|
2
|
-
import { promptTemplate } from '@gjsify/create-app/prompt';
|
|
1
|
+
import { createProject, discoverTemplates, PACKAGE_MANAGERS, supportedRuntimes } from '@gjsify/create-app';
|
|
2
|
+
import { isCancelled, promptSetup, promptTemplate } from '@gjsify/create-app/prompt';
|
|
3
3
|
export const createCommand = {
|
|
4
4
|
command: 'create [project-name]',
|
|
5
5
|
description: 'Scaffold a new Gjsify project in a new directory.',
|
|
6
6
|
builder: (yargs) => {
|
|
7
7
|
const templates = discoverTemplates();
|
|
8
8
|
const templateChoices = templates.map((t) => t.name);
|
|
9
|
+
// DERIVED from the shipped templates, never a hand-written list:
|
|
10
|
+
// accepting `--runtime deno` is a claim that some template runs there,
|
|
11
|
+
// and only the templates can make it.
|
|
12
|
+
const runtimeChoices = [...new Set(templates.flatMap((t) => supportedRuntimes(t)))];
|
|
9
13
|
return yargs
|
|
10
14
|
.positional('project-name', {
|
|
11
15
|
describe: 'Name of the project directory to create',
|
|
@@ -17,6 +21,12 @@ export const createCommand = {
|
|
|
17
21
|
describe: 'Template to scaffold from',
|
|
18
22
|
type: 'string',
|
|
19
23
|
choices: templateChoices.length > 0 ? templateChoices : undefined,
|
|
24
|
+
})
|
|
25
|
+
.option('runtime', {
|
|
26
|
+
alias: 'r',
|
|
27
|
+
describe: 'Runtime to set the project up for. Decides which package managers are offered and which start script the next steps name.',
|
|
28
|
+
type: 'string',
|
|
29
|
+
choices: runtimeChoices.length > 0 ? runtimeChoices : undefined,
|
|
20
30
|
})
|
|
21
31
|
.option('force', {
|
|
22
32
|
alias: 'f',
|
|
@@ -25,30 +35,63 @@ export const createCommand = {
|
|
|
25
35
|
default: false,
|
|
26
36
|
})
|
|
27
37
|
.option('install', {
|
|
28
|
-
describe: '
|
|
38
|
+
describe: 'Install dependencies after scaffolding',
|
|
29
39
|
type: 'boolean',
|
|
30
40
|
default: false,
|
|
41
|
+
})
|
|
42
|
+
.option('package-manager', {
|
|
43
|
+
alias: 'p',
|
|
44
|
+
describe: 'Package manager to install with, and to name in the printed next steps. Must be one the chosen runtime can install for. `gjsify` is the only one that works on a host with no Node.js.',
|
|
45
|
+
type: 'string',
|
|
46
|
+
choices: PACKAGE_MANAGERS,
|
|
31
47
|
});
|
|
32
48
|
},
|
|
33
49
|
handler: async (args) => {
|
|
50
|
+
const interactive = Boolean(process.stdin.isTTY);
|
|
51
|
+
const templates = discoverTemplates();
|
|
34
52
|
let template = args.template;
|
|
35
53
|
if (!template) {
|
|
36
|
-
|
|
37
|
-
if (!process.stdin.isTTY) {
|
|
54
|
+
if (!interactive) {
|
|
38
55
|
const list = templates.map((t) => t.name).join(', ');
|
|
39
56
|
console.error(`Error: --template is required in non-interactive mode. Available templates: ${list || '(none)'}`);
|
|
40
57
|
// `return` — the deferred GJS exit otherwise fell through into
|
|
41
58
|
// the interactive template prompt on a non-TTY.
|
|
42
59
|
return process.exit(1);
|
|
43
60
|
}
|
|
44
|
-
|
|
61
|
+
// Ctrl+C is an abort, not a crash.
|
|
62
|
+
const picked = await promptTemplate(templates).catch((error) => {
|
|
63
|
+
if (!isCancelled(error))
|
|
64
|
+
throw error;
|
|
65
|
+
console.error('Cancelled.');
|
|
66
|
+
return undefined;
|
|
67
|
+
});
|
|
68
|
+
if (picked === undefined)
|
|
69
|
+
return process.exit(1);
|
|
45
70
|
template = picked.name;
|
|
46
71
|
}
|
|
72
|
+
// The SAME decision layer `npm create @gjsify/app` runs, not a second
|
|
73
|
+
// one. Wired late: this command used to pass neither runtime nor package
|
|
74
|
+
// manager, so `createProject` fell back to the host runtime and the first
|
|
75
|
+
// manager for it — meaning `gjsify create --install` on a Node host ran
|
|
76
|
+
// `npm install` without ever saying so, the one default that reaches the
|
|
77
|
+
// user's disk and the one the standalone bin refuses to guess.
|
|
78
|
+
const info = templates.find((t) => t.name === template);
|
|
79
|
+
const setup = await promptSetup({
|
|
80
|
+
offered: info ? supportedRuntimes(info) : [],
|
|
81
|
+
runtime: args.runtime,
|
|
82
|
+
packageManager: args['package-manager'],
|
|
83
|
+
install: args.install,
|
|
84
|
+
interactive,
|
|
85
|
+
});
|
|
86
|
+
if (setup === undefined)
|
|
87
|
+
return process.exit(1);
|
|
47
88
|
await createProject({
|
|
48
89
|
projectName: args['project-name'],
|
|
49
90
|
template,
|
|
91
|
+
runtime: setup.runtime,
|
|
50
92
|
force: args.force,
|
|
51
93
|
install: args.install,
|
|
94
|
+
packageManager: setup.packageManager,
|
|
52
95
|
});
|
|
53
96
|
},
|
|
54
97
|
};
|
package/lib/commands/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export * from './trust.js';
|
|
|
26
26
|
export * from './onboard.js';
|
|
27
27
|
export * from './self-update.js';
|
|
28
28
|
export * from './generate-installer.js';
|
|
29
|
+
export * from './prune.js';
|
|
29
30
|
export * from './uninstall.js';
|
|
30
31
|
export * from './format.js';
|
|
31
32
|
export * from './lint.js';
|
package/lib/commands/index.js
CHANGED
|
@@ -26,6 +26,7 @@ export * from './trust.js';
|
|
|
26
26
|
export * from './onboard.js';
|
|
27
27
|
export * from './self-update.js';
|
|
28
28
|
export * from './generate-installer.js';
|
|
29
|
+
export * from './prune.js';
|
|
29
30
|
export * from './uninstall.js';
|
|
30
31
|
export * from './format.js';
|
|
31
32
|
export * from './lint.js';
|
package/lib/commands/install.js
CHANGED
|
@@ -32,6 +32,8 @@ import { nodeBinary } from '../utils/run-node.js';
|
|
|
32
32
|
import { spawnToCompletion } from '../utils/spawn.js';
|
|
33
33
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
34
34
|
import { binDirOnPath, BUNDLER_ENGINE_PACKAGE, defaultGlobalLayout, hasBundlerEngineInstalled, installGjsEnginePackages, linkGlobalBins, specToPackageName, } from '../utils/install-global.js';
|
|
35
|
+
import { resolveHostPlatform } from '../utils/platform-check.js';
|
|
36
|
+
import { pruneAfterInstall } from '../utils/prune-prefix.js';
|
|
35
37
|
import { addDependencyEntry, defaultRangeFromVersion, parseSpec, projectSpecsFromPackageJson, readPackageJson, writePackageJson, } from '../utils/pkg-json-edit.js';
|
|
36
38
|
/**
|
|
37
39
|
* Link type for the workspace↔workspace directory links.
|
|
@@ -125,6 +127,11 @@ export const installCommand = {
|
|
|
125
127
|
description: 'Install a REQUIRED dependency even when its os/cpu/libc excludes the target, instead of failing with EBADPLATFORM (npm config key `force`). Incompatible OPTIONAL dependencies stay skipped — npm ignores --force for those too, and a binary that cannot load is not worth downloading.',
|
|
126
128
|
type: 'boolean',
|
|
127
129
|
default: false,
|
|
130
|
+
})
|
|
131
|
+
.option('prune', {
|
|
132
|
+
description: 'After installing, remove packages an earlier install left behind that this host cannot use (foreign os/cpu/libc). Use --no-prune to disable. Skipped automatically under --immutable, and whenever --os/--cpu/--libc is given: an install must never delete against a target the user typed.',
|
|
133
|
+
type: 'boolean',
|
|
134
|
+
default: true,
|
|
128
135
|
}),
|
|
129
136
|
handler: async (args) => {
|
|
130
137
|
// Must run before anything resolves — `--os/--cpu/--libc/--force` are npm
|
|
@@ -161,7 +168,7 @@ export const installCommand = {
|
|
|
161
168
|
console.warn(`gjsify install --global ignores --${flag}: global installs do not modify a project package.json.`);
|
|
162
169
|
}
|
|
163
170
|
}
|
|
164
|
-
await installGlobalAndLink(args.packages, { verbose: args.verbose });
|
|
171
|
+
await installGlobalAndLink(args.packages, { verbose: args.verbose, prune: args.prune });
|
|
165
172
|
return;
|
|
166
173
|
}
|
|
167
174
|
// Before the heavy install, so the skew is visible even if a later phase
|
|
@@ -170,6 +177,7 @@ export const installCommand = {
|
|
|
170
177
|
const backend = args.backend ?? process.env.GJSIFY_INSTALL_BACKEND ?? 'native';
|
|
171
178
|
if (backend === 'npm') {
|
|
172
179
|
await projectInstallViaNpm(args);
|
|
180
|
+
pruneProjectPrefix(args);
|
|
173
181
|
await runPostInstallChecks(args);
|
|
174
182
|
return;
|
|
175
183
|
}
|
|
@@ -217,6 +225,7 @@ export const installCommand = {
|
|
|
217
225
|
hardExitTimerId?.unref?.();
|
|
218
226
|
try {
|
|
219
227
|
await projectInstallNative(args, overallController?.signal);
|
|
228
|
+
pruneProjectPrefix(args);
|
|
220
229
|
await runPostInstallChecks(args);
|
|
221
230
|
}
|
|
222
231
|
catch (err) {
|
|
@@ -1198,6 +1207,14 @@ async function installGlobalAndLink(specs, opts) {
|
|
|
1198
1207
|
const cliVersion = result.installed.find((r) => r.name === '@gjsify/cli')?.version ?? 'latest';
|
|
1199
1208
|
await installGjsEnginePackages(layout.prefix, cliVersion, { verbose: opts.verbose });
|
|
1200
1209
|
}
|
|
1210
|
+
// Before the LINK on purpose: `linkGlobalBins`' launchers bake the resolved
|
|
1211
|
+
// prebuild directories into GI_TYPELIB_PATH/LD_LIBRARY_PATH, and must not name
|
|
1212
|
+
// a directory this pass is about to delete.
|
|
1213
|
+
if (opts.prune !== false) {
|
|
1214
|
+
pruneAfterInstall(layout.prefix, resolveHostPlatform({ env: {} }), {
|
|
1215
|
+
hint: 'gjsify prune -g --dry-run',
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1201
1218
|
const created = linkGlobalBins(packageNames, layout);
|
|
1202
1219
|
reportLinkedBins(created, layout.binDir);
|
|
1203
1220
|
}
|
|
@@ -1205,6 +1222,20 @@ async function installGlobalAndLink(specs, opts) {
|
|
|
1205
1222
|
lock.release();
|
|
1206
1223
|
}
|
|
1207
1224
|
}
|
|
1225
|
+
/**
|
|
1226
|
+
* The automatic pass for a PROJECT prefix.
|
|
1227
|
+
*
|
|
1228
|
+
* Hooked at the command level rather than inside `installPackages`, so `dlx`'s
|
|
1229
|
+
* throwaway cache prefix — created, used once and dropped — pays nothing for it.
|
|
1230
|
+
*/
|
|
1231
|
+
function pruneProjectPrefix(args) {
|
|
1232
|
+
if (args.prune === false)
|
|
1233
|
+
return;
|
|
1234
|
+
pruneAfterInstall(process.cwd(), resolveHostPlatform({ env: {} }), {
|
|
1235
|
+
immutable: args.immutable === true,
|
|
1236
|
+
hint: 'gjsify prune --dry-run',
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1208
1239
|
function reportLinkedBins(created, binDir) {
|
|
1209
1240
|
if (created.length === 0) {
|
|
1210
1241
|
console.warn('\nNo bins declared (neither `gjsify.bin` nor `bin` in package.json) — nothing was symlinked.');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Command } from '../types/index.js';
|
|
2
|
+
interface PruneOptions {
|
|
3
|
+
global: boolean;
|
|
4
|
+
dryRun: boolean;
|
|
5
|
+
verbose: boolean;
|
|
6
|
+
os?: string;
|
|
7
|
+
cpu?: string;
|
|
8
|
+
libc?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const pruneCommand: Command<unknown, PruneOptions>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// `gjsify prune` — remove installed packages this target cannot use.
|
|
2
|
+
//
|
|
3
|
+
// The repair half of ADR 0025. `install`/`self-update` prune what they would not
|
|
4
|
+
// have placed, but only going forward: a prefix that accreted foreign-platform
|
|
5
|
+
// packages under an older CLI is only reachable by asking. That is this command,
|
|
6
|
+
// and it is why it exists at all rather than the automatic pass alone.
|
|
7
|
+
//
|
|
8
|
+
// `--os/--cpu/--libc` are accepted here and deliberately NOT honoured by the
|
|
9
|
+
// automatic pass: typed on `prune` they are a request ("show me what a darwin-arm64
|
|
10
|
+
// host could not use"), while inherited by an install they would delete the host's
|
|
11
|
+
// own packages. See `automaticPruneRefusal`.
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { acquireInstallLock } from '../utils/install-lock.js';
|
|
14
|
+
import { defaultGlobalLayout } from '../utils/install-global.js';
|
|
15
|
+
import { resolveHostPlatform } from '../utils/platform-check.js';
|
|
16
|
+
import { executePrune, formatPruneReport, planPrune } from '../utils/prune-prefix.js';
|
|
17
|
+
export const pruneCommand = {
|
|
18
|
+
command: 'prune',
|
|
19
|
+
description: 'Remove installed packages this host cannot use (foreign os/cpu/libc). ' +
|
|
20
|
+
'Reads each package manifest — a package that declares no platform is never touched.',
|
|
21
|
+
builder: (yargs) => yargs
|
|
22
|
+
.option('global', {
|
|
23
|
+
description: 'Prune the user-global XDG prefix (the `install -g` target) instead of the project.',
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
alias: 'g',
|
|
26
|
+
default: false,
|
|
27
|
+
})
|
|
28
|
+
.option('dry-run', {
|
|
29
|
+
description: 'Report what would be removed without touching the filesystem.',
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
default: false,
|
|
32
|
+
})
|
|
33
|
+
.option('verbose', {
|
|
34
|
+
description: 'List every package rather than the first few.',
|
|
35
|
+
type: 'boolean',
|
|
36
|
+
default: false,
|
|
37
|
+
})
|
|
38
|
+
.option('os', {
|
|
39
|
+
description: 'Prune AS IF the host were this OS (linux, darwin, win32). Defaults to this host.',
|
|
40
|
+
type: 'string',
|
|
41
|
+
})
|
|
42
|
+
.option('cpu', {
|
|
43
|
+
description: 'Prune AS IF the host were this architecture (x64, arm64, …). Defaults to this host.',
|
|
44
|
+
type: 'string',
|
|
45
|
+
})
|
|
46
|
+
.option('libc', {
|
|
47
|
+
description: 'Prune AS IF the host used this libc (glibc, musl). Defaults to this host.',
|
|
48
|
+
type: 'string',
|
|
49
|
+
}),
|
|
50
|
+
handler: async (args) => {
|
|
51
|
+
const layout = args.global ? defaultGlobalLayout() : null;
|
|
52
|
+
const prefix = layout ? layout.prefix : process.cwd();
|
|
53
|
+
if (!existsSync(`${prefix}/node_modules`)) {
|
|
54
|
+
console.log(`gjsify prune: nothing installed at ${prefix}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// The flags are read HERE rather than through the npm config keys, so the
|
|
58
|
+
// automatic pass — which refuses to act on an overridden target — cannot be
|
|
59
|
+
// reached by way of this command's own options.
|
|
60
|
+
const target = resolveHostPlatform({
|
|
61
|
+
env: {
|
|
62
|
+
...(args.os ? { npm_config_os: args.os } : {}),
|
|
63
|
+
...(args.cpu ? { npm_config_cpu: args.cpu } : {}),
|
|
64
|
+
...(args.libc ? { npm_config_libc: args.libc } : {}),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// A prune racing an install's extract would delete a half-written tree. The
|
|
68
|
+
// lock is the same one every writer of this prefix takes (ADR 0001).
|
|
69
|
+
const lock = await acquireInstallLock(prefix, {});
|
|
70
|
+
let result;
|
|
71
|
+
try {
|
|
72
|
+
result = executePrune(planPrune({ prefix, target }), { dryRun: args.dryRun });
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
lock.release();
|
|
76
|
+
}
|
|
77
|
+
console.log(formatPruneReport(result, { dryRun: args.dryRun, verbose: args.verbose }));
|
|
78
|
+
// Removing nothing is a SUCCESS — idempotent housekeeping, unlike `uninstall`
|
|
79
|
+
// where naming a package that is not there is a mistake worth reporting. Only
|
|
80
|
+
// a removal the user asked for and did not get is a failure.
|
|
81
|
+
if (result.failed.length > 0)
|
|
82
|
+
return process.exit(1);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -25,7 +25,10 @@ import { dirname, join, posix, resolve, win32 } from 'node:path';
|
|
|
25
25
|
import { fileURLToPath } from 'node:url';
|
|
26
26
|
import { fetchPackument } from '@gjsify/npm-registry';
|
|
27
27
|
import { installPackages, makeProgressReporter } from '../utils/install-backend.js';
|
|
28
|
+
import { classifyInstall } from '../utils/install-provenance.js';
|
|
28
29
|
import { defaultGlobalLayout, globalLayoutIsDefault, hasBundlerEngineInstalled, installGjsEnginePackages, linkGlobalBins, resolveBinOnPath, } from '../utils/install-global.js';
|
|
30
|
+
import { resolveHostPlatform } from '../utils/platform-check.js';
|
|
31
|
+
import { pruneAfterInstall } from '../utils/prune-prefix.js';
|
|
29
32
|
const PACKAGE_NAME = '@gjsify/cli';
|
|
30
33
|
export const selfUpdateCommand = {
|
|
31
34
|
command: 'self-update',
|
|
@@ -45,6 +48,11 @@ export const selfUpdateCommand = {
|
|
|
45
48
|
description: 'npm dist-tag or pinned version to install (e.g. `latest`, `next`, `0.5.0`).',
|
|
46
49
|
type: 'string',
|
|
47
50
|
default: 'latest',
|
|
51
|
+
})
|
|
52
|
+
.option('prune', {
|
|
53
|
+
description: 'After updating, remove packages an earlier install left behind that this host cannot use (foreign os/cpu/libc). Use --no-prune to disable.',
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
default: true,
|
|
48
56
|
})
|
|
49
57
|
.option('skip-deps', {
|
|
50
58
|
description: 'Update only the @gjsify/cli bundle itself; do not pull its runtime dependencies ' +
|
|
@@ -60,6 +68,24 @@ export const selfUpdateCommand = {
|
|
|
60
68
|
const currentVersion = readCurrentVersion();
|
|
61
69
|
const installedAtPrefix = existsSync(installedPkgJson);
|
|
62
70
|
console.log(`Current ${PACKAGE_NAME}: v${currentVersion ?? '(unknown)'}`);
|
|
71
|
+
// REFUSE, do not warn-and-continue, when something else owns this install.
|
|
72
|
+
//
|
|
73
|
+
// Everything below writes the user-global XDG prefix. Under a `.deb`,
|
|
74
|
+
// `.rpm` or Flatpak that produces a SECOND copy which shadows the one the
|
|
75
|
+
// package manager tracks — so the next `apt upgrade` reverts it, or the two
|
|
76
|
+
// diverge, and the user's report is "I updated and nothing changed". A
|
|
77
|
+
// clean refusal naming the right command is strictly more useful than a
|
|
78
|
+
// successful write to the wrong place; #1064 is the same lesson one layer
|
|
79
|
+
// down, where the write succeeded and PATH still resolved the old binary.
|
|
80
|
+
const provenance = classifyInstall({ selfDir: readSelfDir(), xdgPrefix: layout.prefix });
|
|
81
|
+
if (provenance.managedElsewhere) {
|
|
82
|
+
console.error(`\ngjsify self-update: this gjsify is managed by something else — ${provenance.evidence}.\n` +
|
|
83
|
+
`Updating it from here would write ${layout.prefix} and leave you running two installs.\n\n` +
|
|
84
|
+
`Update with: ${provenance.updateWith}\n`);
|
|
85
|
+
// `return process.exit()` — a bare `process.exit()` is DEFERRED under
|
|
86
|
+
// GJS and the handler would run on into the install it just refused.
|
|
87
|
+
return process.exit(1);
|
|
88
|
+
}
|
|
63
89
|
if (!installedAtPrefix) {
|
|
64
90
|
console.warn(`\nWarning: no @gjsify/cli install found under ${layout.prefix}.\n` +
|
|
65
91
|
`self-update only manages installs created by install.mjs or \`gjsify install -g\`.\n` +
|
|
@@ -157,6 +183,13 @@ export const selfUpdateCommand = {
|
|
|
157
183
|
if (pullDeps) {
|
|
158
184
|
await installGjsEnginePackages(layout.prefix, target, { verbose: false });
|
|
159
185
|
}
|
|
186
|
+
// Same ordering reason as `install -g`: before the LINK, whose launchers bake
|
|
187
|
+
// the resolved prebuild directories into their env.
|
|
188
|
+
if (args.prune !== false) {
|
|
189
|
+
pruneAfterInstall(layout.prefix, resolveHostPlatform({ env: {} }), {
|
|
190
|
+
hint: 'gjsify prune -g --dry-run',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
160
193
|
const linked = linkGlobalBins([PACKAGE_NAME], layout);
|
|
161
194
|
if (linked.length === 0) {
|
|
162
195
|
console.warn('self-update: install completed but no bins were linked — package.json may be missing a `bin` field.');
|
|
@@ -246,6 +279,28 @@ export function verifyPathResolution(opts) {
|
|
|
246
279
|
* `import.meta.url` (the bundle file under GJS, or `lib/index.js` under
|
|
247
280
|
* Node) until it finds a package.json with `name === '@gjsify/cli'`.
|
|
248
281
|
*/
|
|
282
|
+
/**
|
|
283
|
+
* The directory of the running CLI's own package — the input the provenance
|
|
284
|
+
* question is actually about.
|
|
285
|
+
*
|
|
286
|
+
* The same upward walk `readCurrentVersion()` does, and for the same reason: it
|
|
287
|
+
* works from `lib/index.js` under Node and from the published `dist/cli.gjs.mjs`
|
|
288
|
+
* bundle under GJS. Honours the same `GJSIFY_CLI_PACKAGE_JSON` escape hatch, so a
|
|
289
|
+
* test can place a synthetic install anywhere without a real one.
|
|
290
|
+
*/
|
|
291
|
+
function readSelfDir() {
|
|
292
|
+
const override = process.env.GJSIFY_CLI_PACKAGE_JSON;
|
|
293
|
+
if (override)
|
|
294
|
+
return dirname(resolve(override));
|
|
295
|
+
const here = fileURLToPath(import.meta.url);
|
|
296
|
+
let dir = dirname(resolve(here));
|
|
297
|
+
for (let i = 0; i < 8 && dir !== dirname(dir); i++) {
|
|
298
|
+
if (existsSync(join(dir, 'package.json')))
|
|
299
|
+
return dir;
|
|
300
|
+
dir = dirname(dir);
|
|
301
|
+
}
|
|
302
|
+
return dirname(resolve(here));
|
|
303
|
+
}
|
|
249
304
|
function readCurrentVersion() {
|
|
250
305
|
try {
|
|
251
306
|
// Escape hatch for tests: point GJSIFY_CLI_PACKAGE_JSON at a synthetic
|
|
@@ -188,9 +188,14 @@ function checkGjsVersion() {
|
|
|
188
188
|
if (major < 1 || (major === 1 && minor < 86)) {
|
|
189
189
|
error(`gjs ${major}.${minor} is too old — gjsify requires gjs 1.86 or newer.`);
|
|
190
190
|
error('Install hints:');
|
|
191
|
-
error(' Fedora 43+:
|
|
192
|
-
error('
|
|
193
|
-
|
|
191
|
+
error(' Fedora 43+: sudo dnf install gjs');
|
|
192
|
+
error(' Arch: sudo pacman -S gjs');
|
|
193
|
+
// NOT "Debian 13+". Measured 2026-08-16 against tracker.debian.org: trixie
|
|
194
|
+
// (13, stable) ships 1.82.3, forky (testing) 1.88.1, sid 1.89.2 — 1.84 and
|
|
195
|
+
// 1.86 were skipped entirely. Sending a trixie user to `apt install gjs`
|
|
196
|
+
// hands them the very version this branch just rejected, from the branch
|
|
197
|
+
// that rejected it. Same fact, same wording as `utils/ship/depends.ts`.
|
|
198
|
+
error(' Debian forky/sid: sudo apt install gjs (trixie ships 1.82.3 — too old)');
|
|
194
199
|
exit(1);
|
|
195
200
|
}
|
|
196
201
|
}
|
|
@@ -126,8 +126,8 @@ export declare const PACKAGE_DEPS: Record<string, string[]>;
|
|
|
126
126
|
*/
|
|
127
127
|
export declare function runAllChecks(cwd: string): DepCheck[];
|
|
128
128
|
/**
|
|
129
|
-
* Minimal checks needed to run any GJS example (Node
|
|
130
|
-
* Used by `gjsify showcase` for examples that have no native deps.
|
|
129
|
+
* Minimal checks needed to run any GJS example (the GJS binary; Node is reported but
|
|
130
|
+
* not required). Used by `gjsify showcase` for examples that have no native deps.
|
|
131
131
|
*/
|
|
132
132
|
export declare function runMinimalChecks(): DepCheck[];
|
|
133
133
|
/**
|
|
@@ -18,6 +18,7 @@ import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
|
18
18
|
// The zero-dependency subpath, deliberately: importing the plugin root would
|
|
19
19
|
// pull vite/execa/minify-xml into a system CHECK.
|
|
20
20
|
import { resolveBlueprintCompiler } from '@gjsify/vite-plugin-blueprint/resolve';
|
|
21
|
+
import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
21
22
|
/** Run a binary and return its stdout trimmed, or null if it fails. */
|
|
22
23
|
function tryExecFile(binary, args) {
|
|
23
24
|
try {
|
|
@@ -378,13 +379,41 @@ export function runAllChecks(cwd) {
|
|
|
378
379
|
];
|
|
379
380
|
}
|
|
380
381
|
/**
|
|
381
|
-
*
|
|
382
|
-
*
|
|
382
|
+
* Is a Node.js runtime available — asked of the host rather than asserted.
|
|
383
|
+
*
|
|
384
|
+
* This row used to be the literal `{found: true, version: process.version}`, which is
|
|
385
|
+
* a lie on three of the four hosts: `@gjsify/process` stubs `process.version` to
|
|
386
|
+
* `"v20.0.0"` for npm compatibility (Bun and Deno fake `process.versions.node` for the
|
|
387
|
+
* same reason), so `gjsify system-check` under GJS reported `✓ Node.js (v20.0.0)` on a
|
|
388
|
+
* machine with no Node on it — under the NODE-FREE toolchain, whose entire premise is
|
|
389
|
+
* that there is none. `cli-app.ts`'s `runtimeLabel()` already had to know better; one
|
|
390
|
+
* question gets one answer, so this asks the same module.
|
|
391
|
+
*
|
|
392
|
+
* Under Node the running process IS the answer and its version cannot be faked from
|
|
393
|
+
* inside; elsewhere it is a PATH question whose answer may legitimately be "no".
|
|
394
|
+
*
|
|
395
|
+
* `isNode()` rather than `hostRuntime() === 'node'`: the latter RETURNS `'node'` as its
|
|
396
|
+
* fallback for an unrecognised host, which would put an exotic runtime's possibly-stubbed
|
|
397
|
+
* `process.version` on the trusted branch. `isNode()` demands positive evidence.
|
|
398
|
+
*
|
|
399
|
+
* `optional`, never `required`: `gjsify showcase` and `gjsify install` both treat a
|
|
400
|
+
* missing `required` row as fatal, and a GJS showcase needs no Node. The old row could
|
|
401
|
+
* not have failed either gate — it was constant — so nothing that used to pass now
|
|
402
|
+
* fails; what changes is only that the printed line is true.
|
|
403
|
+
*/
|
|
404
|
+
function checkNodeRuntime() {
|
|
405
|
+
if (isNode()) {
|
|
406
|
+
return { id: 'nodejs', name: 'Node.js', found: true, version: process.version, severity: 'optional' };
|
|
407
|
+
}
|
|
408
|
+
return checkBinary('nodejs', 'Node.js', 'node', ['--version'], 'optional');
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Minimal checks needed to run any GJS example (the GJS binary; Node is reported but
|
|
412
|
+
* not required). Used by `gjsify showcase` for examples that have no native deps.
|
|
383
413
|
*/
|
|
384
414
|
export function runMinimalChecks() {
|
|
385
415
|
const results = [];
|
|
386
|
-
|
|
387
|
-
results.push({ id: 'nodejs', name: 'Node.js', found: true, version: process.version, severity: 'required' });
|
|
416
|
+
results.push(checkNodeRuntime());
|
|
388
417
|
// GJS
|
|
389
418
|
results.push(checkBinary('gjs', 'GJS', 'gjs', ['--version'], 'required', (out) => out.replace(/^GJS\s+/i, '').split('\n')[0] ?? out));
|
|
390
419
|
// The one win32 system dependency that is real and was undiagnosed (#997).
|
|
@@ -528,6 +557,7 @@ function runOptionalChecks(needed, cwd) {
|
|
|
528
557
|
// Entries with multiple space-separated packages are intentional (one dep needs multiple system pkgs).
|
|
529
558
|
const PM_PACKAGES = {
|
|
530
559
|
apt: {
|
|
560
|
+
nodejs: 'nodejs',
|
|
531
561
|
gjs: 'gjs',
|
|
532
562
|
'blueprint-compiler': 'blueprint-compiler',
|
|
533
563
|
'pkg-config': 'pkg-config',
|
|
@@ -553,6 +583,7 @@ const PM_PACKAGES = {
|
|
|
553
583
|
epoxy: 'libepoxy-dev',
|
|
554
584
|
},
|
|
555
585
|
dnf: {
|
|
586
|
+
nodejs: 'nodejs',
|
|
556
587
|
gjs: 'gjs',
|
|
557
588
|
'blueprint-compiler': 'blueprint-compiler',
|
|
558
589
|
'pkg-config': 'pkgconf-pkg-config',
|
|
@@ -578,6 +609,7 @@ const PM_PACKAGES = {
|
|
|
578
609
|
epoxy: 'libepoxy-devel',
|
|
579
610
|
},
|
|
580
611
|
pacman: {
|
|
612
|
+
nodejs: 'nodejs',
|
|
581
613
|
gjs: 'gjs',
|
|
582
614
|
'blueprint-compiler': 'blueprint-compiler',
|
|
583
615
|
'pkg-config': 'pkgconf',
|
|
@@ -603,6 +635,7 @@ const PM_PACKAGES = {
|
|
|
603
635
|
epoxy: 'libepoxy',
|
|
604
636
|
},
|
|
605
637
|
zypper: {
|
|
638
|
+
nodejs: 'nodejs',
|
|
606
639
|
gjs: 'gjs',
|
|
607
640
|
'blueprint-compiler': 'blueprint-compiler',
|
|
608
641
|
'pkg-config': 'pkg-config',
|
|
@@ -628,6 +661,7 @@ const PM_PACKAGES = {
|
|
|
628
661
|
epoxy: 'libepoxy-devel',
|
|
629
662
|
},
|
|
630
663
|
apk: {
|
|
664
|
+
nodejs: 'nodejs',
|
|
631
665
|
gjs: 'gjs',
|
|
632
666
|
'blueprint-compiler': 'blueprint-compiler',
|
|
633
667
|
'pkg-config': 'pkgconf',
|
|
@@ -675,6 +709,7 @@ const PM_PACKAGES = {
|
|
|
675
709
|
// Recorded here so the next reader does not "fix" the gap by guessing a name;
|
|
676
710
|
// both are genuine platform gaps, not table omissions.
|
|
677
711
|
brew: {
|
|
712
|
+
nodejs: 'node',
|
|
678
713
|
gjs: 'gjs',
|
|
679
714
|
'blueprint-compiler': 'blueprint-compiler',
|
|
680
715
|
'pkg-config': 'pkgconf',
|
|
@@ -750,8 +785,10 @@ export function buildInstallCommand(pm, missing) {
|
|
|
750
785
|
'mingw-w64-ucrt-x86_64-libadwaita # inside MSYS2; found automatically afterwards');
|
|
751
786
|
continue;
|
|
752
787
|
}
|
|
753
|
-
|
|
754
|
-
|
|
788
|
+
// No `continue` for `nodejs`. It USED to be unmissable — the row was the
|
|
789
|
+
// literal `found: true` — and since it became a real probe it can report
|
|
790
|
+
// absent under GJS, so skipping it here printed "Missing optional: Node.js"
|
|
791
|
+
// above an install command that did not install it.
|
|
755
792
|
const pkg = pkgMap[dep.id];
|
|
756
793
|
if (pkg)
|
|
757
794
|
pkgs.push(pkg);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Where the running CLI came from. */
|
|
2
|
+
export type InstallProvenanceKind =
|
|
3
|
+
/** `install.mjs` or `gjsify install -g` — the layout `self-update` manages. */
|
|
4
|
+
'xdg-global'
|
|
5
|
+
/** Inside a Flatpak sandbox. */
|
|
6
|
+
| 'flatpak'
|
|
7
|
+
/** A distro package (`.deb`/`.rpm`) staged under a system prefix. */
|
|
8
|
+
| 'system-package'
|
|
9
|
+
/** `npm install -g` (or yarn/pnpm global) — a `node_modules` tree elsewhere. */
|
|
10
|
+
| 'npm-global'
|
|
11
|
+
/** A checkout, a container image, an unpacked tarball — anything else. */
|
|
12
|
+
| 'unknown';
|
|
13
|
+
export interface InstallProvenance {
|
|
14
|
+
kind: InstallProvenanceKind;
|
|
15
|
+
/** What the decision was made ON, so a wrong verdict is debuggable. */
|
|
16
|
+
evidence: string;
|
|
17
|
+
/** How the user should update, when it is not `self-update`'s job. */
|
|
18
|
+
updateWith?: string;
|
|
19
|
+
/** True when `self-update` must refuse rather than write the XDG prefix. */
|
|
20
|
+
managedElsewhere: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface ProvenanceInput {
|
|
23
|
+
/** Absolute path of the running CLI's own package directory. */
|
|
24
|
+
selfDir: string;
|
|
25
|
+
/** `defaultGlobalLayout().prefix` — the layout `self-update` owns. */
|
|
26
|
+
xdgPrefix: string;
|
|
27
|
+
env?: Record<string, string | undefined>;
|
|
28
|
+
platform?: NodeJS.Platform;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Classify an installation from its path and environment.
|
|
32
|
+
*
|
|
33
|
+
* ORDER MATTERS and is not alphabetical:
|
|
34
|
+
*
|
|
35
|
+
* 1. **Flatpak first.** Its `/app` prefix would also satisfy no other test, but
|
|
36
|
+
* `FLATPAK_ID` is the unambiguous signal and is present even when the tree is
|
|
37
|
+
* mounted somewhere unusual.
|
|
38
|
+
* 2. **XDG before system.** The XDG prefix is under `$HOME`, so it can never be
|
|
39
|
+
* `/usr` — but a user whose `$HOME` is `/opt/someuser` exists, and asking the
|
|
40
|
+
* narrow question first means that user is not told to run `dnf`.
|
|
41
|
+
* 3. **System before npm.** A `.deb` stages a `node_modules`-shaped tree under
|
|
42
|
+
* `/usr/lib`, so the npm test alone would claim it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function classifyInstall(input: ProvenanceInput): InstallProvenance;
|