@gjsify/cli 0.26.1 → 0.27.1

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.
Files changed (58) hide show
  1. package/dist/cli.gjs.mjs +188 -183
  2. package/lib/bundler-pick.js +12 -4
  3. package/lib/cli-app.js +3 -1
  4. package/lib/commands/affected.js +1 -1
  5. package/lib/commands/build.js +21 -14
  6. package/lib/commands/check.js +40 -4
  7. package/lib/commands/clear.d.ts +8 -0
  8. package/lib/commands/clear.js +90 -0
  9. package/lib/commands/copy.d.ts +8 -0
  10. package/lib/commands/copy.js +87 -0
  11. package/lib/commands/dlx.js +8 -1
  12. package/lib/commands/index.d.ts +2 -0
  13. package/lib/commands/index.js +2 -0
  14. package/lib/commands/install.d.ts +4 -0
  15. package/lib/commands/install.js +164 -15
  16. package/lib/commands/onboard.js +17 -14
  17. package/lib/commands/pack.d.ts +32 -0
  18. package/lib/commands/pack.js +11 -2
  19. package/lib/commands/publish.js +18 -4
  20. package/lib/commands/storybook.js +5 -1
  21. package/lib/commands/test.js +29 -2
  22. package/lib/commands/uninstall.js +44 -2
  23. package/lib/commands/upgrade.js +1 -4
  24. package/lib/templates/install.mjs.tmpl +110 -19
  25. package/lib/utils/bin-shim.d.ts +73 -0
  26. package/lib/utils/bin-shim.js +97 -2
  27. package/lib/utils/build-cache.d.ts +7 -27
  28. package/lib/utils/build-cache.js +27 -2
  29. package/lib/utils/check-system-deps.d.ts +15 -1
  30. package/lib/utils/check-system-deps.js +126 -2
  31. package/lib/utils/clear-targets.d.ts +30 -0
  32. package/lib/utils/clear-targets.js +114 -0
  33. package/lib/utils/copy-targets.d.ts +41 -0
  34. package/lib/utils/copy-targets.js +145 -0
  35. package/lib/utils/detect-native-packages.d.ts +160 -5
  36. package/lib/utils/detect-native-packages.js +326 -50
  37. package/lib/utils/dir-link.d.ts +34 -6
  38. package/lib/utils/dir-link.js +23 -9
  39. package/lib/utils/install-backend-native.d.ts +109 -1
  40. package/lib/utils/install-backend-native.js +622 -59
  41. package/lib/utils/install-backend.js +17 -15
  42. package/lib/utils/install-global.js +6 -34
  43. package/lib/utils/install-lock.js +1 -1
  44. package/lib/utils/install-packument-cache.d.ts +36 -17
  45. package/lib/utils/install-packument-cache.js +59 -31
  46. package/lib/utils/oxc-resolve.d.ts +27 -1
  47. package/lib/utils/oxc-resolve.js +53 -26
  48. package/lib/utils/pkg-json.d.ts +24 -0
  49. package/lib/utils/pkg-json.js +27 -1
  50. package/lib/utils/platform-check.d.ts +215 -0
  51. package/lib/utils/platform-check.js +377 -0
  52. package/lib/utils/resolve-npm-package.js +9 -2
  53. package/lib/utils/run-gjs.d.ts +1 -1
  54. package/lib/utils/run-gjs.js +19 -3
  55. package/lib/utils/spawn.js +50 -2
  56. package/lib/utils/win32-command.d.ts +54 -0
  57. package/lib/utils/win32-command.js +140 -0
  58. package/package.json +19 -20
