@gjsify/cli 0.4.20 → 0.4.22

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.
@@ -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;
@@ -143,8 +149,17 @@ export const publishCommand = {
143
149
  return;
144
150
  }
145
151
  }
146
- // 1. Pack the workspace (rewrites workspace:^, computes integrity)
147
- const packOpts = { dryRun: true };
152
+ // 1. Pack the workspace (rewrites workspace:^, computes integrity).
153
+ // Lifecycle scripts: `prepublishOnly` (publish-specific) runs before
154
+ // `prepack`. Matches `npm publish` semantics — packages that gate
155
+ // release-time validation on `prepublishOnly` (typecheck, smoke
156
+ // tests, version-tagging) get exactly that, and packages whose
157
+ // `prepack` generates build artifacts (process-templates,
158
+ // codegen, …) get those artifacts into the tarball.
159
+ const packOpts = {
160
+ dryRun: true,
161
+ lifecycleScripts: ['prepublishOnly', 'prepack'],
162
+ };
148
163
  const packed = await packWorkspace(wsDir, packOpts);
149
164
  // We need the raw bytes — re-run with destination=null and capture.
150
165
  // packWorkspace returns metadata only; for the bytes we re-pack into
@@ -243,6 +258,34 @@ export const publishCommand = {
243
258
  }
244
259
  }
