@gjsify/cli 0.4.21 → 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.
@@ -1,6 +1,10 @@
1
1
  import type { Command } from '../types/index.js';
2
2
  interface CheckOptions {
3
- json: boolean;
3
+ include?: string[];
4
+ exclude?: string[];
5
+ parallel?: boolean;
6
+ jobs?: number;
7
+ verbose?: boolean;
4
8
  }
5
- export declare const checkCommand: Command<any, CheckOptions>;
9
+ export declare const checkCommand: Command<unknown, CheckOptions>;
6
10
  export {};
@@ -1,72 +1,182 @@
1
- import { runAllChecks, detectPackageManager, buildInstallCommand } from '../utils/check-system-deps.js';
1
+ // `gjsify check` workspace TypeScript-check orchestrator.
2
+ //
3
+ // In a workspace root: runs `npm run check` across every workspace that
4
+ // declares a `check` script (filtered the same way `gjsify foreach` filters:
5
+ // excludes `@girs/*`, honours --include/--exclude). Parallel by default.
6
+ //
7
+ // In a single package (or anywhere a `package.json` with a `check` script
8
+ // is reachable from cwd): runs the local `check` script directly. This is
9
+ // the natural "tsc --noEmit on the current scope" invocation, analogous to
10
+ // `gjsify format` / `lint` / `fix` (which all wrap Biome workspace-wide).
11
+ //
12
+ // The legacy system-dep-check shape lives under `gjsify system-check` after
13
+ // PR #254. The `check` alias on `system-check` stays valid for one release
14
+ // — if both `system-check`-style flags (`--json`) and `check`-style scripts
15
+ // are reachable here, the `--json` flag routes through this command first
16
+ // (since the typescript-check binding is positional `[paths..]`).
17
+ import { spawn, spawnSync } from 'node:child_process';
18
+ import { existsSync, readFileSync } from 'node:fs';
19
+ import { join } from 'node:path';
20
+ import { cpus } from 'node:os';
21
+ import { discoverWorkspaces, filterWorkspaces, } from '@gjsify/workspace';
22
+ import { findWorkspaceRoot } from '../utils/workspace-root.js';
23
+ function readPackageJson(dir) {
24
+ const path = join(dir, 'package.json');
25
+ if (!existsSync(path))
26
+ return null;
27
+ try {
28
+ return JSON.parse(readFileSync(path, 'utf-8'));
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ /**
35
+ * Run a single workspace's `check` script. Returns the exit code; non-zero
36
+ * indicates failure. Output is forwarded to the parent's stdout/stderr,
37
+ * line-prefixed with the workspace name when `prefix` is set.
38
+ */
39
+ function runCheck(ws, prefix) {
40
+ return new Promise((resolve) => {
41
+ const child = spawn('npm', ['run', 'check', '--if-present'], {
42
+ cwd: ws.location,
43
+ stdio: prefix === null ? 'inherit' : ['ignore', 'pipe', 'pipe'],
44
+ });
45
+ if (prefix !== null && child.stdout && child.stderr) {
46
+ const tag = `[${prefix}] `;
47
+ const forward = (stream, dest) => {
48
+ stream.on('data', (chunk) => {
49
+ const text = chunk.toString('utf-8');
50
+ for (const line of text.split('\n')) {
51
+ if (line.length > 0)
52
+ dest.write(`${tag}${line}\n`);
53
+ }
54
+ });
55
+ };
56
+ forward(child.stdout, process.stdout);
57
+ forward(child.stderr, process.stderr);
58
+ }
59
+ child.on('close', (code) => resolve(code ?? 1));
60
+ child.on('error', () => resolve(1));
61
+ });
62
+ }
2
63
  export const checkCommand = {
3
64
  command: '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.',
5
- builder: (yargs) => {
6
- return yargs
7
- .option('json', {
8
- description: 'Output results as JSON',
9
- type: 'boolean',
10
- default: false,
11
- });
12
- },
65
+ description: 'Run `npm run check` (TypeScript type-check) across the current workspace. In a workspace root: walks every package with a `check` script (excludes @girs/*; honours --include/--exclude). In a single package: runs the local `check` script directly. Symmetric peer of `gjsify format` / `lint` / `fix`. (Legacy system-dep `gjsify check` is now `gjsify system-check` — see #254.)',
66
+ builder: (yargs) => yargs
67
+ .option('include', {
68
+ description: 'Only run in workspaces matching these glob patterns (repeatable).',
69
+ type: 'string',
70
+ array: true,
71
+ })
72
+ .option('exclude', {
73
+ description: 'Skip workspaces matching these glob patterns (repeatable). Always excludes @girs/*.',
74
+ type: 'string',
75
+ array: true,
76
+ })
77
+ .option('parallel', {
78
+ description: 'Run workspace checks in parallel (default). Use --no-parallel to run them sequentially with full per-workspace output.',
79
+ type: 'boolean',
80
+ alias: 'p',
81
+ default: true,
82
+ })
83
+ .option('jobs', {
84
+ description: 'Max parallel workers (when --parallel). Default: os.cpus().length.',
85
+ type: 'number',
86
+ alias: 'j',
87
+ })
88
+ .option('verbose', {
89
+ description: 'Log the per-workspace command before spawning.',
90
+ type: 'boolean',
91
+ default: false,
92
+ }),
13
93
  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}`);
94
+ const cwd = process.cwd();
95
+ const workspaceRoot = findWorkspaceRoot(cwd);
96
+ // ---- Single-package mode ----
97
+ // Run when we're inside a package that has a `check` script but is
98
+ // NOT itself the workspace root. This is the "I want to tsc just
99
+ // this package" path — equivalent to `npm run check` but spawned
100
+ // through gjsify so the command surface stays uniform with format/
101
+ // lint/fix.
102
+ if (workspaceRoot && cwd !== workspaceRoot) {
103
+ const pkg = readPackageJson(cwd);
104
+ if (pkg?.scripts?.check) {
105
+ if (args.verbose) {
106
+ console.log(`[check] cwd=${cwd} → npm run check`);
107
+ }
108
+ const r = spawnSync('npm', ['run', 'check'], { cwd, stdio: 'inherit' });
109
+ process.exit(r.status ?? 1);
34
110
  }
111
+ // Fall through to workspace mode when the local package has no
112
+ // check script (cd'd into a non-package dir under the root).
35
113
  }
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
- }
114
+ // ---- Workspace mode ----
115
+ if (!workspaceRoot) {
116
+ console.error('gjsify check: no workspace root found from cwd. Run inside a workspace (or a package within one) with `npm run check` defined.');
117
+ process.exit(1);
47
118
  }
48
- console.log(`\nPackage manager: ${pm}`);
49
- if (allMissing.length === 0) {
50
- console.log('\nAll dependencies found.');
51
- return;
119
+ const allWorkspaces = discoverWorkspaces(workspaceRoot);
120
+ // Always exclude @girs/* (type-only packages, no own tsc check).
121
+ const exclude = ['@girs/*', ...(args.exclude ?? [])];
122
+ const filtered = filterWorkspaces(allWorkspaces, {
123
+ include: args.include,
124
+ exclude,
125
+ // noPrivate omitted → include private workspaces (test pkgs often have check).
126
+ });
127
+ // Skip workspaces without a `check` script (manifest.scripts.check).
128
+ const targets = filtered.filter((ws) => {
129
+ const scripts = ws.manifest.scripts;
130
+ return scripts?.check !== undefined;
131
+ });
132
+ if (targets.length === 0) {
133
+ console.error('gjsify check: no workspaces with a `check` script found.');
134
+ process.exit(1);
52
135
  }
53
- if (missingRequired.length > 0) {
54
- console.log(`\nMissing required: ${missingRequired.map(d => d.name).join(', ')}`);
136
+ if (args.verbose) {
137
+ console.log(`[check] root=${workspaceRoot} workspaces=${targets.length} parallel=${args.parallel ? 'yes' : 'no'}`);
55
138
  }
56
- if (missingOptional.length > 0) {
57
- console.log(`Missing optional: ${missingOptional.map(d => d.name).join(', ')}`);
139
+ // ---- Sequential mode ----
140
+ if (!args.parallel) {
141
+ let firstFail = 0;
142
+ for (const ws of targets) {
143
+ if (args.verbose)
144
+ console.log(`[check] → ${ws.name}`);
145
+ const code = await runCheck(ws, null);
146
+ if (code !== 0 && firstFail === 0)
147
+ firstFail = code;
148
+ }
149
+ if (firstFail !== 0)
150
+ console.error(`gjsify check: failures in ${targets.filter(async (ws) => await runCheck(ws, null) !== 0).length}+ workspaces`);
151
+ process.exit(firstFail);
58
152
  }
59
- const cmd = buildInstallCommand(pm, allMissing);
60
- if (cmd) {
61
- console.log(`\nTo install:\n ${cmd}`);
153
+ // ---- Parallel mode (default) ----
154
+ const concurrency = args.jobs ?? Math.max(1, cpus().length);
155
+ const failures = [];
156
+ let cursor = 0;
157
+ const workers = [];
158
+ for (let i = 0; i < concurrency; i++) {
159
+ workers.push((async () => {
160
+ while (true) {
161
+ const idx = cursor++;
162
+ if (idx >= targets.length)
163
+ return;
164
+ const ws = targets[idx];
165
+ const code = await runCheck(ws, ws.name);
166
+ if (code !== 0)
167
+ failures.push({ name: ws.name, code });
168
+ }
169
+ })());
62
170
  }
63
- else {
64
- console.log('\nNo install command available for your package manager. Install manually.');
171
+ await Promise.all(workers);
172
+ if (failures.length > 0) {
173
+ console.error(`\ngjsify check: ${failures.length} of ${targets.length} workspace(s) failed:`);
174
+ for (const f of failures)
175
+ console.error(` ✗ ${f.name} (exit ${f.code})`);
176
+ process.exit(1);
65
177
  }
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);
178
+ if (args.verbose)
179
+ console.log(`\ngjsify check: ${targets.length} workspace(s) green.`);
180
+ process.exit(0);
71
181
  },
72
182
  };
@@ -19,17 +19,24 @@ export const dlxCommand = {
19
19
  command: 'dlx <spec> [binOrArg] [extraArgs..]',
20
20
  description: 'Run the GJS bundle of an npm-published package without installing it locally.',
21
21
  builder: (yargs) => yargs
22
+ // Collect everything after `--` into argv['--'] so callers can
23
+ // forward flags that would otherwise be intercepted by gjsify's
24
+ // own parser. Canonical example: `gjsify dlx @ts-for-gir/cli --
25
+ // --help` shows ts-for-gir's --help instead of gjsify dlx's.
26
+ // Without `populate--`, the trailing `--help` is consumed at
27
+ // the gjsify level and the bundle never sees it.
28
+ .parserConfiguration({ 'populate--': true })
22
29
  .positional('spec', {
23
30
  description: 'Package spec (`name`, `name@version`, `@scope/name@spec`, or local path).',
24
31
  type: 'string',
25
32
  demandOption: true,
26
33
  })
27
34
  .positional('binOrArg', {
28
- description: 'Optional bin name when the package defines `gjsify.bin` with multiple entries; otherwise treated as the first argument forwarded to the bundle.',
35
+ description: 'Optional bin name when the package defines `gjsify.bin` with multiple entries; otherwise treated as the first argument forwarded to the bundle. To pass a flag here (e.g. `--help`) use the `--` separator: `gjsify dlx <pkg> -- --help`.',
29
36
  type: 'string',
30
37
  })
31
38
  .positional('extraArgs', {
32
- description: 'Extra args forwarded to `gjs -m <bundle>`.',
39
+ description: 'Extra args forwarded to `gjs -m <bundle>`. Use `--` before flags to bypass gjsify-level parsing (`gjsify dlx <pkg> -- --help --verbose`).',
33
40
  type: 'string',
34
41
  array: true,
35
42
  })
@@ -71,7 +78,15 @@ export const dlxCommand = {
71
78
  // gjsify dlx <pkg> mybin → bin if package has gjsify.bin[mybin], else arg
72
79
  // gjsify dlx <pkg> mybin -- arg1 arg2 → bin + extra args
73
80
  // gjsify dlx <pkg> -- arg1 arg2 → no bin, extra args
74
- const { binName, extraArgs } = splitBinAndArgs(pkgDir, args.binOrArg, args.extraArgs ?? []);
81
+ //
82
+ // The `parserConfiguration({ 'populate--': true })` on the builder
83
+ // routes anything after `--` into `args['--']` (as `(string |
84
+ // number)[]`), so flags like `--help` reach the bundle untouched.
85
+ // Merge those into the positional extraArgs the splitter sees so
86
+ // both call shapes share one downstream path.
87
+ const passthroughDoubleDash = (args['--'] ?? []).map((v) => String(v));
88
+ const extraArgsCombined = [...(args.extraArgs ?? []), ...passthroughDoubleDash];
89
+ const { binName, extraArgs } = splitBinAndArgs(pkgDir, args.binOrArg, extraArgsCombined);
75
90
  const entry = resolveGjsEntry(pkgDir, binName);
76
91
  if (entry.fromFallback) {
77
92
  console.warn(`[gjsify dlx] package "${cachedPkgName ?? parsed.kind}" has no \`gjsify\` field — falling back to package.json#main. Add \`gjsify.main\` to silence.`);
@@ -2,8 +2,8 @@
2
2
  //
3
3
  // Equivalent to biome's `check` (format + safe-lint-fix + organize-imports).
4
4
  // Default writes fixes in-place; pass `--no-write` to report-only.
5
- // Naming: deliberately distinct from `gjsify check` (which verifies
6
- // system dependencies).
5
+ // Naming: deliberately distinct from `gjsify check` (workspace TS check)
6
+ // and `gjsify system-check` (system-dependency verifier).
7
7
  import { resolve } from 'node:path';
8
8
  import { BiomeNotFoundError, findBiomeConfig, printBiomeNotFound, runBiome, } from '../utils/biome-resolve.js';
9
9
  export const fixCommand = {
@@ -138,14 +138,28 @@ export const flatpakInitCommand = {
138
138
  const cleanup = flatpak.cleanup;
139
139
  if (cleanup?.length)
140
140
  manifest.cleanup = cleanup;
141
+ // Module assembly. Two precedence rules:
142
+ // `flatpak.modules` — full replacement; if set, neither the
143
+ // extras nor the meson default get added.
144
+ // Right shape for npm-tarball CLI tools
145
+ // where the meson default would be wrong.
146
+ // `flatpak.extraModules` — prepended to the meson default.
147
+ // Right shape for meson-built GTK apps
148
+ // that want a few extra sibling modules
149
+ // (e.g. blueprint-compiler).
141
150
  const modules = [];
142
- if (flatpak.extraModules?.length)
143
- modules.push(...flatpak.extraModules);
144
- modules.push({
145
- name: deriveModuleName(appId),
146
- buildsystem: 'meson',
147
- sources: [{ type: 'dir', path: '.' }],
148
- });
151
+ if (flatpak.modules?.length) {
152
+ modules.push(...flatpak.modules);
153
+ }
154
+ else {
155
+ if (flatpak.extraModules?.length)
156
+ modules.push(...flatpak.extraModules);
157
+ modules.push({
158
+ name: deriveModuleName(appId),
159
+ buildsystem: 'meson',
160
+ sources: [{ type: 'dir', path: '.' }],
161
+ });
162
+ }
149
163
  manifest.modules = modules;
150
164
  const writtenFiles = [];
151
165
  const trackWrite = (p) => {
@@ -2,6 +2,7 @@ export * from './build.js';
2
2
  export * from './test.js';
3
3
  export * from './run.js';
4
4
  export * from './info.js';
5
+ export * from './system-check.js';
5
6
  export * from './check.js';
6
7
  export * from './showcase.js';
7
8
  export * from './create.js';
@@ -2,6 +2,7 @@ export * from './build.js';
2
2
  export * from './test.js';
3
3
  export * from './run.js';
4
4
  export * from './info.js';
5
+ export * from './system-check.js';
5
6
  export * from './check.js';
6
7
  export * from './showcase.js';
7
8
  export * from './create.js';
@@ -7,6 +7,7 @@ interface InstallOptions {
7
7
  'save-optional'?: boolean;
8
8
  immutable?: boolean;
9
9
  verbose: boolean;
10
+ backend?: 'native' | 'npm';
10
11
  }
11
12
  export declare const installCommand: Command<any, InstallOptions>;
12
13
  export {};
@@ -6,10 +6,11 @@
6
6
  // gjsify install -g <pkg> [...] → user-global install (XDG, GJS-runnable bin)
7
7
  //
8
8
  // All three modes route through `@gjsify/{semver,npm-registry,tar}` via
9
- // `installPackagesNative` — no Node/npm required at runtime. Set
10
- // `GJSIFY_INSTALL_BACKEND=npm` to opt back into the legacy `npm install`
11
- // subprocess flow (useful as escape-hatch for projects that hit a
12
- // missing native-backend feature).
9
+ // `installPackagesNative` — no Node/npm required at runtime. Pass
10
+ // `--backend=npm` (or set the legacy `GJSIFY_INSTALL_BACKEND=npm` env var)
11
+ // to opt back into the `npm install` subprocess flow — useful as an
12
+ // escape hatch for projects that hit a missing native-backend feature
13
+ // (Yarn PnP repos, lifecycle scripts, npm's `overrides` quirks).
13
14
  //
14
15
  // Workspace install (`gjsify install` in a monorepo root with a
15
16
  // `"workspaces"` field) hoists every workspace's externals into the root
@@ -53,6 +54,11 @@ export const installCommand = {
53
54
  description: 'Verbose install logging.',
54
55
  type: 'boolean',
55
56
  default: false,
57
+ })
58
+ .option('backend', {
59
+ description: 'Install backend. `native` (default) routes through `@gjsify/{semver,npm-registry,tar}` — no Node/npm at runtime. `npm` shells out to `npm install` as an escape hatch for cases the native backend does not yet model (Yarn PnP repos, lifecycle scripts). Overrides `GJSIFY_INSTALL_BACKEND` if both are set.',
60
+ type: 'string',
61
+ choices: ['native', 'npm'],
56
62
  }),
57
63
  handler: async (args) => {
58
64
  // --immutable is incompatible with explicit `<pkg>` adds and with
@@ -82,8 +88,12 @@ export const installCommand = {
82
88
  await installGlobalAndLink(args.packages, { verbose: args.verbose });
83
89
  return;
84
90
  }
85
- // Escape-hatch: legacy npm subprocess flow.
86
- if (process.env.GJSIFY_INSTALL_BACKEND === 'npm') {
91
+ // Backend selection (in precedence order):
92
+ // 1. --backend flag (explicit user choice)
93
+ // 2. GJSIFY_INSTALL_BACKEND env (back-compat shape from pre-flag era)
94
+ // 3. native (default)
95
+ const backend = args.backend ?? process.env.GJSIFY_INSTALL_BACKEND ?? 'native';
96
+ if (backend === 'npm') {
87
97
  await projectInstallViaNpm(args);
88
98
  await runPostInstallChecks();
89
99
  return;
@@ -4,6 +4,7 @@ interface PackOptions {
4
4
  'pack-destination'?: string;
5
5
  json?: boolean;
6
6
  'dry-run'?: boolean;
7
+ 'ignore-scripts'?: boolean;
7
8
  }
8
9
  interface PackResult {
9
10
  filename: string;
@@ -30,6 +31,21 @@ export interface PackWorkspaceOptions {
30
31
  dryRun?: boolean;
31
32
  /** Skip the workspace:^ rewrite step (rare — useful for testing the raw layout). */
32
33
  skipWorkspaceRewrite?: boolean;
34
+ /**
35
+ * Lifecycle scripts to run from `pkg.scripts` BEFORE the file collection
36
+ * pass. Order matters — entries are executed sequentially, stopping on
37
+ * the first failure.
38
+ *
39
+ * Defaults:
40
+ * - `['prepack']` from the `gjsify pack` CLI handler
41
+ * - `['prepublishOnly', 'prepack']` from `gjsify publish`
42
+ * - `[]` from programmatic callers that have already run scripts
43
+ *
44
+ * Mirrors `npm pack` / `npm publish` semantics. Pass `[]` (or set
45
+ * `--ignore-scripts` on the CLI) to skip — useful when an outer
46
+ * workflow has already produced the build artifacts.
47
+ */
48
+ lifecycleScripts?: readonly string[];
33
49
  }
34
50
  /**
35
51
  * Programmatic equivalent of the `pack` command — used by `gjsify publish`
@@ -29,6 +29,7 @@ import { join, resolve } from 'node:path';
29
29
  import { createTarball, gzip } from '@gjsify/tar';
30
30
  import { discoverWorkspaces } from '@gjsify/workspace';
31
31
  import { findWorkspaceRoot } from '../utils/workspace-root.js';
32
+ import { runLifecycleScript } from '../utils/run-lifecycle-script.js';
32
33
  export const packCommand = {
33
34
  command: 'pack [path]',
34
35
  description: 'Produce an npm-compatible .tgz tarball for the workspace at <path> (default: cwd). Rewrites workspace:^/~/* deps to resolved versions.',
@@ -50,12 +51,20 @@ export const packCommand = {
50
51
  description: 'Compute everything but do not write the .tgz.',
51
52
  type: 'boolean',
52
53
  default: false,
54
+ })
55
+ .option('ignore-scripts', {
56
+ description: 'Skip the `prepack` lifecycle script before packing. ' +
57
+ 'Mirrors `npm pack --ignore-scripts`. Use when scripts ' +
58
+ 'are already run by the outer workflow.',
59
+ type: 'boolean',
60
+ default: false,
53
61
  }),
54
62
  handler: async (args) => {
55
63
  const wsDir = resolve(args.path ?? process.cwd());
56
64
  const result = await packWorkspace(wsDir, {
57
65
  destination: args['pack-destination'],
58
66
  dryRun: args['dry-run'] === true,
67
+ lifecycleScripts: args['ignore-scripts'] ? [] : ['prepack'],
59
68
  });
60
69
  if (args.json) {
61
70
  process.stdout.write(`${JSON.stringify([result], null, 2)}\n`);
@@ -82,17 +91,35 @@ export async function packWorkspace(wsDir, opts = {}) {
82
91
  if (!name) {
83
92
  throw new Error(`gjsify pack: package.json at ${wsDir} has no "name"`);
84
93
  }
94
+ // Run npm-style lifecycle scripts BEFORE walking the file tree. The
95
+ // canonical case is `prepack` — many packages use it to generate
96
+ // build artifacts that aren't otherwise produced by their `build`
97
+ // script (template processing, codegen, etc.). Skipping these means
98
+ // the resulting tarball is missing files the package needs to work
99
+ // post-install. Matches `npm pack` / `npm publish` semantics.
100
+ const lifecycleScripts = opts.lifecycleScripts ?? ['prepack'];
101
+ for (const scriptName of lifecycleScripts) {
102
+ await runLifecycleScript(wsDir, pkg, scriptName, { optional: true });
103
+ }
104
+ // Re-read package.json AFTER lifecycle scripts in case one of them
105
+ // mutated it (e.g. a `prepack` that injects build metadata into
106
+ // package.json fields). Rare but legal — npm pack does the same.
107
+ const sourceAfterScripts = readFileSync(pkgPath, 'utf-8');
108
+ const pkgAfterScripts = sourceAfterScripts === originalSource
109
+ ? pkg
110
+ : JSON.parse(sourceAfterScripts);
85
111
  // Rewrite workspace:^/~/* deps to resolved npm version ranges, mirroring
86
112
  // yarn's auto-rewrite at publish time. Done in-memory only — the source
87
113
  // package.json on disk is never mutated by `gjsify pack`.
88
114
  const rewrittenPkg = opts.skipWorkspaceRewrite
89
- ? pkg
90
- : rewriteWorkspaceDeps(pkg, wsDir);
91
- const rewrittenSource = JSON.stringify(rewrittenPkg, null, indentOf(originalSource)) + '\n';
115
+ ? pkgAfterScripts
116
+ : rewriteWorkspaceDeps(pkgAfterScripts, wsDir);
117
+ const rewrittenSource = JSON.stringify(rewrittenPkg, null, indentOf(sourceAfterScripts)) + '\n';
92
118
  // Collect files according to the package.json `files` field (or npm's
93
119
  // default set). The package.json itself is always included with the
94
- // rewritten contents.
95
- const filesToPack = collectFiles(wsDir, pkg);
120
+ // rewritten contents. We use the post-script `pkgAfterScripts` here so
121
+ // that any `files` array modified by a prepack script is honored.
122
+ const filesToPack = collectFiles(wsDir, pkgAfterScripts);
96
123
  const entries = [{ name: 'package/', directory: true, mode: 0o755 }];
97
124
  const fileMetas = [];
98
125
  let unpackedSize = 0;
@@ -149,8 +149,17 @@ export const publishCommand = {
149
149
  return;
150
150
  }
151
151
  }
152
- // 1. Pack the workspace (rewrites workspace:^, computes integrity)
153
- 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
+ };
154
163
  const packed = await packWorkspace(wsDir, packOpts);
155
164
  // We need the raw bytes — re-run with destination=null and capture.
156
165
  // packWorkspace returns metadata only; for the bytes we re-pack into
@@ -351,9 +360,16 @@ export const publishCommand = {
351
360
  };
352
361
  async function packWorkspaceToBytes(wsDir) {
353
362
  // Cheap re-run that writes to a tempdir, then read back. Avoids
354
- // 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).
355
367
  const tmp = `/tmp/gjsify-publish-${process.pid}-${Date.now()}`;
356
- const res = await packWorkspace(wsDir, { destination: tmp, dryRun: false });
368
+ const res = await packWorkspace(wsDir, {
369
+ destination: tmp,
370
+ dryRun: false,
371
+ lifecycleScripts: [],
372
+ });
357
373
  if (!res.absolutePath)
358
374
  throw new Error('gjsify publish: pack did not produce a file');
359
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
+ };