@@ -93,13 +93,21 @@ function diagnoseNativeEngine() {
93
93
  const facade = path.join(pkgDir, 'lib', 'esm', 'index.js');
94
94
  if (!existsSync(facade)) {
95
95
  return (`The engine's JS facade is NOT BUILT: ${facade} does not exist.\n` +
96
- 'It is a BUILD OUTPUT, not something `gjsify install` produces — so a freshly cloned or freshly ' +
97
- 'installed tree never has it, and every `gjsify build` under GJS fails here until it is built once. ' +
96
+ 'It is a BUILD OUTPUT, so this is almost certainly a clone of the gjsify REPOSITORY rather than a ' +
97
+ 'consumer install: in a clone, node_modules/@gjsify/rolldown-native is a symlink to the workspace ' +
98
+ 'package, whose `lib/` no `gjsify install` produces. An installed package from npm is NOT affected — ' +
99
+ 'the published tarball does contain `lib/esm/index.js` (verified against 0.26.1), so a consumer that ' +
100
+ 'hit this message should report it.\n' +
98
101
  'Building it needs a bundler, which is why it cannot bootstrap itself under GJS; the Node CLI entry ' +
99
- 'does it instead. From the gjsify repo root:\n' +
102
+ 'does it instead. From the gjsify repo root — NOTE both of these REQUIRE Node:\n' +
100
103
  ' gjsify run build:infra # the full cold-tree chain (recommended)\n' +
101
104
  ' node scripts/bootstrap-native-facades.mjs # just the facades, if the rest is already built\n' +
102
- 'Both are idempotent and skip already-built facades in seconds.');
105
+ 'Both are idempotent and skip already-built facades in seconds.\n' +
106
+ 'ON A NODE-LESS GJS HOST there is currently no in-repo way out, and saying so is the point — this ' +
107
+ 'message used to recommend those two commands unconditionally, and on such a host BOTH fail (the ' +
108
+ "second directly, the first at `@gjsify/create-app`'s `node scripts/process-template.mjs`). Until " +
109
+ 'that is closed, seed `lib/` from the published package — which ships it — or from a host that has ' +
110
+ 'Node. Tracked in `status/open-todos.md` ("A Node-less host cannot bootstrap a fresh CLONE").');
103
111
  }
