@gjsify/cli 0.4.20 → 0.4.21

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.
@@ -276,17 +276,23 @@ async function workspaceInstall(cwd, args) {
276
276
  continue;
277
277
  const linkPath = join(target.location, 'node_modules', link.depName);
278
278
  mkdirSync(dirname(linkPath), { recursive: true });
279
- // Remove any prior entry (regular dir, broken symlink, file).
279
+ // Remove any prior entry regular dir, broken symlink, file, or
280
+ // a normal symlink left over from a previous install. Using
281
+ // `{ recursive: true, force: true }` handles every shape in one
282
+ // call: `rmSync` no-ops on missing paths under `force: true`, and
283
+ // `recursive: true` covers the directory case. Avoids the EEXIST
284
+ // race a previous lstat-then-branch version hit when the stat's
285
+ // type-discrimination missed an edge case (e.g. broken symlink
286
+ // whose `isSymbolicLink()` returned a non-truthy value through
287
+ // Gio's NOFOLLOW path, leaving a leftover entry that
288
+ // `symlinkSync` would then refuse to overwrite).
280
289
  try {
281
- const stat = lstatSync(linkPath);
282
- if (stat.isSymbolicLink() || stat.isFile()) {
283
- rmSync(linkPath, { force: true });
284
- }
285
- else if (stat.isDirectory()) {
286
- rmSync(linkPath, { recursive: true, force: true });
287
- }
290
+ rmSync(linkPath, { recursive: true, force: true });
291
+ }
292
+ catch { /* unexpected — Gio failure on a path we just lstat'd to
293
+ decide we wanted to remove. The subsequent symlinkSync
294
+ will surface the real reason if there is one. */
288
295
  }
289
- catch { /* ENOENT — fine, nothing to remove */ }
290
296
  // Relative symlink so the repo is portable across checkout paths.
291
297
  const relTarget = relative(dirname(linkPath), link.targetLocation);
292
298
  symlinkSync(relTarget, linkPath);
@@ -4,6 +4,7 @@ interface PublishOptions {
4
4
  tag?: string;
5
5
  access?: string;
6
6
  'tolerate-republish'?: boolean;
7
+ 'tolerate-untrusted-new'?: boolean;
7
8
  provenance?: boolean;
8
9
  'dry-run'?: boolean;
9
10
  json?: boolean;
@@ -57,6 +57,11 @@ export const publishCommand = {
57
57
  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`.',
58
58
  type: 'boolean',
59
59
  default: false,
60
+ })
61
+ .option('tolerate-untrusted-new', {
62
+ description: 'Skip (exit 0) when OIDC token exchange returns `package not found` AND no fallback token is configured — i.e. a never-before-published `@scope/<name>` whose Trusted Publisher entry hasn\'t been set up on npmjs.com yet. Without this flag, one un-bootstrapped new package breaks the entire serialized `gjsify foreach publish` loop. Pair with `--tolerate-republish` in CI release workflows so a fresh-merged package gracefully skips its first CI publish, leaving the manual-bootstrap step to a maintainer (see AGENTS.md "New @gjsify/* package: first-publish + Trusted Publisher bootstrap").',
63
+ type: 'boolean',
64
+ default: false,
60
65
  })
61
66
  .option('provenance', {
62
67
  description: 'Pass-through flag — recorded in the payload but no signing happens (gjsify doesn\'t ship a sigstore signer yet).',
@@ -91,6 +96,7 @@ export const publishCommand = {
91
96
  const tag = args.tag ?? 'latest';
92
97
  const access = args.access;
93
98
  const tolerate = args['tolerate-republish'] === true;
99
+ const tolerateUntrustedNew = args['tolerate-untrusted-new'] === true;
94
100
  const provenance = args.provenance === true;
95
101
  const dryRun = args['dry-run'] === true;
96
102
  const checkTrustedOnly = args['check-trusted'] === true;
@@ -243,6 +249,34 @@ export const publishCommand = {
243
249
  }
244
250
  }
245
251
  catch (err) {
252
+ // Detect the "never-before-published @scope/pkg" shape:
253
+ // npm's OIDC exchange returns 404 with body
254
+ // {"message":"OIDC token exchange error - package not found"}
255
+ // for any package that has no Trusted Publisher entry (which
256
+ // includes every package that doesn't exist on npm yet —
257
+ // see AGENTS.md "New @gjsify/* package: first-publish +
258
+ // Trusted Publisher bootstrap"). Skip such a package when
259
+ // --tolerate-untrusted-new is set so one un-bootstrapped
260
+ // package doesn't break the entire serialized publish loop.
261
+ const isUntrustedNewPackage = err instanceof OidcExchangeError &&
262
+ err.status === 404 &&
263
+ /package not found/i.test(err.body);
264
+ if (isUntrustedNewPackage && tolerateUntrustedNew) {
265
+ const headerMsg = `${packed.name}@${packed.version} (skipped — no Trusted Publisher on npm, see AGENTS.md "New @gjsify/* package: first-publish + Trusted Publisher bootstrap")`;
266
+ if (args.json) {
267
+ process.stdout.write(`${JSON.stringify({
268
+ ok: true,
269
+ action: 'skipped-untrusted-new',
270
+ name: packed.name,
271
+ version: packed.version,
272
+ reason: 'no-trusted-publisher',
273
+ }, null, 2)}\n`);
274
+ }
275
+ else {
276
+ process.stdout.write(`~ ${headerMsg}\n`);
277
+ }
278
+ return;
279
+ }
246
280
  if (trustedFlag === true) {
247
281
  // Explicit --trusted: bail with a clear error.
248
282
  handleOidcFailure(err, packed.name, args.json === true);
package/lib/index.js CHANGED
@@ -1,8 +1,27 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
2
5
  import yargs from 'yargs';
3
6
  import { hideBin } from 'yargs/helpers';
4
7
  import { buildCommand as build, testCommand as test, 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, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, } from './commands/index.js';
5
8
  import { APP_NAME } from './constants.js';
9
+ // Read the version from package.json adjacent to the bundle. yargs's
10
+ // auto-version-discovery (its `pkg-up`-driven default) doesn't reach
11
+ // through the bundled `dist/cli.gjs.mjs` path on GJS — falls back to
12
+ // "unknown". Both layouts are covered:
13
+ // - dev (tsx, `yarn workspace`): src/index.ts → ../package.json
14
+ // - bundled (install -g): dist/cli.gjs.mjs → ../package.json
15
+ function readBundleVersion() {
16
+ try {
17
+ const here = dirname(fileURLToPath(import.meta.url));
18
+ const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
19
+ return typeof pkg.version === 'string' ? pkg.version : 'unknown';
20
+ }
21
+ catch {
22
+ return 'unknown';
23
+ }
24
+ }
6
25
  // `parseAsync()` instead of `.argv` so the top-level await keeps the
7
26
  // process alive until command handlers complete. Under Node this is
8
27
  // cosmetic — the event loop holds the process up — but under GJS the
@@ -11,6 +30,7 @@ import { APP_NAME } from './constants.js';
11
30
  const cli = yargs(hideBin(process.argv));
12
31
  await cli
13
32
  .scriptName(APP_NAME)
33
+ .version(readBundleVersion())
14
34
  .strict()
15
35
  // Use the full terminal width for help. yargs's default caps at 80
16
36
  // (`Math.min(80, process.stdout.columns)`); we explicitly opt into
@@ -1,6 +1,14 @@
1
+ import { type Packument } from "@gjsify/npm-registry";
1
2
  import type { InstallOptions } from "./install-backend.ts";
3
+ interface ParsedSpec {
4
+ name: string;
5
+ range: string;
6
+ }
2
7
  export interface InstalledTopLevel {
3
8
  name: string;
4
9
  version: string;
5
10
  }
6
11
  export declare function installPackagesNative(opts: InstallOptions): Promise<InstalledTopLevel[]>;
12
+ export declare function parseSpec(raw: string): ParsedSpec;
13
+ export declare function pickVersion(packument: Packument, range: string): string | null;
14
+ export {};
@@ -384,22 +384,36 @@ function describeLockfileDrift(lockfile, specs) {
384
384
  lines.push(` - ${removed.sort().join("\n - ")}`);
385
385
  return lines.join("\n");
386
386
  }
387
- function parseSpec(raw) {
387
+ // Exported for unit-testing — keep the function name + signature
388
+ // stable, the install-backend itself still calls it via the local
389
+ // binding below. Internal API.
390
+ export function parseSpec(raw) {
391
+ // Bare names without an explicit `@version` resolve to the `latest`
392
+ // dist-tag. This matches npm CLI behaviour (`npm install foo` →
393
+ // foo@latest) and — crucially — picks up prereleases when the
394
+ // publisher has tagged them as `latest`. Using semver `*` here
395
+ // would silently exclude any version with a `-` (rc, beta, alpha,
396
+ // …) suffix per semver §9 ("Pre-release versions have a lower
397
+ // precedence than the associated normal version"); ts-for-gir
398
+ // shipped only prereleases (4.0.0-rc.17 is the `latest` tag, no
399
+ // stable 4.x yet) and `*` was selecting the abandoned 3.3.0
400
+ // instead.
388
401
  if (raw.startsWith("@")) {
389
402
  const slash = raw.indexOf("/");
390
403
  if (slash < 0)
391
404
  throw new Error(`Invalid spec (scoped name without slash): ${raw}`);
392
405
  const at = raw.indexOf("@", slash);
393
406
  if (at < 0)
394
- return { name: raw, range: "*" };
395
- return { name: raw.slice(0, at), range: raw.slice(at + 1) || "*" };
407
+ return { name: raw, range: "latest" };
408
+ return { name: raw.slice(0, at), range: raw.slice(at + 1) || "latest" };
396
409
  }
397
410
  const at = raw.indexOf("@");
398
411
  if (at < 0)
399
- return { name: raw, range: "*" };
400
- return { name: raw.slice(0, at), range: raw.slice(at + 1) || "*" };
412
+ return { name: raw, range: "latest" };
413
+ return { name: raw.slice(0, at), range: raw.slice(at + 1) || "latest" };
401
414
  }
402
- function pickVersion(packument, range) {
415
+ // Exported for unit-testing. Internal API.
416
+ export function pickVersion(packument, range) {
403
417
  // dist-tag fast path: `latest`, `next`, ...
404
418
  if (packument["dist-tags"][range])
405
419
  return packument["dist-tags"][range];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.4.20",
3
+ "version": "0.4.21",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -37,18 +37,18 @@
37
37
  "cli"
38
38
  ],
39
39
  "dependencies": {
40
- "@gjsify/buffer": "^0.4.20",
41
- "@gjsify/create-app": "^0.4.20",
42
- "@gjsify/node-globals": "^0.4.20",
43
- "@gjsify/node-polyfills": "^0.4.20",
44
- "@gjsify/npm-registry": "^0.4.20",
45
- "@gjsify/resolve-npm": "^0.4.20",
46
- "@gjsify/rolldown-plugin-gjsify": "^0.4.20",
47
- "@gjsify/rolldown-plugin-pnp": "^0.4.20",
48
- "@gjsify/semver": "^0.4.20",
49
- "@gjsify/tar": "^0.4.20",
50
- "@gjsify/web-polyfills": "^0.4.20",
51
- "@gjsify/workspace": "^0.4.20",
40
+ "@gjsify/buffer": "^0.4.21",
41
+ "@gjsify/create-app": "^0.4.21",
42
+ "@gjsify/node-globals": "^0.4.21",
43
+ "@gjsify/node-polyfills": "^0.4.21",
44
+ "@gjsify/npm-registry": "^0.4.21",
45
+ "@gjsify/resolve-npm": "^0.4.21",
46
+ "@gjsify/rolldown-plugin-gjsify": "^0.4.21",
47
+ "@gjsify/rolldown-plugin-pnp": "^0.4.21",
48
+ "@gjsify/semver": "^0.4.21",
49
+ "@gjsify/tar": "^0.4.21",
50
+ "@gjsify/web-polyfills": "^0.4.21",
51
+ "@gjsify/workspace": "^0.4.21",
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.20",
59
+ "@gjsify/unit": "^0.4.21",
60
60
  "@types/yargs": "^17.0.35",
61
61
  "typescript": "^6.0.3"
62
62
  },
63
63
  "peerDependencies": {
64
- "@gjsify/rolldown-native": "^0.4.20"
64
+ "@gjsify/rolldown-native": "^0.4.21"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "@gjsify/rolldown-native": {