@gjsify/cli 0.3.21 → 0.4.3

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 (69) hide show
  1. package/dist/cli.gjs.mjs +791 -0
  2. package/lib/actions/build.js +4 -17
  3. package/lib/bundler-pick.d.ts +79 -0
  4. package/lib/bundler-pick.js +436 -0
  5. package/lib/commands/foreach.d.ts +17 -0
  6. package/lib/commands/foreach.js +341 -0
  7. package/lib/commands/index.d.ts +2 -0
  8. package/lib/commands/index.js +2 -0
  9. package/lib/commands/install.d.ts +1 -0
  10. package/lib/commands/install.js +401 -27
  11. package/lib/commands/run.d.ts +1 -1
  12. package/lib/commands/run.js +113 -20
  13. package/lib/commands/workspace.d.ts +8 -0
  14. package/lib/commands/workspace.js +79 -0
  15. package/lib/config.js +12 -1
  16. package/lib/index.js +11 -3
  17. package/lib/types/config-data.d.ts +10 -1
  18. package/lib/utils/install-backend-native.d.ts +5 -1
  19. package/lib/utils/install-backend-native.js +329 -70
  20. package/lib/utils/install-backend.d.ts +11 -1
  21. package/lib/utils/install-backend.js +4 -2
  22. package/lib/utils/pkg-json-edit.d.ts +47 -0
  23. package/lib/utils/pkg-json-edit.js +108 -0
  24. package/lib/utils/workspace-root.d.ts +1 -0
  25. package/lib/utils/workspace-root.js +46 -0
  26. package/package.json +70 -44
  27. package/src/actions/build.ts +0 -431
  28. package/src/actions/index.ts +0 -1
  29. package/src/commands/build.ts +0 -146
  30. package/src/commands/check.ts +0 -87
  31. package/src/commands/create.ts +0 -63
  32. package/src/commands/dlx.ts +0 -195
  33. package/src/commands/flatpak/build.ts +0 -225
  34. package/src/commands/flatpak/ci.ts +0 -173
  35. package/src/commands/flatpak/deps.ts +0 -120
  36. package/src/commands/flatpak/index.ts +0 -53
  37. package/src/commands/flatpak/init.ts +0 -191
  38. package/src/commands/flatpak/utils.ts +0 -76
  39. package/src/commands/gettext.ts +0 -258
  40. package/src/commands/gresource.ts +0 -97
  41. package/src/commands/gsettings.ts +0 -87
  42. package/src/commands/index.ts +0 -12
  43. package/src/commands/info.ts +0 -70
  44. package/src/commands/install.ts +0 -195
  45. package/src/commands/run.ts +0 -33
  46. package/src/commands/showcase.ts +0 -149
  47. package/src/config.ts +0 -304
  48. package/src/constants.ts +0 -1
  49. package/src/index.ts +0 -37
  50. package/src/types/cli-build-options.ts +0 -100
  51. package/src/types/command.ts +0 -10
  52. package/src/types/config-data-library.ts +0 -5
  53. package/src/types/config-data-typescript.ts +0 -6
  54. package/src/types/config-data.ts +0 -225
  55. package/src/types/cosmiconfig-result.ts +0 -5
  56. package/src/types/index.ts +0 -6
  57. package/src/utils/check-system-deps.ts +0 -480
  58. package/src/utils/detect-native-packages.ts +0 -153
  59. package/src/utils/discover-showcases.ts +0 -75
  60. package/src/utils/dlx-cache.ts +0 -135
  61. package/src/utils/install-backend-native.ts +0 -363
  62. package/src/utils/install-backend.ts +0 -88
  63. package/src/utils/install-global.ts +0 -182
  64. package/src/utils/normalize-bundler-options.ts +0 -129
  65. package/src/utils/parse-spec.ts +0 -48
  66. package/src/utils/resolve-gjs-entry.ts +0 -96
  67. package/src/utils/resolve-plugin-by-name.ts +0 -106
  68. package/src/utils/run-gjs.ts +0 -90
  69. package/tsconfig.json +0 -16
