@gjsify/cli 0.4.9 → 0.4.10
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 +135 -134
- package/lib/commands/generate-installer.d.ts +10 -0
- package/lib/commands/generate-installer.js +113 -0
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/self-update.d.ts +8 -0
- package/lib/commands/self-update.js +138 -0
- package/lib/index.js +3 -1
- package/lib/templates/install.mjs.tmpl +248 -0
- package/lib/utils/install-global.js +13 -1
- package/package.json +17 -17
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Command } from '../types/index.js';
|
|
2
|
+
interface GenerateInstallerOptions {
|
|
3
|
+
target?: string;
|
|
4
|
+
'bin-name'?: string;
|
|
5
|
+
'bootstrap-url'?: string;
|
|
6
|
+
output: string;
|
|
7
|
+
force: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const generateInstallerCommand: Command<any, GenerateInstallerOptions>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// `gjsify generate-installer [target]` — scaffold an `install.mjs` for any
|
|
2
|
+
// GJS-runnable npm package, modeled on gjsify's own installer.
|
|
3
|
+
//
|
|
4
|
+
// The generated `install.mjs` is a verbatim copy of gjsify's root `install.mjs`
|
|
5
|
+
// with three constants substituted:
|
|
6
|
+
//
|
|
7
|
+
// DEFAULT_TARGET → the consumer's npm package name
|
|
8
|
+
// DEFAULT_BIN_NAME → the consumer's bin name (key of `gjsify.bin` or `bin`)
|
|
9
|
+
// DEFAULT_BOOTSTRAP_URL → URL of a gjsify `cli.gjs.mjs` bootstrap bundle
|
|
10
|
+
//
|
|
11
|
+
// End-user workflow:
|
|
12
|
+
// cd my-gjs-app
|
|
13
|
+
// gjsify generate-installer
|
|
14
|
+
// git add install.mjs && git commit
|
|
15
|
+
// # README:
|
|
16
|
+
// # curl -fsSL https://github.com/me/my-gjs-app/raw/main/install.mjs \
|
|
17
|
+
// # -o /tmp/i.mjs && gjs -m /tmp/i.mjs && rm /tmp/i.mjs
|
|
18
|
+
//
|
|
19
|
+
// The template is read at build time (static-read-inliner inlines the file
|
|
20
|
+
// contents into the bundled CLI), so the runtime cost is just a string
|
|
21
|
+
// replace + write.
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { resolve } from 'node:path';
|
|
24
|
+
// Lazy load. Reading at the top level breaks `gjsify run copy-templates`
|
|
25
|
+
// (the bootstrap step that ships the template into `lib/templates/` after
|
|
26
|
+
// `tsc`): the run script must first import this module to dispatch into
|
|
27
|
+
// itself, which would then ENOENT on the not-yet-copied template file.
|
|
28
|
+
// The static-read-inliner can still detect this shape inside the handler.
|
|
29
|
+
function loadInstallerTemplate() {
|
|
30
|
+
return readFileSync(new URL('../templates/install.mjs.tmpl', import.meta.url), 'utf-8');
|
|
31
|
+
}
|
|
32
|
+
const DEFAULT_BOOTSTRAP_URL = 'https://github.com/gjsify/gjsify/releases/latest/download/cli.gjs.mjs';
|
|
33
|
+
export const generateInstallerCommand = {
|
|
34
|
+
command: 'generate-installer [target]',
|
|
35
|
+
description: 'Scaffold an install.mjs in the current directory for a GJS-runnable npm package.',
|
|
36
|
+
builder: (yargs) => yargs
|
|
37
|
+
.positional('target', {
|
|
38
|
+
description: 'Npm package name to install (default: current package.json name).',
|
|
39
|
+
type: 'string',
|
|
40
|
+
})
|
|
41
|
+
.option('bin-name', {
|
|
42
|
+
description: 'Bin name produced by the installer (default: first key of `gjsify.bin` or `bin`).',
|
|
43
|
+
type: 'string',
|
|
44
|
+
})
|
|
45
|
+
.option('bootstrap-url', {
|
|
46
|
+
description: 'Override the cli.gjs.mjs bootstrap bundle URL (default: gjsify GitHub releases/latest).',
|
|
47
|
+
type: 'string',
|
|
48
|
+
})
|
|
49
|
+
.option('output', {
|
|
50
|
+
description: 'Where to write the generated installer.',
|
|
51
|
+
type: 'string',
|
|
52
|
+
default: 'install.mjs',
|
|
53
|
+
})
|
|
54
|
+
.option('force', {
|
|
55
|
+
description: 'Overwrite an existing output file.',
|
|
56
|
+
type: 'boolean',
|
|
57
|
+
default: false,
|
|
58
|
+
}),
|
|
59
|
+
handler: (args) => {
|
|
60
|
+
const outputPath = resolve(process.cwd(), args.output);
|
|
61
|
+
if (existsSync(outputPath) && !args.force) {
|
|
62
|
+
console.error(`${args.output} already exists. Re-run with --force to overwrite.`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const pkgJsonPath = resolve(process.cwd(), 'package.json');
|
|
67
|
+
let pkgJson = null;
|
|
68
|
+
if (existsSync(pkgJsonPath)) {
|
|
69
|
+
try {
|
|
70
|
+
pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* no pkg.json or unparsable — fall back to flags */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const target = args.target ?? pkgJson?.name;
|
|
77
|
+
if (!target) {
|
|
78
|
+
console.error('No target package: pass `gjsify generate-installer <pkg>` or run inside a directory with a package.json.');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const binName = args['bin-name'] ?? pickDefaultBinName(pkgJson, target);
|
|
83
|
+
const bootstrapUrl = args['bootstrap-url'] ?? DEFAULT_BOOTSTRAP_URL;
|
|
84
|
+
const rendered = loadInstallerTemplate()
|
|
85
|
+
.replace(/const DEFAULT_TARGET = '[^']+';/, `const DEFAULT_TARGET = ${JSON.stringify(target)};`)
|
|
86
|
+
.replace(/const DEFAULT_BIN_NAME = '[^']+';/, `const DEFAULT_BIN_NAME = ${JSON.stringify(binName)};`)
|
|
87
|
+
.replace(/const DEFAULT_BOOTSTRAP_URL =\s*'[^']+';/, `const DEFAULT_BOOTSTRAP_URL = ${JSON.stringify(bootstrapUrl)};`);
|
|
88
|
+
writeFileSync(outputPath, rendered, { mode: 0o755 });
|
|
89
|
+
console.log(`Wrote ${args.output} (target=${target}, bin=${binName}).`);
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log('Install one-liner for your README:');
|
|
92
|
+
console.log(` curl -fsSL https://github.com/<you>/<repo>/raw/main/${args.output} -o /tmp/i.mjs \\`);
|
|
93
|
+
console.log(' && gjs -m /tmp/i.mjs && rm /tmp/i.mjs');
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
function pickDefaultBinName(pkgJson, target) {
|
|
97
|
+
const gjsifyBin = pkgJson?.gjsify?.bin;
|
|
98
|
+
if (gjsifyBin && typeof gjsifyBin === 'object') {
|
|
99
|
+
const first = Object.keys(gjsifyBin)[0];
|
|
100
|
+
if (first)
|
|
101
|
+
return first;
|
|
102
|
+
}
|
|
103
|
+
const npmBin = pkgJson?.bin;
|
|
104
|
+
if (npmBin && typeof npmBin === 'object') {
|
|
105
|
+
const first = Object.keys(npmBin)[0];
|
|
106
|
+
if (first)
|
|
107
|
+
return first;
|
|
108
|
+
}
|
|
109
|
+
if (typeof npmBin === 'string') {
|
|
110
|
+
return target.startsWith('@') ? target.slice(target.indexOf('/') + 1) : target;
|
|
111
|
+
}
|
|
112
|
+
return target.startsWith('@') ? target.slice(target.indexOf('/') + 1) : target;
|
|
113
|
+
}
|
package/lib/commands/index.d.ts
CHANGED
package/lib/commands/index.js
CHANGED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// `gjsify self-update` — refresh the installed @gjsify/cli to a newer release.
|
|
2
|
+
//
|
|
3
|
+
// Walks `import.meta.url` to find this CLI's own package.json (works whether
|
|
4
|
+
// running from `lib/index.js` under Node or the published `dist/cli.gjs.mjs`
|
|
5
|
+
// bundle under GJS). Compares against the latest version on the npm registry
|
|
6
|
+
// (or the requested `--tag`); when an upgrade is needed, re-uses the existing
|
|
7
|
+
// `installPackages` + `linkGlobalBins` pipeline to lay down the new tree at
|
|
8
|
+
// the user-global XDG location.
|
|
9
|
+
//
|
|
10
|
+
// Limitation: only works when the current CLI is installed under
|
|
11
|
+
// `defaultGlobalLayout().prefix` (i.e. via `gjsify install -g` or via the
|
|
12
|
+
// `install.mjs` bootstrap). Installs from `npm install -g @gjsify/cli` land
|
|
13
|
+
// elsewhere and we don't try to chase them — we print a warning and exit.
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join, resolve } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { fetchPackument } from '@gjsify/npm-registry';
|
|
18
|
+
import { installPackages } from '../utils/install-backend.js';
|
|
19
|
+
import { defaultGlobalLayout, linkGlobalBins } from '../utils/install-global.js';
|
|
20
|
+
const PACKAGE_NAME = '@gjsify/cli';
|
|
21
|
+
export const selfUpdateCommand = {
|
|
22
|
+
command: 'self-update',
|
|
23
|
+
description: `Update the installed ${PACKAGE_NAME} to the latest release (or pinned --tag).`,
|
|
24
|
+
builder: (yargs) => yargs
|
|
25
|
+
.option('check', {
|
|
26
|
+
description: 'Only check whether a newer version is available; do not install.',
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
default: false,
|
|
29
|
+
})
|
|
30
|
+
.option('force', {
|
|
31
|
+
description: 'Reinstall even when the current version already matches the target tag.',
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
default: false,
|
|
34
|
+
})
|
|
35
|
+
.option('tag', {
|
|
36
|
+
description: 'npm dist-tag or pinned version to install (e.g. `latest`, `next`, `0.5.0`).',
|
|
37
|
+
type: 'string',
|
|
38
|
+
default: 'latest',
|
|
39
|
+
}),
|
|
40
|
+
handler: async (args) => {
|
|
41
|
+
const layout = defaultGlobalLayout();
|
|
42
|
+
const installedPkgDir = join(layout.prefix, 'node_modules', PACKAGE_NAME);
|
|
43
|
+
const installedPkgJson = join(installedPkgDir, 'package.json');
|
|
44
|
+
const currentVersion = readCurrentVersion();
|
|
45
|
+
const installedAtPrefix = existsSync(installedPkgJson);
|
|
46
|
+
console.log(`Current ${PACKAGE_NAME}: v${currentVersion ?? '(unknown)'}`);
|
|
47
|
+
if (!installedAtPrefix) {
|
|
48
|
+
console.warn(`\nWarning: no @gjsify/cli install found under ${layout.prefix}.\n` +
|
|
49
|
+
`self-update only manages installs created by install.mjs or \`gjsify install -g\`.\n` +
|
|
50
|
+
`If you installed via \`npm install -g\`, remove that and use:\n` +
|
|
51
|
+
` curl -fsSL https://github.com/gjsify/gjsify/releases/latest/download/install.mjs -o /tmp/g.mjs && gjs -m /tmp/g.mjs && rm /tmp/g.mjs`);
|
|
52
|
+
}
|
|
53
|
+
console.log(`Fetching dist-tags for ${PACKAGE_NAME}@${args.tag} ...`);
|
|
54
|
+
let packument;
|
|
55
|
+
try {
|
|
56
|
+
packument = await fetchPackument(PACKAGE_NAME);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
60
|
+
console.error(`Failed to fetch packument: ${msg}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const target = resolveTag(packument, args.tag);
|
|
65
|
+
if (!target) {
|
|
66
|
+
console.error(`Unknown dist-tag '${args.tag}' on ${PACKAGE_NAME}. ` +
|
|
67
|
+
`Known tags: ${Object.keys(packument['dist-tags'] ?? {}).join(', ') || '(none)'}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
console.log(`Latest matching --tag ${args.tag}: v${target}`);
|
|
72
|
+
if (currentVersion === target && !args.force) {
|
|
73
|
+
console.log(`Already up to date (v${target}).`);
|
|
74
|
+
if (!args.check)
|
|
75
|
+
console.log(`Run with --force to reinstall anyway.`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (args.check) {
|
|
79
|
+
console.log(currentVersion
|
|
80
|
+
? `Update available: v${currentVersion} → v${target}`
|
|
81
|
+
: `Install required: → v${target}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.log(`Installing ${PACKAGE_NAME}@${target} ...`);
|
|
86
|
+
await installPackages({
|
|
87
|
+
prefix: layout.prefix,
|
|
88
|
+
specs: [`${PACKAGE_NAME}@${target}`],
|
|
89
|
+
verbose: false,
|
|
90
|
+
});
|
|
91
|
+
const linked = linkGlobalBins([PACKAGE_NAME], layout);
|
|
92
|
+
if (linked.length === 0) {
|
|
93
|
+
console.warn('self-update: install completed but no bins were linked — package.json may be missing a `bin` field.');
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
for (const bin of linked) {
|
|
97
|
+
console.log(` • ${bin.link} → ${bin.target}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
console.log(`\nUpdated ${PACKAGE_NAME} to v${target}.`);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the CLI's own `package.json#version`. Walks up from
|
|
105
|
+
* `import.meta.url` (the bundle file under GJS, or `lib/index.js` under
|
|
106
|
+
* Node) until it finds a package.json with `name === '@gjsify/cli'`.
|
|
107
|
+
*/
|
|
108
|
+
function readCurrentVersion() {
|
|
109
|
+
try {
|
|
110
|
+
const here = fileURLToPath(import.meta.url);
|
|
111
|
+
let dir = dirname(resolve(here));
|
|
112
|
+
for (let i = 0; i < 8 && dir !== dirname(dir); i++) {
|
|
113
|
+
const candidate = join(dir, 'package.json');
|
|
114
|
+
if (existsSync(candidate)) {
|
|
115
|
+
const pkg = JSON.parse(readFileSync(candidate, 'utf-8'));
|
|
116
|
+
if (pkg.name === PACKAGE_NAME && typeof pkg.version === 'string') {
|
|
117
|
+
return pkg.version;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
dir = dirname(dir);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* not in a recognizable layout */
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function resolveTag(packument, tag) {
|
|
129
|
+
const distTags = (packument['dist-tags'] ?? {});
|
|
130
|
+
if (distTags[tag])
|
|
131
|
+
return distTags[tag];
|
|
132
|
+
// Allow pinned versions via `--tag 0.5.0`
|
|
133
|
+
if (packument.versions && typeof packument.versions === 'object') {
|
|
134
|
+
if (packument.versions[tag])
|
|
135
|
+
return tag;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import yargs from 'yargs';
|
|
3
3
|
import { hideBin } from 'yargs/helpers';
|
|
4
|
-
import { buildCommand as build, runCommand as run, infoCommand as info, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, } from './commands/index.js';
|
|
4
|
+
import { buildCommand as build, runCommand as run, infoCommand as info, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, } from './commands/index.js';
|
|
5
5
|
import { APP_NAME } from './constants.js';
|
|
6
6
|
// `parseAsync()` instead of `.argv` so the top-level await keeps the
|
|
7
7
|
// process alive until command handlers complete. Under Node this is
|
|
@@ -27,6 +27,8 @@ await yargs(hideBin(process.argv))
|
|
|
27
27
|
.command(workspace.command, workspace.description, workspace.builder, workspace.handler)
|
|
28
28
|
.command(pack.command, pack.description, pack.builder, pack.handler)
|
|
29
29
|
.command(publish.command, publish.description, publish.builder, publish.handler)
|
|
30
|
+
.command(selfUpdate.command, selfUpdate.description, selfUpdate.builder, selfUpdate.handler)
|
|
31
|
+
.command(generateInstaller.command, generateInstaller.description, generateInstaller.builder, generateInstaller.handler)
|
|
30
32
|
.demandCommand(1)
|
|
31
33
|
.help()
|
|
32
34
|
.parseAsync();
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env -S gjs -m
|
|
2
|
+
/**
|
|
3
|
+
* gjsify universal installer — bootstraps `@gjsify/cli` (or any GJS app
|
|
4
|
+
* published to npm) on a system that has only `gjs` (and `curl`/`wget`)
|
|
5
|
+
* available, without requiring Node.js or `npm`.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* gjs -m install.mjs # install / update @gjsify/cli
|
|
9
|
+
* gjs -m install.mjs --target @scope/x # install any other GJS-runnable npm package
|
|
10
|
+
* gjs -m install.mjs --tag next # pick an npm dist-tag or pinned version
|
|
11
|
+
* gjs -m install.mjs --force # reinstall even if already present
|
|
12
|
+
* gjs -m install.mjs --help
|
|
13
|
+
*
|
|
14
|
+
* How it works (two-stage bootstrap):
|
|
15
|
+
* 1. Download a small self-contained GJS bundle of the @gjsify/cli
|
|
16
|
+
* (`cli.gjs.mjs`) from this repo's GitHub releases. Verify SHA-256.
|
|
17
|
+
* 2. Spawn that bundle: `gjs -m <bundle> install -g <target>@<tag>`. The
|
|
18
|
+
* bundle handles transitive dependency resolution, native prebuilds,
|
|
19
|
+
* lockfiles, and the `~/.local/bin` launchers — all the things this
|
|
20
|
+
* thin bootstrapper deliberately does NOT re-implement.
|
|
21
|
+
*
|
|
22
|
+
* Generated by `gjsify generate-installer` for end-user GJS apps: in that
|
|
23
|
+
* mode the constants below (BOOTSTRAP_URL, DEFAULT_TARGET, DEFAULT_BIN_NAME)
|
|
24
|
+
* are pre-substituted to the consumer's package + custom bootstrap URL.
|
|
25
|
+
*
|
|
26
|
+
* Test hooks (set by tests/e2e/install-script/run.mjs):
|
|
27
|
+
* GJSIFY_INSTALL_BOOTSTRAP_URL override the cli.gjs.mjs download origin
|
|
28
|
+
* (accepts file:// for offline tests)
|
|
29
|
+
* GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL override the .sha256 companion URL
|
|
30
|
+
* (set to empty string to skip SHA-256)
|
|
31
|
+
* GJSIFY_GLOBAL_PREFIX override install prefix (forwarded to cli)
|
|
32
|
+
* GJSIFY_GLOBAL_BIN_DIR override bin dir (forwarded to cli)
|
|
33
|
+
* GJSIFY_INSTALL_REGISTRY override npm registry (forwarded as npm_config_registry)
|
|
34
|
+
* GJSIFY_INSTALL_BOOTSTRAP_CACHE override the bootstrap cache dir
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import GLib from 'gi://GLib';
|
|
38
|
+
import Gio from 'gi://Gio';
|
|
39
|
+
import Soup from 'gi://Soup?version=3.0';
|
|
40
|
+
import system, { exit } from 'system';
|
|
41
|
+
|
|
42
|
+
Gio._promisify(Soup.Session.prototype, 'send_and_read_async');
|
|
43
|
+
Gio._promisify(Gio.Subprocess.prototype, 'wait_check_async');
|
|
44
|
+
|
|
45
|
+
// Substituted by `gjsify generate-installer` for end-user apps.
|
|
46
|
+
const DEFAULT_TARGET = '@gjsify/cli';
|
|
47
|
+
const DEFAULT_BIN_NAME = 'gjsify';
|
|
48
|
+
const DEFAULT_BOOTSTRAP_URL =
|
|
49
|
+
'https://github.com/gjsify/gjsify/releases/latest/download/cli.gjs.mjs';
|
|
50
|
+
const DEFAULT_BOOTSTRAP_SHA256_URL = `${DEFAULT_BOOTSTRAP_URL}.sha256`;
|
|
51
|
+
|
|
52
|
+
const USER_AGENT = 'gjsify-installer/1.0';
|
|
53
|
+
|
|
54
|
+
function info(msg) { print(`[gjsify] ${msg}`); }
|
|
55
|
+
function error(msg) { printerr(`[gjsify] ERROR: ${msg}`); }
|
|
56
|
+
|
|
57
|
+
function parseArgs() {
|
|
58
|
+
const argv = system?.programArgs ?? [];
|
|
59
|
+
let target = DEFAULT_TARGET;
|
|
60
|
+
let tag = 'latest';
|
|
61
|
+
let force = false;
|
|
62
|
+
let help = false;
|
|
63
|
+
let bootstrapUrl = GLib.getenv('GJSIFY_INSTALL_BOOTSTRAP_URL') || DEFAULT_BOOTSTRAP_URL;
|
|
64
|
+
let bootstrapSha256Url = GLib.getenv('GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL');
|
|
65
|
+
if (bootstrapSha256Url === null || bootstrapSha256Url === undefined) {
|
|
66
|
+
bootstrapSha256Url = bootstrapUrl === DEFAULT_BOOTSTRAP_URL
|
|
67
|
+
? DEFAULT_BOOTSTRAP_SHA256_URL
|
|
68
|
+
: `${bootstrapUrl}.sha256`;
|
|
69
|
+
}
|
|
70
|
+
for (let i = 0; i < argv.length; i++) {
|
|
71
|
+
const a = argv[i];
|
|
72
|
+
if (a === '--force' || a === '-f') force = true;
|
|
73
|
+
else if (a === '--help' || a === '-h') help = true;
|
|
74
|
+
else if (a === '--target') target = argv[++i];
|
|
75
|
+
else if (a.startsWith('--target=')) target = a.slice('--target='.length);
|
|
76
|
+
else if (a === '--tag') tag = argv[++i];
|
|
77
|
+
else if (a.startsWith('--tag=')) tag = a.slice('--tag='.length);
|
|
78
|
+
else if (a === '--bootstrap-url') bootstrapUrl = argv[++i];
|
|
79
|
+
else if (a.startsWith('--bootstrap-url=')) bootstrapUrl = a.slice('--bootstrap-url='.length);
|
|
80
|
+
}
|
|
81
|
+
return { target, tag, force, help, bootstrapUrl, bootstrapSha256Url };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function printUsage() {
|
|
85
|
+
print(`Usage: gjs -m install.mjs [options]
|
|
86
|
+
|
|
87
|
+
Installs (or updates) ${DEFAULT_TARGET} into the user-global XDG location,
|
|
88
|
+
using a self-contained GJS bundle of @gjsify/cli as a one-shot bootstrap.
|
|
89
|
+
|
|
90
|
+
Options:
|
|
91
|
+
--target <pkg> npm package to install (default: ${DEFAULT_TARGET})
|
|
92
|
+
--tag <tag> npm dist-tag or version (default: latest)
|
|
93
|
+
--force, -f Reinstall even when present.
|
|
94
|
+
--bootstrap-url <url> Override the cli.gjs.mjs download URL.
|
|
95
|
+
--help, -h Show this message.
|
|
96
|
+
|
|
97
|
+
Env vars:
|
|
98
|
+
GJSIFY_INSTALL_BOOTSTRAP_URL alternate bootstrap bundle URL (file:// OK)
|
|
99
|
+
GJSIFY_GLOBAL_PREFIX install prefix (default: ~/.local/share/gjsify/global)
|
|
100
|
+
GJSIFY_GLOBAL_BIN_DIR bin dir (default: ~/.local/bin)
|
|
101
|
+
GJSIFY_INSTALL_REGISTRY npm registry override
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
# Install / update the gjsify CLI itself:
|
|
105
|
+
gjs -m install.mjs
|
|
106
|
+
|
|
107
|
+
# Install some other GJS-runnable package from npm:
|
|
108
|
+
gjs -m install.mjs --target @ts-for-gir/cli
|
|
109
|
+
|
|
110
|
+
# Pin a specific version:
|
|
111
|
+
gjs -m install.mjs --tag 0.4.9
|
|
112
|
+
`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function checkGjsVersion() {
|
|
116
|
+
// imports.system.version is packed: major*10000 + minor*100 + micro
|
|
117
|
+
const v = system?.version;
|
|
118
|
+
if (typeof v !== 'number') return;
|
|
119
|
+
const major = Math.floor(v / 10000);
|
|
120
|
+
const minor = Math.floor((v - major * 10000) / 100);
|
|
121
|
+
if (major < 1 || (major === 1 && minor < 86)) {
|
|
122
|
+
error(`gjs ${major}.${minor} is too old — gjsify requires gjs 1.86 or newer.`);
|
|
123
|
+
error('Install hints:');
|
|
124
|
+
error(' Fedora 43+: sudo dnf install gjs');
|
|
125
|
+
error(' Debian 13+: sudo apt install gjs');
|
|
126
|
+
error(' Arch: sudo pacman -S gjs');
|
|
127
|
+
exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function fetchBytes(session, url) {
|
|
132
|
+
if (url.startsWith('file://')) {
|
|
133
|
+
const path = url.slice('file://'.length);
|
|
134
|
+
const file = Gio.File.new_for_path(path);
|
|
135
|
+
const [, bytes] = file.load_contents(null);
|
|
136
|
+
return bytes;
|
|
137
|
+
}
|
|
138
|
+
const message = Soup.Message.new('GET', url);
|
|
139
|
+
message.request_headers.append('User-Agent', USER_AGENT);
|
|
140
|
+
const bytes = await session.send_and_read_async(message, GLib.PRIORITY_DEFAULT, null);
|
|
141
|
+
const status = message.get_status();
|
|
142
|
+
if (status !== Soup.Status.OK) {
|
|
143
|
+
throw new Error(`HTTP ${status} from ${url}`);
|
|
144
|
+
}
|
|
145
|
+
return bytes.get_data();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sha256Hex(bytes) {
|
|
149
|
+
const checksum = GLib.Checksum.new(GLib.ChecksumType.SHA256);
|
|
150
|
+
checksum.update(bytes);
|
|
151
|
+
return checksum.get_string();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function cacheDir() {
|
|
155
|
+
const override = GLib.getenv('GJSIFY_INSTALL_BOOTSTRAP_CACHE');
|
|
156
|
+
if (override) return override;
|
|
157
|
+
const xdg = GLib.getenv('XDG_CACHE_HOME') ||
|
|
158
|
+
GLib.build_filenamev([GLib.get_home_dir(), '.cache']);
|
|
159
|
+
return GLib.build_filenamev([xdg, 'gjsify', 'bootstrap']);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function ensureDir(dir) {
|
|
163
|
+
Gio.File.new_for_path(dir).make_directory_with_parents(null);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function writeBytes(path, bytes) {
|
|
167
|
+
Gio.File.new_for_path(path).replace_contents(
|
|
168
|
+
bytes, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function downloadBootstrap(session, bootstrapUrl, sha256Url) {
|
|
173
|
+
info(`Downloading bootstrap from ${bootstrapUrl} ...`);
|
|
174
|
+
const bundleBytes = await fetchBytes(session, bootstrapUrl);
|
|
175
|
+
if (sha256Url && sha256Url !== '') {
|
|
176
|
+
info('Verifying SHA-256 ...');
|
|
177
|
+
let sumExpected;
|
|
178
|
+
try {
|
|
179
|
+
const sumBytes = await fetchBytes(session, sha256Url);
|
|
180
|
+
sumExpected = new TextDecoder().decode(sumBytes).trim().split(/\s+/)[0];
|
|
181
|
+
} catch (err) {
|
|
182
|
+
error(`Could not fetch ${sha256Url} — skipping verification: ${err.message}`);
|
|
183
|
+
}
|
|
184
|
+
if (sumExpected) {
|
|
185
|
+
const sumActual = sha256Hex(bundleBytes);
|
|
186
|
+
if (sumExpected.toLowerCase() !== sumActual.toLowerCase()) {
|
|
187
|
+
error(`SHA-256 mismatch: expected ${sumExpected}, got ${sumActual}`);
|
|
188
|
+
exit(1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const dir = cacheDir();
|
|
193
|
+
try { ensureDir(dir); } catch { /* exists */ }
|
|
194
|
+
const bundlePath = GLib.build_filenamev([dir, 'cli.gjs.mjs']);
|
|
195
|
+
writeBytes(bundlePath, bundleBytes);
|
|
196
|
+
info(`Bootstrap cached at ${bundlePath} (${bundleBytes.length} bytes)`);
|
|
197
|
+
return bundlePath;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildSpec(target, tag) {
|
|
201
|
+
if (!tag || tag === 'latest') return target;
|
|
202
|
+
return `${target}@${tag}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function runInstall(bundlePath, spec) {
|
|
206
|
+
// `gjsify install -g <spec>` is always idempotent — it rewrites the tree
|
|
207
|
+
// unconditionally. There is no separate "force" flag to forward; the
|
|
208
|
+
// installer's own `--force` is satisfied by the fact that we always
|
|
209
|
+
// re-download the bootstrap and re-invoke the CLI.
|
|
210
|
+
info(`Running: gjs -m <bootstrap> install -g ${spec}`);
|
|
211
|
+
const argv = ['gjs', '-m', bundlePath, 'install', '-g', spec];
|
|
212
|
+
// Forward env vars verbatim so override paths set by tests / power-users
|
|
213
|
+
// reach the spawned CLI.
|
|
214
|
+
const launcher = new Gio.SubprocessLauncher({
|
|
215
|
+
flags: Gio.SubprocessFlags.NONE,
|
|
216
|
+
});
|
|
217
|
+
const proc = launcher.spawnv(argv);
|
|
218
|
+
await proc.wait_check_async(null);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function main() {
|
|
222
|
+
const opts = parseArgs();
|
|
223
|
+
if (opts.help) { printUsage(); exit(0); }
|
|
224
|
+
checkGjsVersion();
|
|
225
|
+
|
|
226
|
+
const session = new Soup.Session();
|
|
227
|
+
let bundlePath;
|
|
228
|
+
try {
|
|
229
|
+
bundlePath = await downloadBootstrap(session, opts.bootstrapUrl, opts.bootstrapSha256Url);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
error(`Bootstrap download failed: ${err.message}`);
|
|
232
|
+
exit(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const spec = buildSpec(opts.target, opts.tag);
|
|
236
|
+
try {
|
|
237
|
+
await runInstall(bundlePath, spec);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
error(`Install failed: ${err.message}`);
|
|
240
|
+
exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
info('');
|
|
244
|
+
info(`Installed ${spec}`);
|
|
245
|
+
info(`Run: ${DEFAULT_BIN_NAME} --help`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await main();
|
|
@@ -91,7 +91,19 @@ export function linkGlobalBins(packageNames, layout) {
|
|
|
91
91
|
// Inline `${target}` directly — this file is rewritten on every
|
|
92
92
|
// install, paths are user-owned, and POSIX `sh` quoting via
|
|
93
93
|
// single-quotes plus `'\''` for embedded quotes is well-defined.
|
|
94
|
-
|
|
94
|
+
//
|
|
95
|
+
// `.gjs.mjs` and `.mjs` bins are GJS-runnable bundles; we wrap them
|
|
96
|
+
// with `gjs -m` rather than direct-exec because not every published
|
|
97
|
+
// bundle ships a `#!/usr/bin/env -S gjs -m` shebang (the CLI's
|
|
98
|
+
// build:gjs-bundle script gained the `--shebang` flag late in
|
|
99
|
+
// Phase F, but published <=0.4.x tarballs predate it). Direct-
|
|
100
|
+
// exec'ing a shebang-less .mjs file falls back to /bin/sh which
|
|
101
|
+
// then tries to parse JavaScript as shell. Plain Node scripts
|
|
102
|
+
// with shebangs (lib/index.js) keep the direct-exec path.
|
|
103
|
+
const isGjsBundle = targetAbs.endsWith('.gjs.mjs') || targetAbs.endsWith('.mjs');
|
|
104
|
+
const launcher = isGjsBundle
|
|
105
|
+
? `#!/bin/sh\nexec gjs -m ${shQuote(targetAbs)} "$@"\n`
|
|
106
|
+
: `#!/bin/sh\nexec ${shQuote(targetAbs)} "$@"\n`;
|
|
95
107
|
fs.writeFileSync(linkPath, launcher);
|
|
96
108
|
fs.chmodSync(linkPath, 0o755);
|
|
97
109
|
created.push({ name: binName, target: targetAbs, link: linkPath });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"clear": "rm -rf lib dist tsconfig.tsbuildinfo || exit 0",
|
|
24
24
|
"check": "tsc --noEmit",
|
|
25
25
|
"start": "node lib/index.js",
|
|
26
|
-
"build": "tsc && gjsify run chmod",
|
|
27
|
-
"build:gjs-bundle": "node lib/index.js build src/index.ts --app gjs --outfile dist/cli.gjs.mjs",
|
|
26
|
+
"build": "tsc && mkdir -p lib/templates && cp -L src/templates/install.mjs.tmpl lib/templates/install.mjs.tmpl && gjsify run chmod",
|
|
27
|
+
"build:gjs-bundle": "node lib/index.js build src/index.ts --app gjs --outfile dist/cli.gjs.mjs --shebang",
|
|
28
28
|
"chmod": "chmod +x ./lib/index.js",
|
|
29
29
|
"build:test:node": "node lib/index.js build src/test.mts --app node --outfile dist/test.node.mjs",
|
|
30
30
|
"test:node": "node dist/test.node.mjs",
|
|
@@ -37,18 +37,18 @@
|
|
|
37
37
|
"cli"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@gjsify/buffer": "^0.4.
|
|
41
|
-
"@gjsify/create-app": "^0.4.
|
|
42
|
-
"@gjsify/node-globals": "^0.4.
|
|
43
|
-
"@gjsify/node-polyfills": "^0.4.
|
|
44
|
-
"@gjsify/npm-registry": "^0.4.
|
|
45
|
-
"@gjsify/resolve-npm": "^0.4.
|
|
46
|
-
"@gjsify/rolldown-plugin-gjsify": "^0.4.
|
|
47
|
-
"@gjsify/rolldown-plugin-pnp": "^0.4.
|
|
48
|
-
"@gjsify/semver": "^0.4.
|
|
49
|
-
"@gjsify/tar": "^0.4.
|
|
50
|
-
"@gjsify/web-polyfills": "^0.4.
|
|
51
|
-
"@gjsify/workspace": "^0.4.
|
|
40
|
+
"@gjsify/buffer": "^0.4.10",
|
|
41
|
+
"@gjsify/create-app": "^0.4.10",
|
|
42
|
+
"@gjsify/node-globals": "^0.4.10",
|
|
43
|
+
"@gjsify/node-polyfills": "^0.4.10",
|
|
44
|
+
"@gjsify/npm-registry": "^0.4.10",
|
|
45
|
+
"@gjsify/resolve-npm": "^0.4.10",
|
|
46
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.4.10",
|
|
47
|
+
"@gjsify/rolldown-plugin-pnp": "^0.4.10",
|
|
48
|
+
"@gjsify/semver": "^0.4.10",
|
|
49
|
+
"@gjsify/tar": "^0.4.10",
|
|
50
|
+
"@gjsify/web-polyfills": "^0.4.10",
|
|
51
|
+
"@gjsify/workspace": "^0.4.10",
|
|
52
52
|
"cosmiconfig": "^9.0.1",
|
|
53
53
|
"get-tsconfig": "^4.14.0",
|
|
54
54
|
"pkg-types": "^2.3.1",
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
"yargs": "^18.0.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@gjsify/unit": "^0.4.
|
|
59
|
+
"@gjsify/unit": "^0.4.10",
|
|
60
60
|
"@types/yargs": "^17.0.35",
|
|
61
61
|
"typescript": "^6.0.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@gjsify/rolldown-native": "^0.4.
|
|
64
|
+
"@gjsify/rolldown-native": "^0.4.10"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"@gjsify/rolldown-native": {
|