@gjsify/cli 0.4.21 → 0.4.23

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,29 @@ 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[];
49
+ /**
50
+ * Stdio mode for lifecycle scripts. Default `'inherit'` — child output
51
+ * appears in the parent's terminal. Pass `'inherit-stderr'` to redirect
52
+ * the child's stdout → parent's stderr; used by `gjsify pack --json`
53
+ * and `gjsify publish` so the parent's stdout stays a clean
54
+ * machine-readable JSON stream.
55
+ */
56
+ lifecycleStdio?: 'inherit' | 'inherit-stderr' | 'pipe' | 'ignore';
33
57
  }
34
58
  /**
35
59
  * 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,25 @@ 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'],
68
+ // When emitting machine-readable JSON on stdout, the prepack
69
+ // script's chatty log lines must NOT land on the parent's
70
+ // stdout — `JSON.parse(stdout)` callers would otherwise fail
71
+ // with `Unexpected token …`. Route lifecycle output to stderr.
72
+ lifecycleStdio: args.json ? 'inherit-stderr' : 'inherit',
59
73
  });
60
74
  if (args.json) {
61
75
  process.stdout.write(`${JSON.stringify([result], null, 2)}\n`);
@@ -82,17 +96,38 @@ export async function packWorkspace(wsDir, opts = {}) {
82
96
  if (!name) {
83
97
  throw new Error(`gjsify pack: package.json at ${wsDir} has no "name"`);
84
98
  }
99
+ // Run npm-style lifecycle scripts BEFORE walking the file tree. The
100
+ // canonical case is `prepack` — many packages use it to generate
101
+ // build artifacts that aren't otherwise produced by their `build`
102
+ // script (template processing, codegen, etc.). Skipping these means
103
+ // the resulting tarball is missing files the package needs to work
104
+ // post-install. Matches `npm pack` / `npm publish` semantics.
105
+ const lifecycleScripts = opts.lifecycleScripts ?? ['prepack'];
106
+ for (const scriptName of lifecycleScripts) {
107
+ await runLifecycleScript(wsDir, pkg, scriptName, {
108
+ optional: true,
109
+ stdio: opts.lifecycleStdio,
110
+ });
111
+ }
112
+ // Re-read package.json AFTER lifecycle scripts in case one of them
113
+ // mutated it (e.g. a `prepack` that injects build metadata into
114
+ // package.json fields). Rare but legal — npm pack does the same.
115
+ const sourceAfterScripts = readFileSync(pkgPath, 'utf-8');
116
+ const pkgAfterScripts = sourceAfterScripts === originalSource
117
+ ? pkg
118
+ : JSON.parse(sourceAfterScripts);
85
119
  // Rewrite workspace:^/~/* deps to resolved npm version ranges, mirroring
86
120
  // yarn's auto-rewrite at publish time. Done in-memory only — the source
87
121
  // package.json on disk is never mutated by `gjsify pack`.
88
122
  const rewrittenPkg = opts.skipWorkspaceRewrite
89
- ? pkg
90
- : rewriteWorkspaceDeps(pkg, wsDir);
91
- const rewrittenSource = JSON.stringify(rewrittenPkg, null, indentOf(originalSource)) + '\n';
123
+ ? pkgAfterScripts
124
+ : rewriteWorkspaceDeps(pkgAfterScripts, wsDir);
125
+ const rewrittenSource = JSON.stringify(rewrittenPkg, null, indentOf(sourceAfterScripts)) + '\n';
92
126
  // Collect files according to the package.json `files` field (or npm's
93
127
  // default set). The package.json itself is always included with the
94
- // rewritten contents.
95
- const filesToPack = collectFiles(wsDir, pkg);
128
+ // rewritten contents. We use the post-script `pkgAfterScripts` here so
129
+ // that any `files` array modified by a prepack script is honored.
130
+ const filesToPack = collectFiles(wsDir, pkgAfterScripts);
96
131
  const entries = [{ name: 'package/', directory: true, mode: 0o755 }];
97
132
  const fileMetas = [];
98
133
  let unpackedSize = 0;
@@ -149,8 +149,21 @@ 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
+ // `gjsify publish --json` emits the publish summary on stdout.
163
+ // Lifecycle scripts must not pollute that stream with their
164
+ // own log lines; redirect their stdout → parent's stderr.
165
+ lifecycleStdio: args.json ? 'inherit-stderr' : 'inherit',
166
+ };
154
167
  const packed = await packWorkspace(wsDir, packOpts);
155
168
  // We need the raw bytes — re-run with destination=null and capture.
156
169
  // packWorkspace returns metadata only; for the bytes we re-pack into
@@ -351,9 +364,16 @@ export const publishCommand = {
351
364
  };
352
365
  async function packWorkspaceToBytes(wsDir) {
353
366
  // Cheap re-run that writes to a tempdir, then read back. Avoids
354
- // duplicating the file-walking + tar-building logic here.
367
+ // duplicating the file-walking + tar-building logic here. Lifecycle
368
+ // scripts are already run by the outer publish flow's first pack
369
+ // call — passing `[]` here skips re-running them (idempotent for
370
+ // most projects but a needless cost otherwise).
355
371
  const tmp = `/tmp/gjsify-publish-${process.pid}-${Date.now()}`;
356
- const res = await packWorkspace(wsDir, { destination: tmp, dryRun: false });
372
+ const res = await packWorkspace(wsDir, {
373
+ destination: tmp,
374
+ dryRun: false,
375
+ lifecycleScripts: [],
376
+ });
357
377
  if (!res.absolutePath)
358
378
  throw new Error('gjsify publish: pack did not produce a file');
359
379
  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 {};