245
260
  catch (err) {
261
+ // Detect the "never-before-published @scope/pkg" shape:
262
+ // npm's OIDC exchange returns 404 with body
263
+ // {"message":"OIDC token exchange error - package not found"}
264
+ // for any package that has no Trusted Publisher entry (which
265
+ // includes every package that doesn't exist on npm yet —
266
+ // see AGENTS.md "New @gjsify/* package: first-publish +
267
+ // Trusted Publisher bootstrap"). Skip such a package when
268
+ // --tolerate-untrusted-new is set so one un-bootstrapped
269
+ // package doesn't break the entire serialized publish loop.
270
+ const isUntrustedNewPackage = err instanceof OidcExchangeError &&
271
+ err.status === 404 &&
272
+ /package not found/i.test(err.body);
273
+ if (isUntrustedNewPackage && tolerateUntrustedNew) {
274
+ const headerMsg = `${packed.name}@${packed.version} (skipped — no Trusted Publisher on npm, see AGENTS.md "New @gjsify/* package: first-publish + Trusted Publisher bootstrap")`;
275
+ if (args.json) {
276
+ process.stdout.write(`${JSON.stringify({
277
+ ok: true,
278
+ action: 'skipped-untrusted-new',
279
+ name: packed.name,
280
+ version: packed.version,
281
+ reason: 'no-trusted-publisher',
282
+ }, null, 2)}\n`);
283
+ }
284
+ else {
285
+ process.stdout.write(`~ ${headerMsg}\n`);
286
+ }
287
+ return;
288
+ }
246
289
  if (trustedFlag === true) {
247
290
  // Explicit --trusted: bail with a clear error.
248
291
  handleOidcFailure(err, packed.name, args.json === true);
@@ -317,9 +360,16 @@ export const publishCommand = {
317
360
  };
318
361
  async function packWorkspaceToBytes(wsDir) {
319
362
  // Cheap re-run that writes to a tempdir, then read back. Avoids
320
- // duplicating the file-walking + tar-building logic here.
363
+ // duplicating the file-walking + tar-building logic here. Lifecycle
364
+ // scripts are already run by the outer publish flow's first pack
365
+ // call — passing `[]` here skips re-running them (idempotent for
366
+ // most projects but a needless cost otherwise).
321
367
  const tmp = `/tmp/gjsify-publish-${process.pid}-${Date.now()}`;
322
- const res = await packWorkspace(wsDir, { destination: tmp, dryRun: false });
368
+ const res = await packWorkspace(wsDir, {
369
+ destination: tmp,
370
+ dryRun: false,
371
+ lifecycleScripts: [],
372
+ });
323
373
  if (!res.absolutePath)
324
374
  throw new Error('gjsify publish: pack did not produce a file');
325
375
  const bytes = new Uint8Array(readFileSync(res.absolutePath));
@@ -0,0 +1,6 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface CheckOptions {
3
+ json: boolean;
4
+ }
5
+ export declare const systemCheckCommand: Command<any, CheckOptions>;
6
+ export {};
@@ -0,0 +1,72 @@
1
+ import { runAllChecks, detectPackageManager, buildInstallCommand } from '../utils/check-system-deps.js';
2
+ export const systemCheckCommand = {
3
+ command: 'system-check',
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
+ builder: (yargs) => {
6
+ return yargs
7
+ .option('json', {
8
+ description: 'Output results as JSON',
9
+ type: 'boolean',
10
+ default: false,
11
+ });
12
+ },
13
+ handler: async (args) => {
14
+ const results = runAllChecks(process.cwd());
15
+ 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');
18
+ const allMissing = [...missingRequired, ...missingOptional];
19
+ if (args.json) {
20
+ console.log(JSON.stringify({ packageManager: pm, deps: results }, null, 2));
21
+ // Only required deps influence the exit code.
22
+ process.exit(missingRequired.length > 0 ? 1 : 0);
23
+ return;
24
+ }
25
+ console.log('System dependency check\n');
26
+ const required = results.filter(r => r.severity === 'required');
27
+ const optional = results.filter(r => r.severity === 'optional');
28
+ if (required.length > 0) {
29
+ console.log('Required:');
30
+ for (const dep of required) {
31
+ const icon = dep.found ? '✓' : '✗';
32
+ const ver = dep.version ? ` (${dep.version})` : '';
33
+ console.log(` ${icon} ${dep.name}${ver}`);
34
+ }
35
+ }
36
+ if (optional.length > 0) {
37
+ console.log('\nOptional:');
38
+ for (const dep of optional) {
39
+ // ⚠ for missing-but-needed-by-installed-packages, ○ for missing-but-not-needed (shouldn't appear in conditional mode)
40
+ const icon = dep.found ? '✓' : '⚠';
41
+ const ver = dep.version ? ` (${dep.version})` : '';
42
+ const requiredBy = dep.requiredBy && dep.requiredBy.length > 0
43
+ ? ` — needed by ${dep.requiredBy.join(', ')}`
44
+ : '';
45
+ console.log(` ${icon} ${dep.name}${ver}${requiredBy}`);
46
+ }
47
+ }
48
+ console.log(`\nPackage manager: ${pm}`);
49
+ if (allMissing.length === 0) {
50
+ console.log('\nAll dependencies found.');
51
+ return;
52
+ }
53
+ if (missingRequired.length > 0) {
54
+ console.log(`\nMissing required: ${missingRequired.map(d => d.name).join(', ')}`);
55
+ }
56
+ if (missingOptional.length > 0) {
57
+ console.log(`Missing optional: ${missingOptional.map(d => d.name).join(', ')}`);
58
+ }
59
+ const cmd = buildInstallCommand(pm, allMissing);
60
+ if (cmd) {
61
+ console.log(`\nTo install:\n ${cmd}`);
62
+ }
63
+ else {
64
+ console.log('\nNo install command available for your package manager. Install manually.');
65
+ }
66
+ // Exit non-zero ONLY if a required dependency is missing.
67
+ // Optional deps that are missing but needed by an installed @gjsify/*
68
+ // package generate a warning but keep exit code 0 — the user can still
69
+ // build/run code paths that don't touch the optional library.
70
+ process.exit(missingRequired.length > 0 ? 1 : 0);
71
+ },
72
+ };
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
- 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';
7
+ import { buildCommand as build, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, 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
@@ -28,6 +48,7 @@ await cli
28
48
  .command(run.command, run.description, run.builder, run.handler)
29
49
  .command(dlx.command, dlx.description, dlx.builder, dlx.handler)
30
50
  .command(info.command, info.description, info.builder, info.handler)
51
+ .command(systemCheck.command, systemCheck.description, systemCheck.builder, systemCheck.handler)
31
52
  .command(check.command, check.description, check.builder, check.handler)
32
53
  .command(showcase.command, showcase.description, showcase.builder, showcase.handler)
33
54
  .command(gresource.command, gresource.description, gresource.builder, gresource.handler)
@@ -253,6 +253,13 @@ export interface ConfigDataFlatpak {
253
253
  finishArgs?: string[];
254
254
  /** Extra Flatpak modules prepended before the app's own meson/simple module (e.g. `blueprint-compiler` build). */
255
255
  extraModules?: unknown[];
256
+ /**
257
+ * Full replacement for the manifest's `modules` array. When set, neither
258
+ * `extraModules` nor the meson default get added — the array is used
259
+ * verbatim. Right shape for CLI tools that ship a pre-built bundle and
260
+ * install via shell commands (`buildsystem: simple`) instead of meson.
261
+ */
262
+ modules?: unknown[];
256
263
  /** Cleanup glob patterns applied to the final manifest, e.g. `['/include', '/lib/pkgconfig']`. */
257
264
  cleanup?: string[];
258
265
  /** Source-of-truth lockfile for `gjsify flatpak deps` — `yarn.lock` or `package-lock.json`. */
@@ -81,6 +81,14 @@ const OPTIONAL_DEPS = {
81
81
  pangocairo: { id: 'pangocairo', name: 'PangoCairo', pkgName: 'pangocairo' },
82
82
  webkitgtk: { id: 'webkitgtk', name: 'WebKitGTK', pkgName: 'webkitgtk-6.0' },
83
83
  cairo: { id: 'cairo', name: 'Cairo', pkgName: 'cairo' },
84
+ // Build-time deps for @gjsify/*-native Vala prebuilds. End-users with
85
+ // installed prebuilds don't need the -devel package — only contributors
86
+ // rebuilding from source via `yarn build:prebuilds`. We still surface
87
+ // them in the optional set so the "missing" diagnostic catches build-
88
+ // time failures with an actionable install-hint instead of a meson
89
+ // `Run-time dependency X found: NO` error mid-build.
90
+ gnutls: { id: 'gnutls', name: 'GnuTLS', pkgName: 'gnutls' },
91
+ nghttp2: { id: 'nghttp2', name: 'libnghttp2', pkgName: 'libnghttp2' },
84
92
  };
85
93
  /**
86
94
  * Map of @gjsify/* package name → ids of OPTIONAL_DEPS this package needs.
@@ -101,6 +109,12 @@ const PACKAGE_DEPS = {
101
109
  // runOptionalChecks. Mapping it here so its presence in the project's
102
110
  // dep tree triggers the check.
103
111
  '@gjsify/webgl': ['gwebgl'],
112
+ // Native Vala bridges with `dependency('gnutls')` / `dependency('libnghttp2')`
113
+ // in their meson.build. Optional because the shipped prebuild covers the
114
+ // common-arch user path; only contributors rebuilding from source hit the
115
+ // build-time dep.
116
+ '@gjsify/tls-native': ['gnutls'],
117
+ '@gjsify/http2-native': ['nghttp2'],
104
118
  // @gjsify/event-bridge only needs gtk4/gdk which are already in the
105
119
  // required set, so it doesn't need an optional entry.
106
120
  };
@@ -278,6 +292,8 @@ const PM_PACKAGES = {
278
292
  pango: 'libpango1.0-dev',
279
293
  pangocairo: 'libpango1.0-dev',
280
294
  cairo: 'libcairo2-dev',
295
+ gnutls: 'libgnutls28-dev',
296
+ nghttp2: 'libnghttp2-dev',
281
297
  },
282
298
  dnf: {
283
299
  gjs: 'gjs',
@@ -296,6 +312,8 @@ const PM_PACKAGES = {
296
312
  pango: 'pango-devel',
297
313
  pangocairo: 'pango-devel',
298
314
  cairo: 'cairo-devel',
315
+ gnutls: 'gnutls-devel',
316
+ nghttp2: 'libnghttp2-devel',
299
317
  },
300
318
  pacman: {
301
319
  gjs: 'gjs',
@@ -314,6 +332,8 @@ const PM_PACKAGES = {
314
332
  pango: 'pango',
315
333
  pangocairo: 'pango',
316
334
  cairo: 'cairo',
335
+ gnutls: 'gnutls',
336
+ nghttp2: 'libnghttp2',
317
337
  },
318
338
  zypper: {
319
339
  gjs: 'gjs',
@@ -332,6 +352,8 @@ const PM_PACKAGES = {
332
352
  pango: 'pango-devel',
333
353
  pangocairo: 'pango-devel',
334
354
  cairo: 'cairo-devel',
355
+ gnutls: 'libgnutls-devel',
356
+ nghttp2: 'libnghttp2-devel',
335
357
  },
336
358
  apk: {
337
359
  gjs: 'gjs',
@@ -350,6 +372,8 @@ const PM_PACKAGES = {
350
372
  pango: 'pango-dev',
351
373
  pangocairo: 'pango-dev',
352
374
  cairo: 'cairo-dev',
375
+ gnutls: 'gnutls-dev',
376
+ nghttp2: 'nghttp2-dev',
353
377
  },
354
378
  unknown: {},
355
379
  };
@@ -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];
@@ -0,0 +1,14 @@
1
+ export interface RunLifecycleScriptOptions {
2
+ /** When true, do not throw on missing scripts — return `false` instead. */
3
+ optional?: boolean;
4
+ /** Stdio inheritance. Defaults to `'inherit'` so output goes to the parent. */
5
+ stdio?: 'inherit' | 'pipe' | 'ignore';
6
+ /** Extra environment variables layered on top of the defaults. */
7
+ env?: Record<string, string>;
8
+ }
9
+ /**
10
+ * Run a lifecycle script defined in `pkg.scripts[name]` from `wsDir`.
11
+ * Returns `true` if the script existed and exited 0. Returns `false` if
12
+ * `optional: true` and the script is missing. Throws on non-zero exit.
13
+ */
14
+ export declare function runLifecycleScript(wsDir: string, pkg: Record<string, unknown>, name: string, opts?: RunLifecycleScriptOptions): Promise<boolean>;
@@ -0,0 +1,74 @@
1
+ // Run an npm-style lifecycle script (`prepack`, `prepare`, `prepublishOnly`,
2
+ // `postpack`, `postpublish`, …) from inside a `gjsify pack` / `gjsify
3
+ // publish` flow.
4
+ //
5
+ // Why this exists separately from `gjsify run`: `gjsify run <script>` ends
6
+ // with `process.exit(<code>)` so it can be used as a CLI entrypoint that
7
+ // behaves like `yarn run` / `npm run`. That's wrong for the embedded use
8
+ // case where pack/publish needs the lifecycle script to finish, then keep
9
+ // running its own logic. This helper resolves to a Promise that settles
10
+ // when the child exits — no process.exit, no GLib-mainloop intermingling
11
+ // from `ensureMainLoop`.
12
+ //
13
+ // Matches yarn / npm script semantics:
14
+ // - `shell: true` so `&&` / `|` / env-var refs work
15
+ // - PATH prepended with `<wsDir>/node_modules/.bin` + monorepo-root bin
16
+ // - `npm_lifecycle_event` / `npm_package_name` / `npm_package_version`
17
+ // env vars set
18
+ // - FORCE_COLOR=1 default unless caller overrides
19
+ import { spawn } from 'node:child_process';
20
+ import { delimiter, join } from 'node:path';
21
+ import { findWorkspaceRoot } from './workspace-root.js';
22
+ /**
23
+ * Run a lifecycle script defined in `pkg.scripts[name]` from `wsDir`.
24
+ * Returns `true` if the script existed and exited 0. Returns `false` if
25
+ * `optional: true` and the script is missing. Throws on non-zero exit.
26
+ */
27
+ export async function runLifecycleScript(wsDir, pkg, name, opts = {}) {
28
+ const scripts = pkg.scripts ?? {};
29
+ const literal = scripts[name];
30
+ if (typeof literal !== 'string') {
31
+ if (opts.optional !== false)
32
+ return false;
33
+ throw new Error(`gjsify lifecycle-script: no "${name}" in ${wsDir}/package.json`);
34
+ }
35
+ const monorepoRoot = findWorkspaceRoot(wsDir);
36
+ const binDirs = [join(wsDir, 'node_modules', '.bin')];
37
+ if (monorepoRoot && monorepoRoot !== wsDir) {
38
+ binDirs.push(join(monorepoRoot, 'node_modules', '.bin'));
39
+ }
40
+ // Match yarn / npm color-forcing default — see runScript() in run.ts
41
+ // for the full reasoning. Without this, lifecycle scripts that call
42
+ // tools like biome / esbuild / tsc lose ANSI color in piped contexts
43
+ // (CI logs, redirected output) because `process.stdout.isTTY` is
44
+ // false for the spawned child.
45
+ const colorEnv = process.env.FORCE_COLOR !== undefined || process.env.NO_COLOR !== undefined
46
+ ? {}
47
+ : { FORCE_COLOR: '1' };
48
+ const env = {
49
+ ...process.env,
50
+ ...colorEnv,
51
+ PATH: [...binDirs, process.env.PATH ?? ''].filter(Boolean).join(delimiter),
52
+ npm_lifecycle_event: name,
53
+ npm_package_name: pkg.name ?? '',
54
+ npm_package_version: pkg.version ?? '',
55
+ ...(opts.env ?? {}),
56
+ };
57
+ await new Promise((resolveOk, reject) => {
58
+ const child = spawn(literal, [], {
59
+ cwd: wsDir,
60
+ env: env,
61
+ stdio: opts.stdio ?? 'inherit',
62
+ shell: true,
63
+ });
64
+ child.on('close', (code) => {
65
+ if (code === 0)
66
+ resolveOk();
67
+ else {
68
+ reject(new Error(`gjsify lifecycle-script: "${name}" in ${wsDir} exited with code ${code}`));
69
+ }
70
+ });
71
+ child.on('error', reject);
72
+ });
73
+ return true;
74
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.4.20",
3
+ "version": "0.4.22",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -12,6 +12,89 @@
12
12
  "gjsify": {
13
13
  "bin": {
14
14
  "gjsify": "./dist/cli.gjs.mjs"
15
+ },
16
+ "flatpak": {
17
+ "appId": "io.github.gjsify.Cli",
18
+ "kind": "cli",
19
+ "name": "gjsify",
20
+ "runtime": "gnome",
21
+ "runtimeVersion": "50",
22
+ "sdkExtensions": [
23
+ "org.freedesktop.Sdk.Extension.node24"
24
+ ],
25
+ "appendPath": [
26
+ "/usr/lib/sdk/node24/bin"
27
+ ],
28
+ "command": "gjsify",
29
+ "finishArgs": [
30
+ "--share=network",
31
+ "--filesystem=host"
32
+ ],
33
+ "developer": {
34
+ "id": "io.github.gjsify",
35
+ "name": "gjsify contributors",
36
+ "email": "pascal@artandcode.studio"
37
+ },
38
+ "summary": "Node.js + Web APIs for GJS — bundle, install, lint, format, flatpak-pack GJS apps",
39
+ "description": [
40
+ {
41
+ "p": "gjsify is the build-and-tooling CLI for the gjsify project — a polyfill family + bundler that lets you write GNOME apps in Node-shaped TypeScript and run them on GJS (the GNOME JavaScript runtime) without Node.js at runtime."
42
+ },
43
+ {
44
+ "p": "Use this Flatpak when you want gjsify available on any modern Linux distro without installing Node.js or a system-wide npm setup. The CLI ships its own GJS bundle and a pinned Node 24 SDK extension for the subcommands (`gjsify build`, `gjsify lint`, `gjsify format`) that still call out to Node tooling internally."
45
+ },
46
+ {
47
+ "ul": [
48
+ {
49
+ "item": "`gjsify build` — bundle a TypeScript entry into a single GJS / Node / browser file via Rolldown"
50
+ },
51
+ {
52
+ "item": "`gjsify install` — Node-free npm install with a lockfile (XDG-aware, native backend)"
53
+ },
54
+ {
55
+ "item": "`gjsify dlx <pkg>` — fetch and run any npm package's GJS bundle without persisting it"
56
+ },
57
+ {
58
+ "item": "`gjsify lint` / `gjsify format` — Biome-powered linter + formatter"
59
+ },
60
+ {
61
+ "item": "`gjsify flatpak {init,build,check,deps,ci}` — pack a GJS app or CLI into a Flatpak end-to-end"
62
+ },
63
+ {
64
+ "item": "`gjsify gresource` — bundle GResource XML files for GTK apps"
65
+ },
66
+ {
67
+ "item": "`gjsify showcase` — run the bundled demo programs (Excalibur, three.js, WebRTC, …)"
68
+ }
69
+ ]
70
+ }
71
+ ],
72
+ "license": {
73
+ "metadata": "CC0-1.0",
74
+ "project": "MIT"
75
+ },
76
+ "categories": [
77
+ "Development"
78
+ ],
79
+ "homepageUrl": "https://gjsify.github.io/gjsify/",
80
+ "vcsBrowserUrl": "https://github.com/gjsify/gjsify",
81
+ "issueTrackerUrl": "https://github.com/gjsify/gjsify/issues",
82
+ "modules": [
83
+ {
84
+ "name": "gjsify-cli",
85
+ "buildsystem": "simple",
86
+ "build-commands": [
87
+ "install -Dm755 dist/cli.gjs.mjs /app/share/gjsify/cli.gjs.mjs",
88
+ "install -Dm755 launcher.sh /app/bin/gjsify"
89
+ ],
90
+ "sources": [
91
+ {
92
+ "type": "dir",
93
+ "path": "."
94
+ }
95
+ ]
96
+ }
97
+ ]
15
98
  }
16
99
  },
17
100
  "files": [
@@ -37,18 +120,18 @@
37
120
  "cli"
38
121
  ],
39
122
  "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",
123
+ "@gjsify/buffer": "^0.4.22",
124
+ "@gjsify/create-app": "^0.4.22",
125
+ "@gjsify/node-globals": "^0.4.22",
126
+ "@gjsify/node-polyfills": "^0.4.22",
127
+ "@gjsify/npm-registry": "^0.4.22",
128
+ "@gjsify/resolve-npm": "^0.4.22",
129
+ "@gjsify/rolldown-plugin-gjsify": "^0.4.22",
130
+ "@gjsify/rolldown-plugin-pnp": "^0.4.22",
131
+ "@gjsify/semver": "^0.4.22",
132
+ "@gjsify/tar": "^0.4.22",
133
+ "@gjsify/web-polyfills": "^0.4.22",
134
+ "@gjsify/workspace": "^0.4.22",
52
135
  "cosmiconfig": "^9.0.1",
53
136
  "get-tsconfig": "^4.14.0",
54
137
  "pkg-types": "^2.3.1",
@@ -56,12 +139,12 @@
56
139
  "yargs": "^18.0.0"
57
140
  },
58
141
  "devDependencies": {
59
- "@gjsify/unit": "^0.4.20",
142
+ "@gjsify/unit": "^0.4.22",
60
143
  "@types/yargs": "^17.0.35",
61
144
  "typescript": "^6.0.3"
62
145
  },
63
146
  "peerDependencies": {
64
- "@gjsify/rolldown-native": "^0.4.20"
147
+ "@gjsify/rolldown-native": "^0.4.22"
65
148
  },
66
149
  "peerDependenciesMeta": {
67
150
  "@gjsify/rolldown-native": {