@gjsify/cli 0.4.27 → 0.4.29
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 +34 -32
- package/lib/actions/barrels-generate.js +1 -5
- package/lib/actions/build.d.ts +3 -3
- package/lib/actions/build.js +56 -64
- package/lib/bundler-pick.d.ts +3 -3
- package/lib/bundler-pick.js +5 -6
- package/lib/commands/build.js +37 -31
- package/lib/commands/check.js +3 -3
- package/lib/commands/fix.js +33 -23
- package/lib/commands/flatpak/build.js +6 -2
- package/lib/commands/flatpak/check.js +9 -3
- package/lib/commands/flatpak/ci.js +1 -2
- package/lib/commands/flatpak/deps.js +1 -2
- package/lib/commands/flatpak/diff.js +2 -6
- package/lib/commands/flatpak/init.js +19 -19
- package/lib/commands/flatpak/release.js +2 -2
- package/lib/commands/flatpak/scaffold.js +3 -11
- package/lib/commands/flatpak/sync-flathub.js +4 -8
- package/lib/commands/flatpak/utils.js +1 -6
- package/lib/commands/foreach.js +5 -14
- package/lib/commands/format.js +54 -41
- package/lib/commands/gettext.js +2 -10
- package/lib/commands/gresource.js +2 -8
- package/lib/commands/gsettings.js +1 -3
- package/lib/commands/install.js +13 -6
- package/lib/commands/lint.d.ts +1 -1
- package/lib/commands/lint.js +22 -22
- package/lib/commands/pack.js +29 -17
- package/lib/commands/publish.d.ts +1 -0
- package/lib/commands/publish.js +113 -21
- package/lib/commands/run.js +2 -6
- package/lib/commands/self-update.js +36 -8
- package/lib/commands/showcase.js +1 -1
- package/lib/commands/system-check.js +8 -11
- package/lib/commands/test.js +12 -8
- package/lib/commands/uninstall.js +1 -3
- package/lib/commands/upgrade.d.ts +1 -1
- package/lib/commands/upgrade.js +109 -120
- package/lib/commands/workspace.js +1 -3
- package/lib/config.js +18 -13
- package/lib/index.js +21 -0
- package/lib/templates/install.mjs.tmpl +20 -14
- package/lib/templates/oxfmtrc.tmpl +54 -0
- package/lib/templates/oxlintrc.json.tmpl +35 -0
- package/lib/types/config-data.d.ts +23 -13
- package/lib/utils/check-system-deps.js +10 -4
- package/lib/utils/detect-native-packages.js +1 -1
- package/lib/utils/dlx-cache.js +2 -7
- package/lib/utils/install-backend-native.d.ts +2 -2
- package/lib/utils/install-backend-native.js +72 -63
- package/lib/utils/install-backend.d.ts +13 -0
- package/lib/utils/install-backend.js +2 -1
- package/lib/utils/install-global.js +1 -3
- package/lib/utils/normalize-bundler-options.js +52 -17
- package/lib/utils/oxc-resolve.d.ts +63 -0
- package/lib/utils/oxc-resolve.js +264 -0
- package/lib/utils/pkg-json-edit.js +1 -6
- package/lib/utils/run-gjs.js +1 -4
- package/lib/utils/run-lifecycle-script.js +3 -7
- package/lib/utils/workspace-root.js +3 -1
- package/package.json +17 -17
- package/lib/templates/biome.json.tmpl +0 -79
- package/lib/utils/biome-resolve.d.ts +0 -47
- package/lib/utils/biome-resolve.js +0 -204
package/lib/commands/lint.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
// `gjsify lint` — wraps
|
|
1
|
+
// `gjsify lint` — wraps oxlint.
|
|
2
2
|
//
|
|
3
|
-
// Sibling of `gjsify format`. Spawns
|
|
4
|
-
// (
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Sibling of `gjsify format`. Spawns oxlint via its Node launcher
|
|
4
|
+
// (`node_modules/oxlint/bin/oxlint` → `dist/cli.js`) — NOT a bare binary —
|
|
5
|
+
// because oxlint's JS-plugin host (used by the internal
|
|
6
|
+
// `oxlint-plugin-gjsify` rule) lives in the JS launcher. Default behaviour:
|
|
7
|
+
// report-only. Pass `--fix` for oxlint's safe-fix mode, or use `gjsify fix`
|
|
8
|
+
// for the combined oxfmt + oxlint --fix surface.
|
|
7
9
|
import { resolve } from 'node:path';
|
|
8
|
-
import {
|
|
10
|
+
import { OxcNotFoundError, findOxlintConfig, printOxcNotFound, runOxlint } from '../utils/oxc-resolve.js';
|
|
9
11
|
export const lintCommand = {
|
|
10
12
|
command: 'lint [paths..]',
|
|
11
|
-
description: 'Run
|
|
13
|
+
description: 'Run oxlint diagnostics (spawned via its Node launcher to support JS plugins).',
|
|
12
14
|
builder: (yargs) => {
|
|
13
15
|
return yargs
|
|
14
16
|
.positional('paths', {
|
|
@@ -16,41 +18,39 @@ export const lintCommand = {
|
|
|
16
18
|
type: 'string',
|
|
17
19
|
array: true,
|
|
18
20
|
})
|
|
19
|
-
.option('
|
|
21
|
+
.option('fix', {
|
|
20
22
|
description: 'Apply safe lint fixes in place.',
|
|
21
23
|
type: 'boolean',
|
|
22
24
|
default: false,
|
|
23
25
|
})
|
|
24
26
|
.option('config-path', {
|
|
25
|
-
description: 'Path to
|
|
27
|
+
description: 'Path to an .oxlintrc.json. Default: walks up from cwd to find one.',
|
|
26
28
|
type: 'string',
|
|
27
29
|
normalize: true,
|
|
28
30
|
})
|
|
29
31
|
.option('verbose', {
|
|
30
|
-
description: 'Echo the resolved
|
|
32
|
+
description: 'Echo the resolved oxlint launcher + args before spawning.',
|
|
31
33
|
type: 'boolean',
|
|
32
34
|
default: false,
|
|
33
35
|
});
|
|
34
36
|
},
|
|
35
37
|
handler: async (args) => {
|
|
36
38
|
const cwd = process.cwd();
|
|
37
|
-
const paths = args.paths?.length
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
biomeArgs.push('--write');
|
|
43
|
-
const configPath = args.configPath ?? findBiomeConfig(cwd) ?? undefined;
|
|
39
|
+
const paths = args.paths?.length ? args.paths : ['.'];
|
|
40
|
+
const oxlintArgs = [];
|
|
41
|
+
if (args.fix)
|
|
42
|
+
oxlintArgs.push('--fix');
|
|
43
|
+
const configPath = args.configPath ?? findOxlintConfig(cwd) ?? undefined;
|
|
44
44
|
if (configPath)
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
oxlintArgs.push('--config', resolve(configPath));
|
|
46
|
+
oxlintArgs.push(...paths);
|
|
47
47
|
try {
|
|
48
|
-
const code = await
|
|
48
|
+
const code = await runOxlint(oxlintArgs, { cwd, verbose: args.verbose });
|
|
49
49
|
process.exitCode = code;
|
|
50
50
|
}
|
|
51
51
|
catch (err) {
|
|
52
|
-
if (err instanceof
|
|
53
|
-
|
|
52
|
+
if (err instanceof OxcNotFoundError) {
|
|
53
|
+
printOxcNotFound(err);
|
|
54
54
|
process.exitCode = 1;
|
|
55
55
|
return;
|
|
56
56
|
}
|
package/lib/commands/pack.js
CHANGED
|
@@ -113,15 +113,11 @@ export async function packWorkspace(wsDir, opts = {}) {
|
|
|
113
113
|
// mutated it (e.g. a `prepack` that injects build metadata into
|
|
114
114
|
// package.json fields). Rare but legal — npm pack does the same.
|
|
115
115
|
const sourceAfterScripts = readFileSync(pkgPath, 'utf-8');
|
|
116
|
-
const pkgAfterScripts = sourceAfterScripts === originalSource
|
|
117
|
-
? pkg
|
|
118
|
-
: JSON.parse(sourceAfterScripts);
|
|
116
|
+
const pkgAfterScripts = sourceAfterScripts === originalSource ? pkg : JSON.parse(sourceAfterScripts);
|
|
119
117
|
// Rewrite workspace:^/~/* deps to resolved npm version ranges, mirroring
|
|
120
118
|
// yarn's auto-rewrite at publish time. Done in-memory only — the source
|
|
121
119
|
// package.json on disk is never mutated by `gjsify pack`.
|
|
122
|
-
const rewrittenPkg = opts.skipWorkspaceRewrite
|
|
123
|
-
? pkgAfterScripts
|
|
124
|
-
: rewriteWorkspaceDeps(pkgAfterScripts, wsDir);
|
|
120
|
+
const rewrittenPkg = opts.skipWorkspaceRewrite ? pkgAfterScripts : rewriteWorkspaceDeps(pkgAfterScripts, wsDir);
|
|
125
121
|
const rewrittenSource = JSON.stringify(rewrittenPkg, null, indentOf(sourceAfterScripts)) + '\n';
|
|
126
122
|
// Collect files according to the package.json `files` field (or npm's
|
|
127
123
|
// default set). The package.json itself is always included with the
|
|
@@ -148,9 +144,7 @@ export async function packWorkspace(wsDir, opts = {}) {
|
|
|
148
144
|
const tarBytes = createTarball(entries);
|
|
149
145
|
const gzipBytes = await gzip(tarBytes);
|
|
150
146
|
// npm filename: scope replaced with leading dash. "@gjsify/foo" → "gjsify-foo".
|
|
151
|
-
const filenameBase = name.startsWith('@')
|
|
152
|
-
? name.slice(1).replace('/', '-')
|
|
153
|
-
: name;
|
|
147
|
+
const filenameBase = name.startsWith('@') ? name.slice(1).replace('/', '-') : name;
|
|
154
148
|
const filename = `${filenameBase}-${version}.tgz`;
|
|
155
149
|
const sha1 = createHash('sha1').update(gzipBytes).digest('hex');
|
|
156
150
|
const sha512 = createHash('sha512').update(gzipBytes).digest('base64');
|
|
@@ -188,7 +182,9 @@ export async function packWorkspace(wsDir, opts = {}) {
|
|
|
188
182
|
*/
|
|
189
183
|
function collectFiles(wsDir, pkg) {
|
|
190
184
|
const always = forceIncluded(pkg);
|
|
191
|
-
const filesField = Array.isArray(pkg.files)
|
|
185
|
+
const filesField = Array.isArray(pkg.files)
|
|
186
|
+
? pkg.files.filter((f) => typeof f === 'string')
|
|
187
|
+
: null;
|
|
192
188
|
let candidates;
|
|
193
189
|
if (filesField) {
|
|
194
190
|
candidates = expandFilesPatterns(wsDir, filesField);
|
|
@@ -208,12 +204,25 @@ function collectFiles(wsDir, pkg) {
|
|
|
208
204
|
}
|
|
209
205
|
return [...out].sort();
|
|
210
206
|
}
|
|
211
|
-
const ALWAYS_INCLUDED_BASENAMES = new Set(['package.json', 'README', 'README.md', 'LICENSE', 'LICENSE.md', 'NOTICE', 'NOTICE.md']);
|
|
212
207
|
const NEVER_INCLUDED_BASENAMES = new Set([
|
|
213
|
-
'.git',
|
|
214
|
-
'
|
|
215
|
-
'
|
|
216
|
-
'.
|
|
208
|
+
'.git',
|
|
209
|
+
'.svn',
|
|
210
|
+
'.hg',
|
|
211
|
+
'.gitignore',
|
|
212
|
+
'.gitattributes',
|
|
213
|
+
'.npmrc',
|
|
214
|
+
'CVS',
|
|
215
|
+
'.DS_Store',
|
|
216
|
+
'node_modules',
|
|
217
|
+
'.npmignore',
|
|
218
|
+
'package-lock.json',
|
|
219
|
+
'gjsify-lock.json',
|
|
220
|
+
'yarn.lock',
|
|
221
|
+
'yarn-error.log',
|
|
222
|
+
'.yarn',
|
|
223
|
+
'.pnp.cjs',
|
|
224
|
+
'.pnp.loader.mjs',
|
|
225
|
+
'tsconfig.tsbuildinfo',
|
|
217
226
|
]);
|
|
218
227
|
function forceIncluded(pkg) {
|
|
219
228
|
const out = new Set();
|
|
@@ -292,7 +301,7 @@ function loadIgnore(wsDir) {
|
|
|
292
301
|
const npmIgnorePath = join(wsDir, '.npmignore');
|
|
293
302
|
const gitIgnorePath = join(wsDir, '.gitignore');
|
|
294
303
|
const patterns = [];
|
|
295
|
-
const sourcePath = existsSync(npmIgnorePath) ? npmIgnorePath :
|
|
304
|
+
const sourcePath = existsSync(npmIgnorePath) ? npmIgnorePath : existsSync(gitIgnorePath) ? gitIgnorePath : null;
|
|
296
305
|
if (sourcePath) {
|
|
297
306
|
const lines = readFileSync(sourcePath, 'utf-8').split('\n');
|
|
298
307
|
for (const raw of lines) {
|
|
@@ -319,7 +328,10 @@ function globToRegex(glob) {
|
|
|
319
328
|
// Escape regex metachars except *,?,/
|
|
320
329
|
pat = pat.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
321
330
|
// ** → .* * → [^/]* ? → [^/]
|
|
322
|
-
pat = pat
|
|
331
|
+
pat = pat
|
|
332
|
+
.replace(/\*\*/g, '__DOUBLESTAR__')
|
|
333
|
+
.replace(/\*/g, '[^/]*')
|
|
334
|
+
.replace(/__DOUBLESTAR__/g, '.*');
|
|
323
335
|
pat = pat.replace(/\?/g, '[^/]');
|
|
324
336
|
return new RegExp(`^${pat}($|/)`);
|
|
325
337
|
}
|
package/lib/commands/publish.js
CHANGED
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
import { existsSync, readFileSync } from 'node:fs';
|
|
37
37
|
import { homedir } from 'node:os';
|
|
38
38
|
import { join, resolve } from 'node:path';
|
|
39
|
-
import { DEFAULT_REGISTRY, parseNpmrc, registryFor, buildHeaders
|
|
39
|
+
import { DEFAULT_REGISTRY, parseNpmrc, registryFor, buildHeaders } from '@gjsify/npm-registry';
|
|
40
40
|
import { packWorkspace } from './pack.js';
|
|
41
|
-
import { getNpmTrustedToken, hasGithubOidcEnv, OidcExchangeError, OidcUnavailableError
|
|
41
|
+
import { getNpmTrustedToken, hasGithubOidcEnv, OidcExchangeError, OidcUnavailableError } from '../utils/npm-oidc.js';
|
|
42
42
|
export const publishCommand = {
|
|
43
43
|
command: 'publish [path]',
|
|
44
44
|
description: 'Pack + upload the workspace at <path> (default: cwd) to its npm registry. Drop-in for `npm publish` with workspace:^ rewrite handled automatically.',
|
|
@@ -52,6 +52,10 @@ export const publishCommand = {
|
|
|
52
52
|
.option('access', {
|
|
53
53
|
description: 'Package access — `public` or `restricted` (required for first publish of scoped packages on the public registry).',
|
|
54
54
|
type: 'string',
|
|
55
|
+
})
|
|
56
|
+
.option('otp', {
|
|
57
|
+
description: 'npm 2FA one-time code; sent as the `npm-otp` header. Required for manual publishes from a 2FA-enabled account.',
|
|
58
|
+
type: 'string',
|
|
55
59
|
})
|
|
56
60
|
.option('tolerate-republish', {
|
|
57
61
|
description: 'Treat "version already published" as success — covers both classic 409 Conflict and the npm OIDC-path 403 Forbidden + `"previously published"` body shape. Matches yarn `--tolerate-republish`.',
|
|
@@ -64,7 +68,7 @@ export const publishCommand = {
|
|
|
64
68
|
default: false,
|
|
65
69
|
})
|
|
66
70
|
.option('provenance', {
|
|
67
|
-
description:
|
|
71
|
+
description: "Pass-through flag — recorded in the payload but no signing happens (gjsify doesn't ship a sigstore signer yet).",
|
|
68
72
|
type: 'boolean',
|
|
69
73
|
default: false,
|
|
70
74
|
})
|
|
@@ -95,6 +99,7 @@ export const publishCommand = {
|
|
|
95
99
|
const wsDir = resolve(args.path ?? process.cwd());
|
|
96
100
|
const tag = args.tag ?? 'latest';
|
|
97
101
|
const access = args.access;
|
|
102
|
+
const otp = args.otp;
|
|
98
103
|
const tolerate = args['tolerate-republish'] === true;
|
|
99
104
|
const tolerateUntrustedNew = args['tolerate-untrusted-new'] === true;
|
|
100
105
|
const provenance = args.provenance === true;
|
|
@@ -222,9 +227,7 @@ export const publishCommand = {
|
|
|
222
227
|
// libnpmpublish/lib/publish.js). The full scoped filename is what
|
|
223
228
|
// `npm pack` writes to disk, but the registry stores tarballs at
|
|
224
229
|
// the unscoped path.
|
|
225
|
-
const unscopedName = packed.name.includes('/')
|
|
226
|
-
? packed.name.slice(packed.name.indexOf('/') + 1)
|
|
227
|
-
: packed.name;
|
|
230
|
+
const unscopedName = packed.name.includes('/') ? packed.name.slice(packed.name.indexOf('/') + 1) : packed.name;
|
|
228
231
|
const wireFilename = `${unscopedName}-${packed.version}.tgz`;
|
|
229
232
|
const tarballUrl = `${registryClean}/${packed.name}/-/${wireFilename}`;
|
|
230
233
|
// 4. Build payload + PUT
|
|
@@ -240,13 +243,17 @@ export const publishCommand = {
|
|
|
240
243
|
const headers = buildHeaders(url, { npmrc });
|
|
241
244
|
headers['content-type'] = 'application/json';
|
|
242
245
|
headers['accept'] = '*/*';
|
|
246
|
+
// 2FA OTP — sent as the `npm-otp` header (npm-registry-fetch convention,
|
|
247
|
+
// verified in refs/npm-cli/node_modules/npm-registry-fetch/lib/index.js
|
|
248
|
+
// line ~243: `if (opts.otp) headers['npm-otp'] = opts.otp`).
|
|
249
|
+
if (otp)
|
|
250
|
+
headers['npm-otp'] = otp;
|
|
243
251
|
// Trusted Publishing path. `--trusted` forces OIDC (errors if env
|
|
244
252
|
// vars are missing); the default `undefined` triggers auto-detect:
|
|
245
253
|
// OIDC is used iff GitHub OIDC env vars are present AND no
|
|
246
254
|
// `NODE_AUTH_TOKEN` is set. With `NODE_AUTH_TOKEN` set the user has
|
|
247
255
|
// explicitly opted into token auth, so we don't shadow their choice.
|
|
248
|
-
const wantTrusted = trustedFlag === true ||
|
|
249
|
-
(trustedFlag === undefined && hasGithubOidcEnv() && !process.env.NODE_AUTH_TOKEN);
|
|
256
|
+
const wantTrusted = trustedFlag === true || (trustedFlag === undefined && hasGithubOidcEnv() && !process.env.NODE_AUTH_TOKEN);
|
|
250
257
|
let authMode = 'token';
|
|
251
258
|
if (wantTrusted) {
|
|
252
259
|
try {
|
|
@@ -271,9 +278,7 @@ export const publishCommand = {
|
|
|
271
278
|
// Trusted Publisher bootstrap"). Skip such a package when
|
|
272
279
|
// --tolerate-untrusted-new is set so one un-bootstrapped
|
|
273
280
|
// package doesn't break the entire serialized publish loop.
|
|
274
|
-
const isUntrustedNewPackage = err instanceof OidcExchangeError &&
|
|
275
|
-
err.status === 404 &&
|
|
276
|
-
/package not found/i.test(err.body);
|
|
281
|
+
const isUntrustedNewPackage = err instanceof OidcExchangeError && err.status === 404 && /package not found/i.test(err.body);
|
|
277
282
|
if (isUntrustedNewPackage && tolerateUntrustedNew) {
|
|
278
283
|
const headerMsg = `${packed.name}@${packed.version} (skipped — no Trusted Publisher on npm, see AGENTS.md "New @gjsify/* package: first-publish + Trusted Publisher bootstrap")`;
|
|
279
284
|
if (args.json) {
|
|
@@ -306,13 +311,55 @@ export const publishCommand = {
|
|
|
306
311
|
console.error(`gjsify publish: PUT ${url} (${packed.name}@${packed.version})`);
|
|
307
312
|
console.error(` auth-mode: ${authMode}`);
|
|
308
313
|
console.error(` authorization: ${headers['authorization'] ? '(set)' : '(none)'}`);
|
|
314
|
+
console.error(` otp: ${headers['npm-otp'] ? '(set)' : '(none)'}`);
|
|
309
315
|
console.error(` payload size: ${JSON.stringify(payload).length} bytes`);
|
|
310
316
|
}
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
317
|
+
const bodyStr = JSON.stringify(payload);
|
|
318
|
+
// Helper that PUTs with a given header set and returns the response.
|
|
319
|
+
async function doPut(reqHeaders) {
|
|
320
|
+
return fetch(url, {
|
|
321
|
+
method: 'PUT',
|
|
322
|
+
headers: reqHeaders,
|
|
323
|
+
body: bodyStr,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
let res = await doPut(headers);
|
|
327
|
+
// EOTP handling — mirrors npm's otplease.js (refs/npm-cli/lib/utils/auth.js).
|
|
328
|
+
// npm signals "OTP required" via:
|
|
329
|
+
// - HTTP 401 + `www-authenticate` header containing "otp"
|
|
330
|
+
// (refs/npm-cli/node_modules/npm-registry-fetch/lib/check-response.js
|
|
331
|
+
// line ~83: `auth.indexOf('otp') !== -1` → HttpErrorAuthOTP)
|
|
332
|
+
// - HTTP 401 + body containing "one-time pass" (heuristic for
|
|
333
|
+
// malformed responses missing the www-authenticate header —
|
|
334
|
+
// same check-response.js lines ~92-98)
|
|
335
|
+
// We check both shapes, exactly matching npm's logic.
|
|
336
|
+
// If the caller already supplied --otp we skip this (they set the
|
|
337
|
+
// header; if the registry still 401s that is a wrong-code error).
|
|
338
|
+
if (!otp && res.status === 401) {
|
|
339
|
+
const wwwAuth = res.headers.get('www-authenticate') ?? '';
|
|
340
|
+
const body401 = await res.text().catch(() => '');
|
|
341
|
+
const needsOtp = wwwAuth.toLowerCase().split(/,\s*/).includes('otp') || /one-time pass/i.test(body401);
|
|
342
|
+
if (needsOtp) {
|
|
343
|
+
// Interactive path: if stdin is a TTY, prompt and retry once.
|
|
344
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
345
|
+
process.stdout.write('This operation requires a one-time password.\nEnter OTP: ');
|
|
346
|
+
const enteredOtp = await readLineFromStdin();
|
|
347
|
+
if (enteredOtp) {
|
|
348
|
+
const retryHeaders = { ...headers, 'npm-otp': enteredOtp };
|
|
349
|
+
res = await doPut(retryHeaders);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
console.error(`gjsify publish: no OTP entered — re-run with \`--otp <code>\``);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
// Non-interactive: give the maintainer a clear, actionable message.
|
|
358
|
+
console.error(`gjsify publish: npm requires a 2FA one-time code — re-run with \`--otp <code>\``);
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
316
363
|
if (res.ok) {
|
|
317
364
|
const out = {
|
|
318
365
|
ok: true,
|
|
@@ -341,8 +388,7 @@ export const publishCommand = {
|
|
|
341
388
|
// Both are intentionally tolerated under --tolerate-republish so
|
|
342
389
|
// that re-running a release workflow after a partial failure does
|
|
343
390
|
// not error on the already-published packages.
|
|
344
|
-
const isRepublishConflict = res.status === 409 ||
|
|
345
|
-
(res.status === 403 && /previously published/i.test(text));
|
|
391
|
+
const isRepublishConflict = res.status === 409 || (res.status === 403 && /previously published/i.test(text));
|
|
346
392
|
if (isRepublishConflict && tolerate) {
|
|
347
393
|
const out = {
|
|
348
394
|
ok: true,
|
|
@@ -380,11 +426,15 @@ async function packWorkspaceToBytes(wsDir) {
|
|
|
380
426
|
try {
|
|
381
427
|
(await import('node:fs')).rmSync(res.absolutePath);
|
|
382
428
|
}
|
|
383
|
-
catch {
|
|
429
|
+
catch {
|
|
430
|
+
/* best effort */
|
|
431
|
+
}
|
|
384
432
|
try {
|
|
385
433
|
(await import('node:fs')).rmdirSync(tmp);
|
|
386
434
|
}
|
|
387
|
-
catch {
|
|
435
|
+
catch {
|
|
436
|
+
/* best effort */
|
|
437
|
+
}
|
|
388
438
|
return bytes;
|
|
389
439
|
}
|
|
390
440
|
async function loadRewrittenManifest(wsDir, pkg) {
|
|
@@ -437,7 +487,9 @@ async function loadNpmrc(cwd) {
|
|
|
437
487
|
// The auth-token npmrc from actions/setup-node ships
|
|
438
488
|
// `_authToken=${NODE_AUTH_TOKEN}` as a literal placeholder; the env var
|
|
439
489
|
// is set on the publish step.
|
|
440
|
-
const merged = sources
|
|
490
|
+
const merged = sources
|
|
491
|
+
.join('\n')
|
|
492
|
+
.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/gi, (_, name) => process.env[name] ?? '');
|
|
441
493
|
return parseNpmrc(merged);
|
|
442
494
|
}
|
|
443
495
|
function buildPublishPayload(opts) {
|
|
@@ -480,6 +532,46 @@ function base64Encode(bytes) {
|
|
|
480
532
|
}
|
|
481
533
|
return btoa(str);
|
|
482
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* Read a single line from stdin (blocking via async iterator). Used for the
|
|
537
|
+
* interactive OTP prompt path (mirrors npm's `read.otp()`). Resolves with the
|
|
538
|
+
* trimmed line or an empty string if stdin closed without input.
|
|
539
|
+
*/
|
|
540
|
+
async function readLineFromStdin() {
|
|
541
|
+
return new Promise((resolve) => {
|
|
542
|
+
let buf = '';
|
|
543
|
+
const onData = (chunk) => {
|
|
544
|
+
buf += typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
|
|
545
|
+
const nl = buf.indexOf('\n');
|
|
546
|
+
if (nl >= 0) {
|
|
547
|
+
cleanup();
|
|
548
|
+
resolve(buf.slice(0, nl).trim());
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
const onEnd = () => {
|
|
552
|
+
cleanup();
|
|
553
|
+
resolve(buf.trim());
|
|
554
|
+
};
|
|
555
|
+
const onError = () => {
|
|
556
|
+
cleanup();
|
|
557
|
+
resolve('');
|
|
558
|
+
};
|
|
559
|
+
const cleanup = () => {
|
|
560
|
+
process.stdin.removeListener('data', onData);
|
|
561
|
+
process.stdin.removeListener('end', onEnd);
|
|
562
|
+
process.stdin.removeListener('error', onError);
|
|
563
|
+
if (typeof process.stdin.unref === 'function') {
|
|
564
|
+
process.stdin.unref?.();
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
process.stdin.setEncoding('utf-8');
|
|
568
|
+
if (process.stdin.isPaused())
|
|
569
|
+
process.stdin.resume();
|
|
570
|
+
process.stdin.once('data', onData);
|
|
571
|
+
process.stdin.once('end', onEnd);
|
|
572
|
+
process.stdin.once('error', onError);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
483
575
|
function handleOidcFailure(err, packageName, asJson) {
|
|
484
576
|
if (err instanceof OidcUnavailableError) {
|
|
485
577
|
const msg = `gjsify publish: OIDC not available — ${err.message}`;
|
package/lib/commands/run.js
CHANGED
|
@@ -108,9 +108,7 @@ async function runScript(script, extraArgs) {
|
|
|
108
108
|
// (stdout is always a pipe there, but the GHA log viewer renders ANSI
|
|
109
109
|
// fine). Respect user overrides: FORCE_COLOR=0 or NO_COLOR keeps
|
|
110
110
|
// colors off.
|
|
111
|
-
const colorEnv = process.env.FORCE_COLOR !== undefined || process.env.NO_COLOR !== undefined
|
|
112
|
-
? {}
|
|
113
|
-
: { FORCE_COLOR: '1' };
|
|
111
|
+
const colorEnv = process.env.FORCE_COLOR !== undefined || process.env.NO_COLOR !== undefined ? {} : { FORCE_COLOR: '1' };
|
|
114
112
|
const env = {
|
|
115
113
|
...process.env,
|
|
116
114
|
...colorEnv,
|
|
@@ -119,9 +117,7 @@ async function runScript(script, extraArgs) {
|
|
|
119
117
|
npm_package_name: pkg.name ?? '',
|
|
120
118
|
npm_package_version: pkg.version ?? '',
|
|
121
119
|
};
|
|
122
|
-
const fullCmd = extraArgs.length > 0
|
|
123
|
-
? `${literal} ${extraArgs.map(shellEscape).join(' ')}`
|
|
124
|
-
: literal;
|
|
120
|
+
const fullCmd = extraArgs.length > 0 ? `${literal} ${extraArgs.map(shellEscape).join(' ')}` : literal;
|
|
125
121
|
// ensureMainLoop() (called inside spawn) keeps GJS alive after the
|
|
126
122
|
// child exits — without an explicit process.exit() the success path
|
|
127
123
|
// would park the loop forever. The error path already exits.
|
|
@@ -76,18 +76,34 @@ export const selfUpdateCommand = {
|
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
78
|
if (args.check) {
|
|
79
|
-
console.log(currentVersion
|
|
80
|
-
? `Update available: v${currentVersion} → v${target}`
|
|
81
|
-
: `Install required: → v${target}`);
|
|
79
|
+
console.log(currentVersion ? `Update available: v${currentVersion} → v${target}` : `Install required: → v${target}`);
|
|
82
80
|
process.exit(1);
|
|
83
81
|
return;
|
|
84
82
|
}
|
|
85
83
|
console.log(`Installing ${PACKAGE_NAME}@${target} ...`);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
// `@gjsify/cli` is a self-contained GJS bundle: every workspace dep
|
|
85
|
+
// (`@gjsify/node-polyfills`, `@gjsify/v8`, …) is bundled into the
|
|
86
|
+
// published `dist/cli.gjs.mjs` artifact and must NOT be resolved as
|
|
87
|
+
// a separate npm package. `skipDeps: true` limits the native install
|
|
88
|
+
// backend to fetching the top-level tarball only, avoiding spurious
|
|
89
|
+
// packument requests for workspace-internal packages that are not
|
|
90
|
+
// published to the public registry (which previously caused a
|
|
91
|
+
// 406 Not Acceptable → fatal crash on the `@gjsify/v8` packument).
|
|
92
|
+
try {
|
|
93
|
+
await installPackages({
|
|
94
|
+
prefix: layout.prefix,
|
|
95
|
+
specs: [`${PACKAGE_NAME}@${target}`],
|
|
96
|
+
verbose: false,
|
|
97
|
+
skipDeps: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
102
|
+
console.error(`self-update: install failed — ${msg}`);
|
|
103
|
+
console.error(`Re-run \`gjsify self-update\` to retry.`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
91
107
|
const linked = linkGlobalBins([PACKAGE_NAME], layout);
|
|
92
108
|
if (linked.length === 0) {
|
|
93
109
|
console.warn('self-update: install completed but no bins were linked — package.json may be missing a `bin` field.');
|
|
@@ -107,6 +123,18 @@ export const selfUpdateCommand = {
|
|
|
107
123
|
*/
|
|
108
124
|
function readCurrentVersion() {
|
|
109
125
|
try {
|
|
126
|
+
// Escape hatch for tests: point GJSIFY_CLI_PACKAGE_JSON at a synthetic
|
|
127
|
+
// package.json to override the upward-walk discovery. When set to a
|
|
128
|
+
// file whose `name` does not equal PACKAGE_NAME the function returns
|
|
129
|
+
// null, which is the correct production behaviour for "version unknown".
|
|
130
|
+
const override = process.env.GJSIFY_CLI_PACKAGE_JSON;
|
|
131
|
+
if (override) {
|
|
132
|
+
const pkg = JSON.parse(readFileSync(override, 'utf-8'));
|
|
133
|
+
if (pkg.name === PACKAGE_NAME && typeof pkg.version === 'string') {
|
|
134
|
+
return pkg.version;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
110
138
|
const here = fileURLToPath(import.meta.url);
|
|
111
139
|
let dir = dirname(resolve(here));
|
|
112
140
|
for (let i = 0; i < 8 && dir !== dirname(dir); i++) {
|
package/lib/commands/showcase.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { discoverShowcases, findShowcase } from '../utils/discover-showcases.js';
|
|
2
|
-
import { runMinimalChecks, detectPackageManager, buildInstallCommand
|
|
2
|
+
import { runMinimalChecks, detectPackageManager, buildInstallCommand } from '../utils/check-system-deps.js';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
@@ -3,8 +3,7 @@ export const systemCheckCommand = {
|
|
|
3
3
|
command: 'system-check',
|
|
4
4
|
description: 'Check that required system dependencies (GJS, GTK4, libsoup3, …) are installed. Optional dependencies are detected only when their @gjsify/* package is in your project. (Previously called `gjsify check`; the bare name now runs TypeScript checks across the workspace — see `gjsify check --help`.)',
|
|
5
5
|
builder: (yargs) => {
|
|
6
|
-
return yargs
|
|
7
|
-
.option('json', {
|
|
6
|
+
return yargs.option('json', {
|
|
8
7
|
description: 'Output results as JSON',
|
|
9
8
|
type: 'boolean',
|
|
10
9
|
default: false,
|
|
@@ -13,8 +12,8 @@ export const systemCheckCommand = {
|
|
|
13
12
|
handler: async (args) => {
|
|
14
13
|
const results = runAllChecks(process.cwd());
|
|
15
14
|
const pm = detectPackageManager();
|
|
16
|
-
const missingRequired = results.filter(r => !r.found && r.severity === 'required');
|
|
17
|
-
const missingOptional = results.filter(r => !r.found && r.severity === 'optional');
|
|
15
|
+
const missingRequired = results.filter((r) => !r.found && r.severity === 'required');
|
|
16
|
+
const missingOptional = results.filter((r) => !r.found && r.severity === 'optional');
|
|
18
17
|
const allMissing = [...missingRequired, ...missingOptional];
|
|
19
18
|
if (args.json) {
|
|
20
19
|
console.log(JSON.stringify({ packageManager: pm, deps: results }, null, 2));
|
|
@@ -23,8 +22,8 @@ export const systemCheckCommand = {
|
|
|
23
22
|
return;
|
|
24
23
|
}
|
|
25
24
|
console.log('System dependency check\n');
|
|
26
|
-
const required = results.filter(r => r.severity === 'required');
|
|
27
|
-
const optional = results.filter(r => r.severity === 'optional');
|
|
25
|
+
const required = results.filter((r) => r.severity === 'required');
|
|
26
|
+
const optional = results.filter((r) => r.severity === 'optional');
|
|
28
27
|
if (required.length > 0) {
|
|
29
28
|
console.log('Required:');
|
|
30
29
|
for (const dep of required) {
|
|
@@ -39,9 +38,7 @@ export const systemCheckCommand = {
|
|
|
39
38
|
// ⚠ for missing-but-needed-by-installed-packages, ○ for missing-but-not-needed (shouldn't appear in conditional mode)
|
|
40
39
|
const icon = dep.found ? '✓' : '⚠';
|
|
41
40
|
const ver = dep.version ? ` (${dep.version})` : '';
|
|
42
|
-
const requiredBy = dep.requiredBy && dep.requiredBy.length > 0
|
|
43
|
-
? ` — needed by ${dep.requiredBy.join(', ')}`
|
|
44
|
-
: '';
|
|
41
|
+
const requiredBy = dep.requiredBy && dep.requiredBy.length > 0 ? ` — needed by ${dep.requiredBy.join(', ')}` : '';
|
|
45
42
|
console.log(` ${icon} ${dep.name}${ver}${requiredBy}`);
|
|
46
43
|
}
|
|
47
44
|
}
|
|
@@ -51,10 +48,10 @@ export const systemCheckCommand = {
|
|
|
51
48
|
return;
|
|
52
49
|
}
|
|
53
50
|
if (missingRequired.length > 0) {
|
|
54
|
-
console.log(`\nMissing required: ${missingRequired.map(d => d.name).join(', ')}`);
|
|
51
|
+
console.log(`\nMissing required: ${missingRequired.map((d) => d.name).join(', ')}`);
|
|
55
52
|
}
|
|
56
53
|
if (missingOptional.length > 0) {
|
|
57
|
-
console.log(`Missing optional: ${missingOptional.map(d => d.name).join(', ')}`);
|
|
54
|
+
console.log(`Missing optional: ${missingOptional.map((d) => d.name).join(', ')}`);
|
|
58
55
|
}
|
|
59
56
|
const cmd = buildInstallCommand(pm, allMissing);
|
|
60
57
|
if (cmd) {
|
package/lib/commands/test.js
CHANGED
|
@@ -66,13 +66,15 @@ export const testCommand = {
|
|
|
66
66
|
? ['gjs']
|
|
67
67
|
: args.runtime === 'node'
|
|
68
68
|
? ['node']
|
|
69
|
-
:
|
|
69
|
+
: testCfg.runtimes && testCfg.runtimes.length > 0
|
|
70
|
+
? testCfg.runtimes
|
|
71
|
+
: ['gjs', 'node'];
|
|
70
72
|
const results = [];
|
|
71
73
|
for (const runtime of requested) {
|
|
72
74
|
const outfile = join(outdir, `test.${runtime}.mjs`);
|
|
73
75
|
// Build stage (skip if --no-build OR (not --rebuild AND outfile fresher than src)).
|
|
74
76
|
if (args.build !== false) {
|
|
75
|
-
const needsBuild = args.rebuild || !isFresh(outfile, entry
|
|
77
|
+
const needsBuild = args.rebuild || !isFresh(outfile, entry);
|
|
76
78
|
if (needsBuild) {
|
|
77
79
|
const buildStart = Date.now();
|
|
78
80
|
if (args.verbose) {
|
|
@@ -144,11 +146,11 @@ async function buildTestBundle(entry, outfile, runtime, verbose) {
|
|
|
144
146
|
// the merged config; we set it explicitly here so package.json#main /
|
|
145
147
|
// bundler.output.file from the surrounding project don't redirect the
|
|
146
148
|
// bundle elsewhere.
|
|
147
|
-
configData.library = { ...
|
|
149
|
+
configData.library = { ...configData.library };
|
|
148
150
|
configData.bundler = {
|
|
149
|
-
...
|
|
151
|
+
...configData.bundler,
|
|
150
152
|
input: [entry],
|
|
151
|
-
output: { ...
|
|
153
|
+
output: { ...configData.bundler?.output, file: outfile },
|
|
152
154
|
};
|
|
153
155
|
const action = new BuildAction(configData);
|
|
154
156
|
await action.start({ app: runtime, library: false });
|
|
@@ -171,7 +173,7 @@ async function runTestBundle(outfile, runtime) {
|
|
|
171
173
|
});
|
|
172
174
|
}
|
|
173
175
|
/** True when `outfile` exists and is newer than every `.ts`/`.mts` file under the entry's directory tree. */
|
|
174
|
-
function isFresh(outfile, entry
|
|
176
|
+
function isFresh(outfile, entry) {
|
|
175
177
|
if (!existsSync(outfile))
|
|
176
178
|
return false;
|
|
177
179
|
const outMtime = statSync(outfile).mtimeMs;
|
|
@@ -186,7 +188,6 @@ function isFresh(outfile, entry, cwd) {
|
|
|
186
188
|
// On any FS error, force rebuild to stay safe.
|
|
187
189
|
return false;
|
|
188
190
|
}
|
|
189
|
-
void cwd;
|
|
190
191
|
}
|
|
191
192
|
function newestMtimeUnder(path) {
|
|
192
193
|
const st = statSync(path);
|
|
@@ -194,7 +195,10 @@ function newestMtimeUnder(path) {
|
|
|
194
195
|
return st.mtimeMs;
|
|
195
196
|
let max = st.mtimeMs;
|
|
196
197
|
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
197
|
-
if (entry.name === 'node_modules' ||
|
|
198
|
+
if (entry.name === 'node_modules' ||
|
|
199
|
+
entry.name === 'dist' ||
|
|
200
|
+
entry.name === 'lib' ||
|
|
201
|
+
entry.name.startsWith('.')) {
|
|
198
202
|
continue;
|
|
199
203
|
}
|
|
200
204
|
const child = join(path, entry.name);
|
|
@@ -134,9 +134,7 @@ function findBinShimsForPackage(binDir, pkgDir, verbose) {
|
|
|
134
134
|
// Find the `exec [gjs -m] '<target>' "$@"` line; the path may
|
|
135
135
|
// contain `:` from the optional prebuild preamble lines, which
|
|
136
136
|
// is why we anchor to `exec ` rather than the first quoted run.
|
|
137
|
-
const execLine = content
|
|
138
|
-
.split('\n')
|
|
139
|
-
.find((line) => /^exec (?:gjs -m )?'/.test(line));
|
|
137
|
+
const execLine = content.split('\n').find((line) => /^exec (?:gjs -m )?'/.test(line));
|
|
140
138
|
if (!execLine)
|
|
141
139
|
continue;
|
|
142
140
|
const m = execLine.match(/'([^']+)'/);
|