@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.
- package/dist/cli.gjs.mjs +134 -130
- package/lib/commands/generate-installer.d.ts +10 -0
- package/lib/commands/generate-installer.js +113 -0
- package/lib/commands/index.d.ts +4 -0
- package/lib/commands/index.js +4 -0
- package/lib/commands/pack.d.ts +40 -0
- package/lib/commands/pack.js +335 -0
- package/lib/commands/publish.d.ts +12 -0
- package/lib/commands/publish.js +319 -0
- package/lib/commands/self-update.d.ts +8 -0
- package/lib/commands/self-update.js +138 -0
- package/lib/index.js +5 -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,319 @@
|
|
|
1
|
+
// `gjsify publish [path] [--tag <tag>] [--access <a>] [--tolerate-republish] [--dry-run]`
|
|
2
|
+
//
|
|
3
|
+
// Packs the workspace via `packWorkspace()` (Phase E.1), then PUTs the
|
|
4
|
+
// tarball to the configured npm registry. Mirrors `npm publish`'s observable
|
|
5
|
+
// behavior:
|
|
6
|
+
//
|
|
7
|
+
// - Reads .npmrc for registry URL + auth (project-local + ~/.npmrc), with
|
|
8
|
+
// `npm_config_registry` env override.
|
|
9
|
+
// - Scoped packages route to the scope's registry if configured
|
|
10
|
+
// (`@scope:registry=...` in .npmrc).
|
|
11
|
+
// - Auth: bearer token (`_authToken`) or basic (`_auth`).
|
|
12
|
+
// - --tolerate-republish: surface a 409 conflict as a notice + exit 0
|
|
13
|
+
// (matches yarn's flag of the same name).
|
|
14
|
+
// - --tag: published version gets this dist-tag (default `latest`).
|
|
15
|
+
// - --access: public | restricted — required for first publish of a
|
|
16
|
+
// scoped package on the public registry.
|
|
17
|
+
// - --provenance: pass-through only — actual provenance generation requires
|
|
18
|
+
// a sigstore signer that gjsify doesn't ship; we record the flag in the
|
|
19
|
+
// publish manifest but don't sign yet. Surface a warning so callers know.
|
|
20
|
+
// - --dry-run: pack only, no PUT.
|
|
21
|
+
//
|
|
22
|
+
// The request body matches npm's "publish" payload shape:
|
|
23
|
+
// {
|
|
24
|
+
// "_id": "<name>",
|
|
25
|
+
// "name": "<name>",
|
|
26
|
+
// "description": "<from package.json>",
|
|
27
|
+
// "dist-tags": { "<tag>": "<version>" },
|
|
28
|
+
// "versions": { "<version>": { ...full package.json + dist } },
|
|
29
|
+
// "_attachments": { "<filename>": { content_type, data: base64, length } }
|
|
30
|
+
// }
|
|
31
|
+
//
|
|
32
|
+
// Source: documented in https://docs.npmjs.com/cli/v10/commands/npm-publish
|
|
33
|
+
// and npm's @npmcli/registry-fetch internals — verified against npm's
|
|
34
|
+
// in-the-wild publish payloads.
|
|
35
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
36
|
+
import { homedir } from 'node:os';
|
|
37
|
+
import { join, resolve } from 'node:path';
|
|
38
|
+
import { DEFAULT_REGISTRY, parseNpmrc, registryFor, buildHeaders, } from '@gjsify/npm-registry';
|
|
39
|
+
import { packWorkspace } from './pack.js';
|
|
40
|
+
export const publishCommand = {
|
|
41
|
+
command: 'publish [path]',
|
|
42
|
+
description: 'Pack + upload the workspace at <path> (default: cwd) to its npm registry. Drop-in for `npm publish` with workspace:^ rewrite handled automatically.',
|
|
43
|
+
builder: (yargs) => yargs
|
|
44
|
+
.positional('path', { description: 'Workspace path (default: cwd).', type: 'string' })
|
|
45
|
+
.option('tag', {
|
|
46
|
+
description: 'Dist-tag to publish under. Default: latest.',
|
|
47
|
+
type: 'string',
|
|
48
|
+
default: 'latest',
|
|
49
|
+
})
|
|
50
|
+
.option('access', {
|
|
51
|
+
description: 'Package access — `public` or `restricted` (required for first publish of scoped packages on the public registry).',
|
|
52
|
+
type: 'string',
|
|
53
|
+
})
|
|
54
|
+
.option('tolerate-republish', {
|
|
55
|
+
description: 'Treat a 409 conflict (version already published) as success. Matches yarn `--tolerate-republish`.',
|
|
56
|
+
type: 'boolean',
|
|
57
|
+
default: false,
|
|
58
|
+
})
|
|
59
|
+
.option('provenance', {
|
|
60
|
+
description: 'Pass-through flag — recorded in the payload but no signing happens (gjsify doesn\'t ship a sigstore signer yet).',
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
default: false,
|
|
63
|
+
})
|
|
64
|
+
.option('dry-run', {
|
|
65
|
+
description: 'Pack only, do not PUT.',
|
|
66
|
+
type: 'boolean',
|
|
67
|
+
default: false,
|
|
68
|
+
})
|
|
69
|
+
.option('json', {
|
|
70
|
+
description: 'Emit publish metadata as JSON on stdout.',
|
|
71
|
+
type: 'boolean',
|
|
72
|
+
default: false,
|
|
73
|
+
}),
|
|
74
|
+
handler: async (args) => {
|
|
75
|
+
const wsDir = resolve(args.path ?? process.cwd());
|
|
76
|
+
const tag = args.tag ?? 'latest';
|
|
77
|
+
const access = args.access;
|
|
78
|
+
const tolerate = args['tolerate-republish'] === true;
|
|
79
|
+
const provenance = args.provenance === true;
|
|
80
|
+
const dryRun = args['dry-run'] === true;
|
|
81
|
+
if (provenance) {
|
|
82
|
+
console.warn('gjsify publish: --provenance recorded but not signed (no sigstore integration yet).');
|
|
83
|
+
}
|
|
84
|
+
// 1. Pack the workspace (rewrites workspace:^, computes integrity)
|
|
85
|
+
const packOpts = { dryRun: true };
|
|
86
|
+
const packed = await packWorkspace(wsDir, packOpts);
|
|
87
|
+
// We need the raw bytes — re-run with destination=null and capture.
|
|
88
|
+
// packWorkspace returns metadata only; for the bytes we re-pack into
|
|
89
|
+
// memory by reading + rebuilding. Cheap because the second pack runs
|
|
90
|
+
// off the same source files. (A future optimization: have
|
|
91
|
+
// packWorkspace optionally return the bytes itself.)
|
|
92
|
+
const tarBytes = await packWorkspaceToBytes(wsDir);
|
|
93
|
+
// 2. Read the workspace's (rewritten) package.json to assemble the
|
|
94
|
+
// publish payload.
|
|
95
|
+
const pkgPath = join(wsDir, 'package.json');
|
|
96
|
+
const pkgSource = readFileSync(pkgPath, 'utf-8');
|
|
97
|
+
const pkg = JSON.parse(pkgSource);
|
|
98
|
+
// We need the rewritten manifest (workspace:^ → resolved) for the
|
|
99
|
+
// payload — packWorkspace already wrote it into the tarball. Mirror
|
|
100
|
+
// the same rewrite here so the registry's "package metadata" matches
|
|
101
|
+
// the tarball's package.json byte-for-byte.
|
|
102
|
+
const rewrittenPkg = await loadRewrittenManifest(wsDir, pkg);
|
|
103
|
+
if (dryRun) {
|
|
104
|
+
const message = {
|
|
105
|
+
ok: true,
|
|
106
|
+
action: 'dry-run',
|
|
107
|
+
name: packed.name,
|
|
108
|
+
version: packed.version,
|
|
109
|
+
filename: packed.filename,
|
|
110
|
+
size: packed.size,
|
|
111
|
+
shasum: packed.shasum,
|
|
112
|
+
integrity: packed.integrity,
|
|
113
|
+
};
|
|
114
|
+
if (args.json)
|
|
115
|
+
process.stdout.write(`${JSON.stringify(message, null, 2)}\n`);
|
|
116
|
+
else
|
|
117
|
+
process.stdout.write(`+ ${packed.name}@${packed.version} (dry-run, ${packed.size} bytes, ${packed.entryCount} files)\n`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// 3. Resolve registry URL + auth
|
|
121
|
+
const npmrc = await loadNpmrc(wsDir);
|
|
122
|
+
const registry = process.env.npm_config_registry ?? registryFor(packed.name, npmrc) ?? DEFAULT_REGISTRY;
|
|
123
|
+
const registryClean = registry.endsWith('/') ? registry.slice(0, -1) : registry;
|
|
124
|
+
// npm-package-arg's escapedName convention for scoped packages:
|
|
125
|
+
// `@${scope-without-leading-@}%2f${name}` (lowercase %2f, literal @).
|
|
126
|
+
// Unscoped: `encodeURIComponent(name)`. Match it exactly — the
|
|
127
|
+
// npm registry is picky about the publish PUT URL shape.
|
|
128
|
+
const escapedName = packed.name.startsWith('@')
|
|
129
|
+
? (() => {
|
|
130
|
+
const slash = packed.name.indexOf('/');
|
|
131
|
+
const scope = packed.name.slice(1, slash);
|
|
132
|
+
const base = packed.name.slice(slash + 1);
|
|
133
|
+
return `@${encodeURIComponent(scope)}%2f${encodeURIComponent(base)}`;
|
|
134
|
+
})()
|
|
135
|
+
: encodeURIComponent(packed.name);
|
|
136
|
+
const url = `${registryClean}/${escapedName}`;
|
|
137
|
+
// npm publish convention: the dist.tarball URL + _attachments key both
|
|
138
|
+
// use the UNSCOPED basename — `cli-0.4.5.tgz`, not
|
|
139
|
+
// `gjsify-cli-0.4.5.tgz`. This is what libnpmpublish does (see
|
|
140
|
+
// `attachments[\`${unscopedName}-${version}.tgz\`]` in
|
|
141
|
+
// libnpmpublish/lib/publish.js). The full scoped filename is what
|
|
142
|
+
// `npm pack` writes to disk, but the registry stores tarballs at
|
|
143
|
+
// the unscoped path.
|
|
144
|
+
const unscopedName = packed.name.includes('/')
|
|
145
|
+
? packed.name.slice(packed.name.indexOf('/') + 1)
|
|
146
|
+
: packed.name;
|
|
147
|
+
const wireFilename = `${unscopedName}-${packed.version}.tgz`;
|
|
148
|
+
const tarballUrl = `${registryClean}/${packed.name}/-/${wireFilename}`;
|
|
149
|
+
// 4. Build payload + PUT
|
|
150
|
+
const payload = buildPublishPayload({
|
|
151
|
+
pkg: rewrittenPkg,
|
|
152
|
+
tag,
|
|
153
|
+
access,
|
|
154
|
+
tarballBytes: tarBytes,
|
|
155
|
+
tarballUrl,
|
|
156
|
+
packed: { ...packed, wireFilename },
|
|
157
|
+
provenance,
|
|
158
|
+
});
|
|
159
|
+
const headers = buildHeaders(url, { npmrc });
|
|
160
|
+
headers['content-type'] = 'application/json';
|
|
161
|
+
headers['accept'] = '*/*';
|
|
162
|
+
if (process.env.GJSIFY_PUBLISH_DEBUG) {
|
|
163
|
+
console.error(`gjsify publish: PUT ${url} (${packed.name}@${packed.version})`);
|
|
164
|
+
console.error(` authorization: ${headers['authorization'] ? '(set)' : '(none)'}`);
|
|
165
|
+
console.error(` payload size: ${JSON.stringify(payload).length} bytes`);
|
|
166
|
+
}
|
|
167
|
+
const res = await fetch(url, {
|
|
168
|
+
method: 'PUT',
|
|
169
|
+
headers,
|
|
170
|
+
body: JSON.stringify(payload),
|
|
171
|
+
});
|
|
172
|
+
if (res.ok) {
|
|
173
|
+
const out = {
|
|
174
|
+
ok: true,
|
|
175
|
+
name: packed.name,
|
|
176
|
+
version: packed.version,
|
|
177
|
+
filename: packed.filename,
|
|
178
|
+
size: packed.size,
|
|
179
|
+
integrity: packed.integrity,
|
|
180
|
+
tag,
|
|
181
|
+
registry: registryClean,
|
|
182
|
+
};
|
|
183
|
+
if (args.json)
|
|
184
|
+
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
185
|
+
else
|
|
186
|
+
process.stdout.write(`+ ${packed.name}@${packed.version}\n`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const text = await res.text().catch(() => '<no body>');
|
|
190
|
+
if (res.status === 409 && tolerate) {
|
|
191
|
+
const out = {
|
|
192
|
+
ok: true,
|
|
193
|
+
action: 'republish-tolerated',
|
|
194
|
+
name: packed.name,
|
|
195
|
+
version: packed.version,
|
|
196
|
+
status: 409,
|
|
197
|
+
};
|
|
198
|
+
if (args.json)
|
|
199
|
+
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
200
|
+
else
|
|
201
|
+
process.stdout.write(`= ${packed.name}@${packed.version} (already published, tolerated)\n`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
console.error(`gjsify publish: ${packed.name}@${packed.version} — ${res.status} ${res.statusText}`);
|
|
205
|
+
console.error(text);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
async function packWorkspaceToBytes(wsDir) {
|
|
210
|
+
// Cheap re-run that writes to a tempdir, then read back. Avoids
|
|
211
|
+
// duplicating the file-walking + tar-building logic here.
|
|
212
|
+
const tmp = `/tmp/gjsify-publish-${process.pid}-${Date.now()}`;
|
|
213
|
+
const res = await packWorkspace(wsDir, { destination: tmp, dryRun: false });
|
|
214
|
+
if (!res.absolutePath)
|
|
215
|
+
throw new Error('gjsify publish: pack did not produce a file');
|
|
216
|
+
const bytes = new Uint8Array(readFileSync(res.absolutePath));
|
|
217
|
+
try {
|
|
218
|
+
(await import('node:fs')).rmSync(res.absolutePath);
|
|
219
|
+
}
|
|
220
|
+
catch { /* best effort */ }
|
|
221
|
+
try {
|
|
222
|
+
(await import('node:fs')).rmdirSync(tmp);
|
|
223
|
+
}
|
|
224
|
+
catch { /* best effort */ }
|
|
225
|
+
return bytes;
|
|
226
|
+
}
|
|
227
|
+
async function loadRewrittenManifest(wsDir, pkg) {
|
|
228
|
+
// Pack + re-read the tarball's package.json. Easier than duplicating the
|
|
229
|
+
// rewrite logic — pack already does it correctly, including handling
|
|
230
|
+
// workspace:^ patterns we'd otherwise have to reimplement here.
|
|
231
|
+
const tmp = `/tmp/gjsify-publish-manifest-${process.pid}-${Date.now()}.tgz`;
|
|
232
|
+
const res = await packWorkspace(wsDir, {
|
|
233
|
+
destination: tmp.substring(0, tmp.lastIndexOf('/')),
|
|
234
|
+
dryRun: false,
|
|
235
|
+
});
|
|
236
|
+
const { rmSync } = await import('node:fs');
|
|
237
|
+
if (!res.absolutePath)
|
|
238
|
+
throw new Error('gjsify publish: pack did not produce a file');
|
|
239
|
+
const { gunzip, parseTar } = await import('@gjsify/tar');
|
|
240
|
+
const bytes = new Uint8Array(readFileSync(res.absolutePath));
|
|
241
|
+
rmSync(res.absolutePath);
|
|
242
|
+
const tar = await gunzip(bytes);
|
|
243
|
+
for (const entry of parseTar(tar)) {
|
|
244
|
+
if (entry.name === 'package/package.json' && entry.body) {
|
|
245
|
+
return JSON.parse(new TextDecoder().decode(entry.body));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return pkg;
|
|
249
|
+
}
|
|
250
|
+
async function loadNpmrc(cwd) {
|
|
251
|
+
// npm CLI's npmrc resolution order (lowest → highest precedence):
|
|
252
|
+
// 1. globalconfig: /etc/npmrc (system)
|
|
253
|
+
// 2. userconfig: $NPM_CONFIG_USERCONFIG (overrides ~/.npmrc)
|
|
254
|
+
// or ~/.npmrc (default)
|
|
255
|
+
// 3. projectconfig: ./.npmrc (closest)
|
|
256
|
+
//
|
|
257
|
+
// actions/setup-node writes the auth-token npmrc to $RUNNER_TEMP/.npmrc
|
|
258
|
+
// and exports NPM_CONFIG_USERCONFIG pointing at it — it does NOT touch
|
|
259
|
+
// ~/.npmrc. Honor the env var so CI authentication works end-to-end.
|
|
260
|
+
const sources = [];
|
|
261
|
+
const projectNpmrc = join(cwd, '.npmrc');
|
|
262
|
+
if (existsSync(projectNpmrc))
|
|
263
|
+
sources.push(readFileSync(projectNpmrc, 'utf-8'));
|
|
264
|
+
const userConfig = process.env.NPM_CONFIG_USERCONFIG;
|
|
265
|
+
if (userConfig && existsSync(userConfig)) {
|
|
266
|
+
sources.push(readFileSync(userConfig, 'utf-8'));
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
const homeNpmrc = join(homedir(), '.npmrc');
|
|
270
|
+
if (existsSync(homeNpmrc))
|
|
271
|
+
sources.push(readFileSync(homeNpmrc, 'utf-8'));
|
|
272
|
+
}
|
|
273
|
+
// Inline `${VAR}` placeholders (npm CLI's expand-on-read behavior).
|
|
274
|
+
// The auth-token npmrc from actions/setup-node ships
|
|
275
|
+
// `_authToken=${NODE_AUTH_TOKEN}` as a literal placeholder; the env var
|
|
276
|
+
// is set on the publish step.
|
|
277
|
+
const merged = sources.join('\n').replace(/\$\{([A-Z_][A-Z0-9_]*)\}/gi, (_, name) => process.env[name] ?? '');
|
|
278
|
+
return parseNpmrc(merged);
|
|
279
|
+
}
|
|
280
|
+
function buildPublishPayload(opts) {
|
|
281
|
+
const { pkg, tag, access, tarballBytes, tarballUrl, packed, provenance } = opts;
|
|
282
|
+
const versionEntry = {
|
|
283
|
+
...pkg,
|
|
284
|
+
_id: `${packed.name}@${packed.version}`,
|
|
285
|
+
dist: {
|
|
286
|
+
integrity: packed.integrity,
|
|
287
|
+
shasum: packed.shasum,
|
|
288
|
+
tarball: tarballUrl,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
if (provenance)
|
|
292
|
+
versionEntry._hasShrinkwrap = false;
|
|
293
|
+
const payload = {
|
|
294
|
+
_id: packed.name,
|
|
295
|
+
name: packed.name,
|
|
296
|
+
description: typeof pkg.description === 'string' ? pkg.description : '',
|
|
297
|
+
'dist-tags': { [tag]: packed.version },
|
|
298
|
+
versions: { [packed.version]: versionEntry },
|
|
299
|
+
readme: '',
|
|
300
|
+
_attachments: {
|
|
301
|
+
[packed.wireFilename]: {
|
|
302
|
+
content_type: 'application/octet-stream',
|
|
303
|
+
data: base64Encode(tarballBytes),
|
|
304
|
+
length: tarballBytes.byteLength,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
if (access)
|
|
309
|
+
payload.access = access;
|
|
310
|
+
return payload;
|
|
311
|
+
}
|
|
312
|
+
function base64Encode(bytes) {
|
|
313
|
+
let str = '';
|
|
314
|
+
const chunk = 0x8000;
|
|
315
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
316
|
+
str += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
317
|
+
}
|
|
318
|
+
return btoa(str);
|
|
319
|
+
}
|
|
@@ -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, } 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
|
|
@@ -25,6 +25,10 @@ await yargs(hideBin(process.argv))
|
|
|
25
25
|
.command(flatpak.command, flatpak.description, flatpak.builder, flatpak.handler)
|
|
26
26
|
.command(foreach.command, foreach.description, foreach.builder, foreach.handler)
|
|
27
27
|
.command(workspace.command, workspace.description, workspace.builder, workspace.handler)
|
|
28
|
+
.command(pack.command, pack.description, pack.builder, pack.handler)
|
|
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)
|
|
28
32
|
.demandCommand(1)
|
|
29
33
|
.help()
|
|
30
34
|
.parseAsync();
|