@@ -0,0 +1,341 @@
1
+ // `gjsify foreach [flags] <script>` — yarn-workspaces-foreach replacement.
2
+ //
3
+ // Replaces every `yarn workspaces foreach -A -p --no-private --exclude
4
+ // '@girs/*' --topological run build` style invocation in monorepo
5
+ // scripts. Flags mirror yarn 4's shape so root package.json scripts can
6
+ // move over with a 1:1 substitution.
7
+ //
8
+ // Output is line-prefixed `[<workspace-name>]` when --parallel is set,
9
+ // matching yarn's interactive flow. Exit code is non-zero if any child
10
+ // process failed; first failure's stderr is forwarded.
11
+ import { spawn } from 'node:child_process';
12
+ import { cpus } from 'node:os';
13
+ import { buildDependencyGraph, discoverWorkspaces, filterWorkspaces, topologicalSort, } from '@gjsify/workspace';
14
+ import { findWorkspaceRoot } from '../utils/workspace-root.js';
15
+ export const foreachCommand = {
16
+ command: 'foreach [script] [args..]',
17
+ description: 'Run a workspace script across all (or filtered) workspaces. Drop-in for `yarn workspaces foreach`: -A/--all, -p/--parallel, -t/--topological, --include, --exclude, --no-private. Pass --exec to run an arbitrary command instead of a script.',
18
+ builder: (yargs) => yargs
19
+ .positional('script', {
20
+ description: 'Script name to run in each workspace (`run <name>`-equivalent). With --exec, the command to run instead.',
21
+ type: 'string',
22
+ })
23
+ .positional('args', {
24
+ description: 'Extra arguments forwarded to each child invocation.',
25
+ type: 'string',
26
+ array: true,
27
+ })
28
+ .option('all', {
29
+ description: 'Include workspaces declared as `private: true`.',
30
+ type: 'boolean',
31
+ alias: 'A',
32
+ default: false,
33
+ })
34
+ .option('parallel', {
35
+ description: 'Run workspaces in parallel (capped by --jobs).',
36
+ type: 'boolean',
37
+ alias: 'p',
38
+ default: false,
39
+ })
40
+ .option('topological', {
41
+ description: 'Wait for each workspace\'s deps to finish before starting it (production deps only).',
42
+ type: 'boolean',
43
+ alias: 't',
44
+ default: false,
45
+ })
46
+ .option('topological-dev', {
47
+ description: 'Like --topological but also respects devDependencies (often cyclic — use sparingly).',
48
+ type: 'boolean',
49
+ default: false,
50
+ })
51
+ .option('include', {
52
+ description: 'Glob pattern to include workspaces by name (repeatable).',
53
+ type: 'string',
54
+ array: true,
55
+ })
56
+ .option('exclude', {
57
+ description: 'Glob pattern to exclude workspaces by name (repeatable).',
58
+ type: 'string',
59
+ array: true,
60
+ })
61
+ .option('private', {
62
+ // Yargs auto-negates `--no-private` to `private=false`, so the
63
+ // user-facing flag stays `--no-private` (yarn-compatible).
64
+ description: 'Include private workspaces (default true). Pass --no-private to skip them.',
65
+ type: 'boolean',
66
+ default: true,
67
+ })
68
+ .option('verbose', {
69
+ description: 'Echo every spawned command before running it.',
70
+ type: 'boolean',
71
+ alias: 'v',
72
+ default: false,
73
+ })
74
+ .option('jobs', {
75
+ description: 'Maximum concurrent workspaces in --parallel mode (default: cpu count).',
76
+ type: 'number',
77
+ alias: 'j',
78
+ })
79
+ .option('exec', {
80
+ description: 'Treat <script> [args..] as an arbitrary command (yarn `workspaces foreach exec`-equivalent) instead of a package.json script lookup. Workspace filtering by script presence is skipped. Use `-- <cmd> <args...>` to pass flags to the command without yargs intercepting them.',
81
+ type: 'boolean',
82
+ default: false,
83
+ })
84
+ .parserConfiguration({
85
+ // Preserve `--` as args._['--'] so callers can write
86
+ // gjsify foreach --exec -- npm publish --tag latest
87
+ // without yargs grabbing --tag/--access/etc.
88
+ 'populate--': true,
89
+ }),
90
+ handler: async (args) => {
91
+ // Walk up to the monorepo root — foreach is sometimes invoked
92
+ // from inside a child workspace's script chain.
93
+ const cwd = findWorkspaceRoot(process.cwd()) ?? process.cwd();
94
+ const allWorkspaces = discoverWorkspaces(cwd);
95
+ const exec = args.exec === true;
96
+ // In --exec mode, support both
97
+ // gjsify foreach --exec npm something (no flags in command)
98
+ // gjsify foreach --exec -- npm publish --tag X (flags in command)
99
+ // The `--` form is the typical one: `populate--: true` puts the
100
+ // post-separator argv into args._['--'], where yargs cannot grab
101
+ // --tag/--access/etc. as its own options.
102
+ let cmd = args.script;
103
+ let cmdArgs = args.args ?? [];
104
+ if (exec) {
105
+ // With populate--:true, anything after the literal `--`
106
+ // separator lands in top-level args['--']. yargs DOES NOT
107
+ // attach it to args._ — it's a sibling array.
108
+ const fromDoubleDash = (args['--'] ?? [])
109
+ .filter((v) => typeof v === 'string');
110
+ if (fromDoubleDash.length > 0) {
111
+ if (!cmd) {
112
+ cmd = fromDoubleDash[0];
113
+ cmdArgs = [...cmdArgs, ...fromDoubleDash.slice(1)];
114
+ }
115
+ else {
116
+ cmdArgs = [...cmdArgs, ...fromDoubleDash];
117
+ }
118
+ }
119
+ if (!cmd) {
120
+ console.error('gjsify foreach --exec: missing command. Pass it after `--`, e.g. `gjsify foreach --exec -- npm publish --tag latest`.');
121
+ process.exit(1);
122
+ }
123
+ }
124
+ let selected = filterWorkspaces(allWorkspaces, {
125
+ include: args.include,
126
+ exclude: args.exclude,
127
+ noPrivate: args.private === false,
128
+ });
129
+ // In script mode, only run on workspaces that actually have the
130
+ // requested script — yarn does this too, otherwise every project
131
+ // that doesn't declare `<script>` would fail and force the user to
132
+ // `--exclude` it. In --exec mode the command runs unconditionally
133
+ // (yarn's `workspaces foreach exec` semantics).
134
+ if (!exec) {
135
+ if (!cmd) {
136
+ console.error('gjsify foreach: missing <script> positional. Pass --exec to run an arbitrary command instead.');
137
+ process.exit(1);
138
+ }
139
+ const scriptName = cmd;
140
+ selected = selected.filter((ws) => {
141
+ const scripts = ws.manifest.scripts ?? {};
142
+ return typeof scripts[scriptName] === 'string';
143
+ });
144
+ }
145
+ if (selected.length === 0) {
146
+ console.log(`gjsify foreach: no workspaces match (${exec ? 'exec' : 'script'}="${cmd}", include=${JSON.stringify(args.include ?? [])}, exclude=${JSON.stringify(args.exclude ?? [])})`);
147
+ return;
148
+ }
149
+ if (args.topological || args['topological-dev']) {
150
+ const graph = buildDependencyGraph(selected, {
151
+ includeDev: args['topological-dev'] === true,
152
+ });
153
+ selected = topologicalSort(graph);
154
+ }
155
+ const verbose = args.verbose === true;
156
+ // `cmd` is guaranteed string at this point — both branches above
157
+ // exit on undefined, but TS doesn't narrow through them.
158
+ const finalCmd = cmd;
159
+ try {
160
+ if (args.parallel && !args.topological && !args['topological-dev']) {
161
+ const jobs = args.jobs && args.jobs > 0 ? args.jobs : cpus().length;
162
+ await runParallel(selected, finalCmd, cmdArgs, jobs, verbose, exec);
163
+ }
164
+ else if (args.parallel) {
165
+ // Topological + parallel: each workspace starts as soon as its
166
+ // deps (in the selected set) have finished. Yarn calls this
167
+ // "topological order with concurrency"; we cap at --jobs.
168
+ const jobs = args.jobs && args.jobs > 0 ? args.jobs : cpus().length;
169
+ await runTopologicalParallel(selected, finalCmd, cmdArgs, jobs, verbose, args['topological-dev'] === true, exec);
170
+ }
171
+ else {
172
+ await runSequential(selected, finalCmd, cmdArgs, verbose, exec);
173
+ }
174
+ }
175
+ catch (err) {
176
+ console.error(err.message);
177
+ process.exit(1);
178
+ }
179
+ // ensureMainLoop() (called inside spawn) keeps GJS alive after every
180
+ // child exits — without an explicit process.exit() the success path
181
+ // would park the loop forever.
182
+ process.exit(0);
183
+ },
184
+ };
185
+ async function runSequential(workspaces, script, args, verbose, exec) {
186
+ for (const ws of workspaces) {
187
+ await runOne(ws, script, args, /* prefixOutput */ false, verbose, exec);
188
+ }
189
+ }
190
+ async function runParallel(workspaces, script, args, concurrency, verbose, exec) {
191
+ let cursor = 0;
192
+ const workers = [];
193
+ for (let w = 0; w < concurrency; w++) {
194
+ workers.push((async () => {
195
+ while (cursor < workspaces.length) {
196
+ const i = cursor++;
197
+ await runOne(workspaces[i], script, args, /* prefixOutput */ true, verbose, exec);
198
+ }
199
+ })());
200
+ }
201
+ await Promise.all(workers);
202
+ }
203
+ async function runTopologicalParallel(workspaces, script, args, concurrency, verbose, includeDev, exec) {
204
+ const selectedNames = new Set(workspaces.map((w) => w.name));
205
+ const remaining = new Map();
206
+ for (const ws of workspaces) {
207
+ const wsDeps = new Set();
208
+ const m = ws.manifest;
209
+ for (const block of [
210
+ m.dependencies,
211
+ includeDev ? m.devDependencies : undefined,
212
+ m.optionalDependencies,
213
+ ]) {
214
+ if (!block)
215
+ continue;
216
+ for (const [name, spec] of Object.entries(block)) {
217
+ if (typeof spec !== 'string')
218
+ continue;
219
+ if (!spec.startsWith('workspace:'))
220
+ continue;
221
+ if (selectedNames.has(name))
222
+ wsDeps.add(name);
223
+ }
224
+ }
225
+ remaining.set(ws.name, wsDeps);
226
+ }
227
+ const byName = new Map(workspaces.map((w) => [w.name, w]));
228
+ const done = new Set();
229
+ let inflight = 0;
230
+ return new Promise((resolve, reject) => {
231
+ let error = null;
232
+ const pump = () => {
233
+ if (error)
234
+ return;
235
+ while (inflight < concurrency) {
236
+ const ready = [...remaining.entries()]
237
+ .filter(([, deps]) => [...deps].every((d) => done.has(d)))
238
+ .map(([n]) => n);
239
+ if (ready.length === 0)
240
+ break;
241
+ const next = ready.sort()[0];
242
+ remaining.delete(next);
243
+ inflight++;
244
+ runOne(byName.get(next), script, args, /* prefixOutput */ true, verbose, exec)
245
+ .then(() => {
246
+ inflight--;
247
+ done.add(next);
248
+ if (remaining.size === 0 && inflight === 0) {
249
+ resolve();
250
+ return;
251
+ }
252
+ pump();
253
+ })
254
+ .catch((e) => {
255
+ error = e instanceof Error ? e : new Error(String(e));
256
+ // Wait for in-flight tasks to finish (yarn does the
257
+ // same — surfaces all errors instead of abruptly
258
+ // killing siblings).
259
+ if (inflight === 0)
260
+ reject(error);
261
+ });
262
+ }
263
+ if (remaining.size > 0 && inflight === 0 && !error) {
264
+ reject(new Error(`gjsify foreach --topological: stuck — workspaces ${[...remaining.keys()].join(', ')} have unsatisfied deps in the selected set`));
265
+ }
266
+ };
267
+ pump();
268
+ });
269
+ }
270
+ async function runOne(ws, script, args, prefixOutput, verbose, exec) {
271
+ if (exec) {
272
+ // Arbitrary-command mode: spawn `<script> <args...>` directly
273
+ // (yarn `workspaces foreach exec`-equivalent). Used by callers
274
+ // that need to run binaries the workspace doesn't expose as a
275
+ // package.json script — e.g. `gjsify foreach --exec npm publish`.
276
+ if (verbose) {
277
+ console.error(`[${ws.name}] $ ${script} ${args.join(' ')}`);
278
+ }
279
+ await spawnPrefixed(script, args, ws.location, prefixOutput ? `[${ws.name}] ` : null);
280
+ return;
281
+ }
282
+ // Use the same package manager that invoked us — yarn under yarn,
283
+ // npm under npm, gjsify under gjsify. Default to `npm` for portability
284
+ // when nothing is detectable; the script-runner (D.5) will replace
285
+ // this once `gjsify run` ships.
286
+ const runner = detectPackageManager();
287
+ const argv = runner === 'gjsify'
288
+ ? ['run', script, ...args]
289
+ : ['run', script, ...(args.length > 0 ? ['--', ...args] : [])];
290
+ if (verbose) {
291
+ console.error(`[${ws.name}] $ ${runner} ${argv.join(' ')}`);
292
+ }
293
+ await spawnPrefixed(runner, argv, ws.location, prefixOutput ? `[${ws.name}] ` : null);
294
+ }
295
+ function detectPackageManager() {
296
+ // `npm_config_user_agent` is set by npm/yarn/pnpm — first token is
297
+ // `<name>/<version>`. Reuse it so `gjsify foreach build` invoked
298
+ // through `yarn run` keeps using yarn, etc.
299
+ const ua = process.env.npm_config_user_agent ?? '';
300
+ if (ua.startsWith('yarn/'))
301
+ return 'yarn';
302
+ if (ua.startsWith('gjsify/'))
303
+ return 'gjsify';
304
+ return 'npm';
305
+ }
306
+ function spawnPrefixed(cmd, args, cwd, prefix) {
307
+ return new Promise((resolve, reject) => {
308
+ const child = spawn(cmd, args, {
309
+ cwd,
310
+ stdio: prefix ? ['ignore', 'pipe', 'pipe'] : 'inherit',
311
+ env: process.env,
312
+ });
313
+ if (prefix && child.stdout && child.stderr) {
314
+ prefixLines(child.stdout, process.stdout, prefix);
315
+ prefixLines(child.stderr, process.stderr, prefix);
316
+ }
317
+ child.on('close', (code) => {
318
+ if (code === 0)
319
+ resolve();
320
+ else
321
+ reject(new Error(`${cmd} ${args.join(' ')} exited with code ${code}`));
322
+ });
323
+ child.on('error', (err) => reject(err));
324
+ });
325
+ }
326
+ function prefixLines(src, sink, prefix) {
327
+ let buf = '';
328
+ src.setEncoding('utf-8');
329
+ src.on('data', (chunk) => {
330
+ buf += chunk;
331
+ let idx;
332
+ while ((idx = buf.indexOf('\n')) !== -1) {
333
+ sink.write(prefix + buf.slice(0, idx + 1));
334
+ buf = buf.slice(idx + 1);
335
+ }
336
+ });
337
+ src.on('end', () => {
338
+ if (buf.length > 0)
339
+ sink.write(prefix + buf + '\n');
340
+ });
341
+ }
@@ -10,3 +10,5 @@ export * from './gsettings.js';
10
10
  export { flatpakCommand } from './flatpak/index.js';
11
11
  export * from './dlx.js';
12
12
  export * from './install.js';
13
+ export * from './foreach.js';
14
+ export * from './workspace.js';
@@ -10,3 +10,5 @@ export * from './gsettings.js';
10
10
  export { flatpakCommand } from './flatpak/index.js';
11
11
  export * from './dlx.js';
12
12
  export * from './install.js';
13
+ export * from './foreach.js';
14
+ export * from './workspace.js';
@@ -5,6 +5,7 @@ interface InstallOptions {
5
5
  'save-dev'?: boolean;
6
6
  'save-peer'?: boolean;
7
7
  'save-optional'?: boolean;
8
+ immutable?: boolean;
8
9
  verbose: boolean;
9
10
  }
10
11
  export declare const installCommand: Command<any, InstallOptions>;