@gjsify/cli 0.4.5 → 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.
@@ -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
- const launcher = `#!/bin/sh\nexec ${shQuote(targetAbs)} "$@"\n`;
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.5",
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.5",
41
- "@gjsify/create-app": "^0.4.5",
42
- "@gjsify/node-globals": "^0.4.5",
43
- "@gjsify/node-polyfills": "^0.4.5",
44
- "@gjsify/npm-registry": "^0.4.5",
45
- "@gjsify/resolve-npm": "^0.4.5",
46
- "@gjsify/rolldown-plugin-gjsify": "^0.4.5",
47
- "@gjsify/rolldown-plugin-pnp": "^0.4.5",
48
- "@gjsify/semver": "^0.4.5",
49
- "@gjsify/tar": "^0.4.5",
50
- "@gjsify/web-polyfills": "^0.4.5",
51
- "@gjsify/workspace": "^0.4.5",
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.5",
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.5"
64
+ "@gjsify/rolldown-native": "^0.4.10"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "@gjsify/rolldown-native": {