104
112
  return ('The JS facade is built, so this is the typelib/prebuild lookup failing. Most likely the CLI bundle was ' +
105
113
  'invoked DIRECTLY (`gjs -m …/cli.gjs.mjs`, or by executing dist/cli.gjs.mjs), which bypasses the `gjsify` ' +
package/lib/cli-app.js CHANGED
@@ -19,7 +19,7 @@ import { readFileSync } from 'node:fs';
19
19
  import { dirname, join } from 'node:path';
20
20
  import { fileURLToPath } from 'node:url';
21
21
  import yargs from 'yargs';
22
- 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, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
22
+ import { buildCommand as build, clearCommand as clear, copyCommand as copy, 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, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
23
23
  import { APP_NAME } from './constants.js';
24
24
  import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
25
25
  import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
@@ -89,6 +89,8 @@ export async function runCli(argv) {
89
89
  .command(create.command, create.description, create.builder, create.handler)
90
90
  .command(install.command, install.description, install.builder, install.handler)
91
91
  .command(build.command, build.description, build.builder, build.handler)
92
+ .command(clear.command, clear.description, clear.builder, clear.handler)
93
+ .command(copy.command, copy.description, copy.builder, copy.handler)
92
94
  .command(test.command, test.description, test.builder, test.handler)
93
95
  .command(run.command, run.description, run.builder, run.handler)
94
96
  .command(dlx.command, dlx.description, dlx.builder, dlx.handler)
@@ -199,7 +199,7 @@ const OTHER_WORKFLOW_INPUTS = [
199
199
  // Each sibling workflow's own definition. `main.yml` is deliberately
200
200
  // absent — it is a GLOBAL_TRIGGER, since a job-shape change there is
201
201
  // invisible until the workflow re-runs.
202
- /^\.github\/workflows\/(deploy-docs|commitlint|release|release-cut|audit-runtimes|prebuilds|node-gi|napi|cli-cross-platform|build-ci-image)\.yml$/,
202
+ /^\.github\/workflows\/(deploy-docs|commitlint|release|release-cut|audit-runtimes|prebuilds|node-gi|napi|cli-cross-platform|build-ci-image|cancel-pr-runs)\.yml$/,
203
203
  // `prebuilds.yml`'s toolchain (#838): the QEMU-emulated build script and
204
204
  // the `changed-packages` classifier that decides which native packages a
205
205
  // prebuild run rebuilds. `prebuilds.yml` lists `.github/prebuild-toolchain/**`
@@ -1,5 +1,26 @@
1
1
  import { Config } from '../config.js';
2
2
  import { BuildAction } from '../actions/build.js';
3
+ // NO `normalize: true` ON THE GLOB-BEARING OPTIONS — `entry-points` and
4
+ // `exclude` are PATTERNS, not paths.
5
+ //
6
+ // yargs' `normalize` runs `path.normalize()` over every value, which on win32
7
+ // rewrites separators: `src/**/*.{ts,js}` becomes `src\**\*.{ts,js}`. Those
8
+ // patterns go to fast-glob, whose own source says why that must not happen
9
+ // ("we cannot use the standard `path.normalize` method, because on Windows
10
+ // platform it will use of backslashes") — and the failure is not a clean
11
+ // no-match. With no `/` left, fast-glob finds no static base, walks from `.`,
12
+ // follows the workspace's relative `node_modules/@gjsify/*` symlinks with
13
+ // unbounded depth and no cycle detection, and dies of heap exhaustion after
14
+ // tens of minutes without writing one output file. That is issue #914's
15
+ // "`--library` is pathologically slow" report, in full: every `build:gjsify`
16
+ // script in the tree passes its entry points as globs. See
17
+ // `rolldown-plugin-gjsify/src/utils/entry-points.ts` for the second layer.
18
+ //
19
+ // The flag had also been copied onto twelve options that are not paths at all
20
+ // (booleans, `choices` enums, comma-separated identifier lists). Normalizing
21
+ // those is meaningless, and it is what made the one place it is harmful look
22
+ // like house style. `outfile`/`outdir` keep it: they ARE single paths, and
23
+ // nothing globs them.
3
24
  export const buildCommand = {
4
25
  command: 'build [entryPoints..]',
5
26
  description: 'Build and bundle your Gjs project',
@@ -9,7 +30,6 @@ export const buildCommand = {
9
30
  description: 'The entry points you want to bundle. Defaults to bundler.input from package.json#gjsify or .gjsifyrc.js, falling back to src/index.ts when neither is set.',
10
31
  array: true,
11
32
  type: 'string',
12
- normalize: true,
13
33
  // No yargs `default` here on purpose. A yargs default value
14
34
  // is indistinguishable from "user passed the flag" in the
15
35
  // parsed args (cliArgs.entryPoints?.length is truthy either
@@ -29,20 +49,17 @@ export const buildCommand = {
29
49
  description: 'An array of glob patterns to exclude entry-points and aliases',
30
50
  array: true,
31
51
  type: 'string',
32
- normalize: true,
33
52
  default: [],
34
53
  })
35
54
  .option('verbose', {
36
55
  description: 'Switch on the verbose mode',
37
56
  type: 'boolean',
38
- normalize: true,
39
57
  default: false,
40
58
  })
41
59
  .option('app', {
42
60
  description: 'Build target for an application. Defaults to the target of the HOST runtime running the CLI: gjs when run under gjs, node when run under node/bun/deno (both consume the --app node bundle). Set `gjsify.app` in package.json#gjsify to override the host default.',
43
61
  type: 'string',
44
62
  choices: ['gjs', 'node', 'browser', 'nativescript'],
45
- normalize: true,
46
63
  // No yargs `default: 'gjs'` — a yargs default is
47
64
  // indistinguishable from a user-set value and would clobber
48
65
  // `package.json#gjsify.app` in config.ts (same footgun as
@@ -55,18 +72,15 @@ export const buildCommand = {
55
72
  description: 'Override the default output format',
56
73
  type: 'string',
57
74
  choices: ['iife', 'esm', 'cjs'],
58
- normalize: true,
59
75
  })
60
76
  .option('minify', {
61
77
  description: 'Minify the bundled output. Defaults to true; use --no-minify to emit pretty-printed code (e.g. for debugging or readable bundle review).',
62
78
  type: 'boolean',
63
- normalize: true,
64
79
  defaultDescription: 'true',
65
80
  })
66
81
  .option('library', {
67
82
  description: 'Use this if you want to build a library for Gjsify',
68
83
  type: 'boolean',
69
- normalize: true,
70
84
  default: false,
71
85
  })
72
86
  .option('outfile', {
@@ -85,26 +99,22 @@ export const buildCommand = {
85
99
  alias: 'r',
86
100
  description: "Enables TypeScript types on runtime using Deepkit's type compiler",
87
101
  type: 'boolean',
88
- normalize: true,
89
102
  default: false,
90
103
  })
91
104
  .option('log-level', {
92
105
  description: 'The log level can be changed to prevent esbuild from printing warning and/or error messages to the terminal',
93
106
  type: 'string',
94
107
  choices: ['silent', 'error', 'warning', 'info', 'debug', 'verbose'],
95
- normalize: true,
96
108
  default: 'warning',
97
109
  })
98
110
  .option('console-shim', {
99
111
  description: 'Inject a console shim into GJS builds for clean output without the GLib prefix and with working ANSI colors. Use --no-console-shim to disable. Only applies to GJS app builds.',
100
112
  type: 'boolean',
101
- normalize: true,
102
113
  default: true,
103
114
  })
104
115
  .option('globals', {
105
116
  description: "Comma-separated list of global identifiers, 'auto' (default) to detect automatically from the bundled output, or 'none' to disable. The 'auto' token may be combined with explicit identifiers/groups (e.g. 'auto,dom') for cases where the detector cannot statically see a global because it's accessed via indirection. Each identifier is mapped to the corresponding `@gjsify/<pkg>/register` module and injected into the bundle. See the CLI Reference docs for the full list of known identifiers. Only applies to GJS app builds.",
106
117
  type: 'string',
107
- normalize: true,
108
118
  // No yargs `default: 'auto'` — a yargs default is
109
119
  // indistinguishable from a user-set value and would always
110
120
  // clobber `package.json#gjsify.globals` / `.gjsifyrc.*` in
@@ -115,7 +125,6 @@ export const buildCommand = {
115
125
  .option('shebang', {
116
126
  description: 'Prepend a target-appropriate shebang to the output and mark it executable (chmod 755): `#!/usr/bin/env -S gjs -m` for --app gjs, `#!/usr/bin/env node` for --app node. Applies to GJS and Node app builds with a single --outfile. Default: false (use --shebang to enable, or set `shebang: true` in `.gjsifyrc.js`).',
117
127
  type: 'boolean',
118
- normalize: true,
119
128
  })
120
129
  .option('external', {
121
130
  description: 'Module names that should NOT be bundled — EXACT package names only, matched literally on both bundler engines (no globs). Repeat the flag or pass a comma-separated list (e.g. --external typedoc,prettier). Appended to the per-app built-in externals; register subpaths (`<pkg>/register[/…]`) are always force-inlined for --app gjs regardless of this flag.',
@@ -146,13 +155,11 @@ export const buildCommand = {
146
155
  .option('exclude-globals', {
147
156
  description: 'Comma-separated global identifiers to remove from auto-detection results. Use for false positives from dead browser-compat code whose polyfills require unavailable native libraries (e.g. --exclude-globals fetch,XMLHttpRequest).',
148
157
  type: 'string',
149
- normalize: true,
150
158
  })
151
159
  .option('watch', {
152
160
  alias: 'w',
153
161
  description: 'Watch source files and rebuild on change. Logs each rebuild with duration; clean SIGINT shutdown. Only valid with --app gjs|node|browser (rejected with --library). Requires the npm `rolldown` engine — run under Node, not the GJS-bundled CLI.',
154
162
  type: 'boolean',
155
- normalize: true,
156
163
  default: false,
157
164
  });
158
165
  },
@@ -14,7 +14,6 @@
14
14
  // — if both `system-check`-style flags (`--json`) and `check`-style scripts
15
15
  // are reachable here, the `--json` flag routes through this command first
16
16
  // (since the typescript-check binding is positional `[paths..]`).
17
- import { spawnSync } from 'node:child_process';
18
17
  import { prefixLines } from '../utils/prefixed-output.js';
19
18
  import { spawnToCompletion } from '../utils/spawn.js';
20
19
  import { readPackageJson } from '../utils/pkg-json.js';
@@ -22,6 +21,7 @@ import { join } from 'node:path';
22
21
  import { cpus } from 'node:os';
23
22
  import { discoverWorkspaces, filterWorkspaces } from '@gjsify/workspace';
24
23
  import { findWorkspaceRoot } from '../utils/workspace-root.js';
24
+ import { detectPackageManager } from './workspace.js';
25
25
  /**
26
26
  * Run a single workspace's `check` script. Returns the exit code; non-zero
27
27
  * indicates failure. Output is forwarded to the parent's stdout/stderr,
@@ -33,8 +33,25 @@ async function runCheck(ws, prefix) {
33
33
  // GJS, so the STREAMING async path is safe here (and required: the
34
34
  // blocking one would serialize the parallel workers). See utils/spawn.ts.
35
35
  const flushers = [];
36
+ // The RUNNER is detected, never hardcoded — `foreach` and `workspace` have
37
+ // always done this and this site did not, which made `gjsify check` the one
38
+ // orchestrator that still required Node. Under GJS `detectPackageManager()`
39
+ // returns `gjsify` and the script goes through the shim on PATH.
40
+ //
41
+ // It failed in the worst possible shape: `spawnToCompletion` maps the
42
+ // `spawn npm ENOENT` into the rejection the `catch` below turns into a bare
43
+ // `exit 1`, so a Node-less host reported `✗ <pkg> (exit 1)` for a check
44
+ // script that never ran — measured on postmarketOS/aarch64 against a
45
+ // package whose `check` was literally `echo`.
46
+ const runner = detectPackageManager();
47
+ // `--if-present` is an npm/yarn flag with no `gjsify run` equivalent, so for
48
+ // the GJS runner the absence test moves here rather than being dropped.
49
+ if (runner === 'gjsify' && readPackageJson(join(ws.location, 'package.json'))?.scripts?.check === undefined) {
50
+ return 0;
51
+ }
52
+ const argv = runner === 'gjsify' ? ['run', 'check'] : ['run', 'check', '--if-present'];
36
53
  try {
37
- const { code } = await spawnToCompletion('npm', ['run', 'check', '--if-present'], {
54
+ const { code } = await spawnToCompletion(runner, argv, {
38
55
  completion: 'exit',
39
56
  cwd: ws.location,
40
57
  stdio: prefix === null ? 'inherit' : 'pipe',
@@ -111,11 +128,30 @@ export const checkCommand = {
111
128
  if (args.verbose) {
112
129
  console.log(`[check] cwd=${cwd} → npm run check`);
113
130
  }
114
- const r = spawnSync('npm', ['run', 'check'], { cwd, stdio: 'inherit' });
131
+ // Through `spawnToCompletion`, like workspace mode above and
132
+ // like every other spawn in the CLI. This site kept a raw
133
+ // `spawnSync('npm', …)` when the six wrappers were consolidated
134
+ // (see utils/spawn.ts), and that made it the last place where
135
+ // `npm` was handed to `spawn` as a bare name — ENOENT on
136
+ // Windows, where npm is a `.cmd` shim `CreateProcess` never
137
+ // finds. Reading only `r.status` then turned that into
138
+ // `process.exit(1)` with NO diagnostic at all: `gjsify check`
139
+ // inside any package failed silently on win32. Measured there
140
+ // before the change; `--verbose` printed the command line and
141
+ // nothing else.
142
+ // Runner detected, not hardcoded — same reason as workspace mode
143
+ // above. This branch already established that the package HAS a
144
+ // check script, so there is no `--if-present` to translate.
145
+ const { code } = await spawnToCompletion(detectPackageManager(), ['run', 'check'], {
146
+ cwd,
147
+ // The handler exits the process on every path below, so the
148
+ // streaming path is safe under GJS too.
149
+ completion: 'exit',
150
+ });
115
151
  // `return` — a bare `process.exit()` is deferred under GJS (no
116
152
  // atexit), so execution fell through into workspace mode and
117
153
  // re-checked the whole monorepo after the single-package run.
118
- return process.exit(r.status ?? 1);
154
+ return process.exit(code ?? 1);
119
155
  }
120
156
  // Fall through to workspace mode when the local package has no
121
157
  // check script (cd'd into a non-package dir under the root).
@@ -0,0 +1,8 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface ClearOptions {
3
+ paths?: string[];
4
+ dryRun?: boolean;
5
+ verbose?: boolean;
6
+ }
7
+ export declare const clearCommand: Command<unknown, ClearOptions>;
8
+ export {};
@@ -0,0 +1,90 @@
1
+ // `gjsify clear <paths…>` — portable recursive delete for package `clear` scripts.
2
+ //
3
+ // WHY THIS EXISTS
4
+ //
5
+ // Every `clear` script in this workspace was `rm -rf <dirs…> || exit 0`. npm runs
6
+ // package scripts through `cmd.exe` on Windows, which has neither `rm` nor that
7
+ // syntax, so `gjsify foreach clear` — and with it the documented full gate
8
+ // (`gjsify install --immutable && gjsify run clear && gjsify run build …`) —
9
+ // could not run there at all. 224 package.json files carried that line, so the
10
+ // fix is one portable implementation, not 224 hand-written platform branches
11
+ // (issue #914, finding 3).
12
+ //
13
+ // `force: true` already ignores a missing path, which is all the `|| exit 0` tail
14
+ // ever did — so the rewritten scripts drop it and stop swallowing REAL failures.
15
+ // A permission error used to exit 0 and read as a successful clean.
16
+ //
17
+ // THE BOOTSTRAP QUESTION, ANSWERED DELIBERATELY
18
+ //
19
+ // A `clear` script that calls `gjsify` makes cleaning depend on a built CLI —
20
+ // the same circularity `gjsify run chmod` had in the CLI's own build (removed in
21
+ // ce47b6a). It is acceptable HERE, and for a stronger reason than "you cannot
22
+ // clear a tree you never built": for the single-command case there is no second
23
+ // process at all. `gjsify run clear` recognises a script that is one
24
+ // `gjsify <subcommand>` and dispatches it IN-PROCESS (see `cli-app.ts`), so the
25
+ // nested-gjs cost the chmod case paid does not arise. `gjsify foreach clear`
26
+ // still spawns one child per package, exactly as it did for `rm -rf`, and the
27
+ // root script already passes `-p`.
28
+ //
29
+ // What it does mean: `clear` is a NEW command, so it does not exist in the
30
+ // committed `dist/cli.gjs.mjs` until that bundle is rebuilt. On a checkout
31
+ // carrying a stale bundle, `gjsify run clear` fails with "Unknown command" rather
32
+ // than silently clearing nothing — which is the honest failure of the two.
33
+ import { rm, stat } from 'node:fs/promises';
34
+ import { readdirSync } from 'node:fs';
35
+ import { relative } from 'node:path';
36
+ import { resolveClearTargets } from '../utils/clear-targets.js';
37
+ export const clearCommand = {
38
+ command: 'clear [paths..]',
39
+ description: 'Delete build output recursively. Portable replacement for `rm -rf` in package `clear` scripts.',
40
+ // No `normalize: true` anywhere below — see the comment block in
41
+ // `commands/build.ts`. It is meaningless for a path `fs.rm` resolves itself,
42
+ // and on win32 it is what would backslash a wildcard segment.
43
+ builder: (yargs) => {
44
+ return yargs
45
+ .positional('paths', {
46
+ description: 'Files and directories to delete, relative to the current package. A missing path is not an error. `*` and `?` are honoured in the last segment.',
47
+ type: 'string',
48
+ array: true,
49
+ })
50
+ .option('dry-run', {
51
+ description: 'Print what would be deleted and exit without touching anything.',
52
+ type: 'boolean',
53
+ default: false,
54
+ })
55
+ .option('verbose', {
56
+ alias: 'v',
57
+ description: 'Print each path as it is deleted.',
58
+ type: 'boolean',
59
+ default: false,
60
+ });
61
+ },
62
+ handler: async (args) => {
63
+ const cwd = process.cwd();
64
+ const paths = args.paths ?? [];
65
+ if (paths.length === 0) {
66
+ throw new Error('gjsify clear: no paths given. Usage: gjsify clear <paths…>');
67
+ }
68
+ // Resolution validates every target and throws on anything outside the
69
+ // package — before a single delete runs.
70
+ const targets = resolveClearTargets(paths, { cwd, readdir: readdirSync });
71
+ for (const target of targets) {
72
+ const shown = relative(cwd, target);
73
+ if (args.dryRun) {
74
+ const present = await stat(target).then(() => true, () => false);
75
+ console.log(`gjsify clear: would remove ${shown}${present ? '' : ' (absent)'}`);
76
+ continue;
77
+ }
78
+ // `maxRetries` is for Windows: a file another process still holds
79
+ // open fails there with EBUSY/EPERM instead of being unlinked while
80
+ // open as on POSIX — and clearing `@gjsify/cli` itself deletes the
81
+ // `lib/`+`dist/` the running process was loaded from. Both runtimes
82
+ // read a module fully and close it, so this covers a transient lock
83
+ // (a scanner, an editor), not a genuinely held handle; if it does
84
+ // fail, the error names the path instead of exiting 0.
85
+ await rm(target, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
86
+ if (args.verbose)
87
+ console.log(`gjsify clear: removed ${shown}`);
88
+ }
89
+ },
90
+ };
@@ -0,0 +1,8 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface CopyOptions {
3
+ paths?: string[];
4
+ dryRun?: boolean;
5
+ verbose?: boolean;
6
+ }
7
+ export declare const copyCommand: Command<unknown, CopyOptions>;
8
+ export {};
@@ -0,0 +1,87 @@
1
+ // `gjsify copy <sources…> <dest>` — portable file staging for package scripts.
2
+ //
3
+ // WHY THIS EXISTS
4
+ //
5
+ // The sibling of `gjsify clear`, and the second half of the same finding. npm
6
+ // runs package scripts through `cmd.exe` on Windows, which has no `cp`, no
7
+ // `mkdir -p` and expands no glob. `clear` closed the 224 `rm -rf` scripts
8
+ // (issue #914); what remained were 34 scripts that stage static assets with
9
+ // `mkdir -p X && cp -r …` — `build:assets` (24), `build:public` (5), and one
10
+ // each of `build:views`, `sync:theme`, `prebuild:test`, `prebuild:test:fixtures`.
11
+ //
12
+ // They were not merely a hygiene item. `showcases/node/express-webserver` is the
13
+ // one showcase that runs on Windows at all — its `--app node` bundle needs no
14
+ // gjs — and `build:public` is what puts `index.html` and `style.css` next to it.
15
+ // Without this command the showcase builds and then serves 404s for its own
16
+ // frontend, which is a worse failure than not building: the server starts.
17
+ //
18
+ // WHAT IT IS NOT
19
+ //
20
+ // Not a `cp` clone. Two deliberate differences, both because a build step is
21
+ // re-run:
22
+ //
23
+ // - `mkdir -p` is implied. Every one of the replaced scripts paired the two,
24
+ // and a copy that fails because the output directory does not exist yet is
25
+ // not a distinction worth preserving.
26
+ // - directory-vs-path is decided by the ARGUMENTS, never by what is on disk —
27
+ // see `isDirectoryDestination`. `cp -r src/assets dist/res` means two
28
+ // different things on the first and second run; this does not.
29
+ //
30
+ // Overwriting is unconditional (`force: true`), which is what the `-f` in most
31
+ // of the replaced scripts asked for and what the rest wanted without saying so.
32
+ import { cp, mkdir } from 'node:fs/promises';
33
+ import { readdirSync } from 'node:fs';
34
+ import { dirname, relative } from 'node:path';
35
+ import { planCopy } from '../utils/copy-targets.js';
36
+ export const copyCommand = {
37
+ command: 'copy [paths..]',
38
+ description: 'Copy files and directories into the build output. Portable replacement for `mkdir -p` + `cp -r` in package scripts.',
39
+ // No `normalize: true` — see the comment block in `commands/build.ts`. It is
40
+ // meaningless for a path resolved here anyway, and on win32 it is what would
41
+ // backslash a wildcard segment before it can be matched.
42
+ builder: (yargs) => {
43
+ return yargs
44
+ .positional('paths', {
45
+ description: 'One or more sources followed by the destination. The destination is a DIRECTORY when it ends with "/", when several sources are given, or when a source carries a wildcard; otherwise it is the exact target path. Missing parent directories are created. `*` and `?` are honoured in the last segment of a source.',
46
+ type: 'string',
47
+ array: true,
48
+ })
49
+ .option('dry-run', {
50
+ description: 'Print what would be copied and exit without touching anything.',
51
+ type: 'boolean',
52
+ default: false,
53
+ })
54
+ .option('verbose', {
55
+ alias: 'v',
56
+ description: 'Print each path as it is copied.',
57
+ type: 'boolean',
58
+ default: false,
59
+ });
60
+ },
61
+ handler: async (args) => {
62
+ const cwd = process.cwd();
63
+ const paths = args.paths ?? [];
64
+ if (paths.length < 2) {
65
+ throw new Error('gjsify copy: needs at least one source and a destination. Usage: gjsify copy <sources…> <dest>');
66
+ }
67
+ // Planning validates every argument — and every destination against the
68
+ // package boundary — before a single byte is written.
69
+ const ops = planCopy(paths, { cwd, readdir: readdirSync });
70
+ const show = (p) => {
71
+ const rel = relative(cwd, p);
72
+ // A source may legitimately live in a sibling package; showing it as
73
+ // `../../../packages/…` is more useful than an absolute path there.
74
+ return rel === '' ? '.' : rel;
75
+ };
76
+ for (const { from, to } of ops) {
77
+ if (args.dryRun) {
78
+ console.log(`gjsify copy: would copy ${show(from)} -> ${show(to)}`);
79
+ continue;
80
+ }
81
+ await mkdir(dirname(to), { recursive: true });
82
+ await cp(from, to, { recursive: true, force: true });
83
+ if (args.verbose)
84
+ console.log(`gjsify copy: ${show(from)} -> ${show(to)}`);
85
+ }
86
+ },
87
+ };
@@ -89,7 +89,14 @@ export const dlxCommand = {
89
89
  const { binName, extraArgs } = splitBinAndArgs(pkgDir, args.binOrArg, extraArgsCombined);
90
90
  const entry = resolveGjsEntry(pkgDir, binName);
91
91
  if (entry.fromFallback) {
92
- console.warn(`[gjsify dlx] package "${cachedPkgName ?? parsed.kind}" has no \`gjsify\` field — falling back to package.json#main. Add \`gjsify.main\` to silence.`);
92
+ // Name the key that is actually missing. The condition is
93
+ // `resolveGjsEntry` finding no `gjsify.main` (or no matching
94
+ // `gjsify.bin`), but the text said "no `gjsify` field" — and two
95
+ // showcases DO carry a `gjsify` object (tier + example) without a
96
+ // `main`, so the warning sent the reader looking for a field that
97
+ // was right there. Measured running `gjsify showcase
98
+ // webrtc-loopback` on a Node-less device.
99
+ console.warn(`[gjsify dlx] package "${cachedPkgName ?? parsed.kind}" declares no \`gjsify.main\` — falling back to package.json#main. Add \`gjsify.main\` to silence.`);
93
100
  }
94
101
  // Terminal call — exit on success, or the GJS main loop parks this
95
102
  // process forever (see RunGjsBundleOptions.exitOnSuccess).
@@ -1,4 +1,6 @@
1
1
  export * from './build.js';
2
+ export * from './clear.js';
3
+ export * from './copy.js';
2
4
  export * from './test.js';
3
5
  export * from './run.js';
4
6
  export * from './info.js';
@@ -1,4 +1,6 @@
1
1
  export * from './build.js';
2
+ export * from './clear.js';
3
+ export * from './copy.js';
2
4
  export * from './test.js';
3
5
  export * from './run.js';
4
6
  export * from './info.js';
@@ -12,6 +12,10 @@ interface InstallOptions {
12
12
  progress?: boolean;
13
13
  backend?: 'native' | 'npm';
14
14
  timeout: number;
15
+ os?: string;
16
+ cpu?: string;
17
+ libc?: string;
18
+ force?: boolean;
15
19
  }
16
20
  export declare const installCommand: Command<unknown, InstallOptions>;
17
21
  export declare function buildBinShim(wsLocation: string, nodeTarget?: string, gjsTarget?: string, nativePrebuildDirs?: string[], platform?: string, scanRoot?: string): string;