@gjsify/cli 0.9.0 → 0.11.0

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.
package/lib/cli-app.js CHANGED
@@ -19,9 +19,10 @@ 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, 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, } from './commands/index.js';
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, 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 { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
25
+ import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
25
26
  // Detect which runtime is executing the CLI (GJS or Node.js).
26
27
  // GJS MUST be checked first because @gjsify/process sets
27
28
  // `process.versions.node = '20.0.0'` for compatibility — a plain
@@ -68,6 +69,11 @@ function readBundleVersion() {
68
69
  * exit before any async work runs.
69
70
  */
70
71
  export async function runCli(argv) {
72
+ // Under GJS, make a GJS-runnable `gjsify` available on PATH for child
73
+ // processes (workspace/foreach orchestration + compound `gjsify run`
74
+ // scripts) so node-free builds don't fall back to the Node bin. No-op on
75
+ // Node.
76
+ ensureGjsifyShimOnPath();
71
77
  const cli = yargs(argv);
72
78
  await cli
73
79
  .scriptName(APP_NAME)
@@ -108,6 +114,8 @@ export async function runCli(argv) {
108
114
  .command(tsc.command, tsc.description, tsc.builder, tsc.handler)
109
115
  .command(affected.command, affected.description, affected.builder, affected.handler)
110
116
  .command(storybook.command, storybook.description, storybook.builder, storybook.handler)
117
+ .command(debug.command, debug.description, debug.builder, debug.handler)
118
+ .command(browse.command, browse.description, browse.builder, browse.handler)
111
119
  .demandCommand(1)
112
120
  .epilogue(`Running on ${runtimeLabel()}`)
113
121
  .help()
@@ -0,0 +1,14 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface BrowseCliOptions {
3
+ url?: string;
4
+ appId?: string;
5
+ title?: string;
6
+ globals?: string;
7
+ out?: string;
8
+ devtools: boolean;
9
+ buildOnly: boolean;
10
+ }
11
+ /** Derive a DBus app id from the package name (mirrored by `gjsify debug`). */
12
+ export declare function deriveBrowserAppId(name?: string): string;
13
+ export declare const browseCommand: Command<unknown, BrowseCliOptions>;
14
+ export {};
@@ -0,0 +1,93 @@
1
+ // `gjsify browse [url]` — launch the Adwaita web browser from
2
+ // @gjsify/devtools-browser, optionally at a URL. Generates a tiny entry that
3
+ // calls runBrowserDevtools(), builds it (`--app gjs --globals auto,dom`), and
4
+ // launches it. Dispatch is IN-PROCESS (runCli + runGjsBundle), like
5
+ // `gjsify storybook`. `--devtools` sets GJSIFY_DEVTOOLS=1 so @gjsify/devtools
6
+ // wires the org.gjsify.Devtools control plane (driveable via `gjsify debug`).
7
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { join, resolve } from 'node:path';
9
+ function readPackage(cwd) {
10
+ try {
11
+ return JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
12
+ }
13
+ catch {
14
+ return {};
15
+ }
16
+ }
17
+ /** Derive a DBus app id from the package name (mirrored by `gjsify debug`). */
18
+ export function deriveBrowserAppId(name) {
19
+ if (!name)
20
+ return 'org.gjsify.Browser';
21
+ const slug = name
22
+ .replace(/^@/, '')
23
+ .replace(/[^a-zA-Z0-9]+/g, '.')
24
+ .replace(/^\.+|\.+$/g, '');
25
+ return `org.gjsify.browser.${slug}`;
26
+ }
27
+ function generateEntry(applicationId, url, title) {
28
+ const titleLine = title ? `\n title: ${JSON.stringify(title)},` : '';
29
+ const urlLine = url ? `\n homeUrl: ${JSON.stringify(url)},` : '';
30
+ return `// Auto-generated by \`gjsify browse\` — do not edit by hand.
31
+ import { runBrowserDevtools } from '@gjsify/devtools-browser';
32
+
33
+ await runBrowserDevtools({
34
+ applicationId: ${JSON.stringify(applicationId)},${titleLine}${urlLine}
35
+ });
36
+ `;
37
+ }
38
+ export const browseCommand = {
39
+ command: 'browse [url]',
40
+ description: 'Launch the Adwaita web browser, optionally pointed at a URL.',
41
+ builder: (yargs) => yargs
42
+ .positional('url', { description: 'Initial URL to load (default: page:welcome)', type: 'string' })
43
+ .option('app-id', {
44
+ description: 'GApplication id (default: gjsify.browse.applicationId or derived from the package name)',
45
+ type: 'string',
46
+ })
47
+ .option('title', { description: 'Window title', type: 'string' })
48
+ .option('globals', {
49
+ description: 'Value for `gjsify build --globals` (default: auto,dom — WebKit/iframe need DOM globals)',
50
+ type: 'string',
51
+ })
52
+ .option('out', {
53
+ description: 'Output bundle path (default: node_modules/.cache/gjsify-browse)',
54
+ type: 'string',
55
+ })
56
+ .option('devtools', {
57
+ description: 'Enable the MCP devtools control plane (sets GJSIFY_DEVTOOLS=1)',
58
+ type: 'boolean',
59
+ default: false,
60
+ })
61
+ .option('build-only', {
62
+ description: 'Build the bundle but do not launch it',
63
+ type: 'boolean',
64
+ default: false,
65
+ }),
66
+ handler: async (args) => {
67
+ const cwd = process.cwd();
68
+ const pkg = readPackage(cwd);
69
+ const config = pkg.gjsify?.browse ?? {};
70
+ const applicationId = args.appId ?? config.applicationId ?? deriveBrowserAppId(pkg.name);
71
+ const title = args.title ?? config.title;
72
+ const globals = args.globals ?? config.globals ?? 'auto,dom';
73
+ const url = args.url;
74
+ const cacheDir = join(cwd, 'node_modules', '.cache', 'gjsify-browse');
75
+ mkdirSync(cacheDir, { recursive: true });
76
+ const entryPath = join(cacheDir, 'entry.ts');
77
+ const outPath = args.out ? resolve(cwd, args.out) : join(cacheDir, 'browse.gjs.mjs');
78
+ writeFileSync(entryPath, generateEntry(applicationId, url, title));
79
+ console.log(`[gjsify browse] app=${applicationId}${url ? ` url=${url}` : ''}${args.devtools ? ' devtools=1' : ''}`);
80
+ const { runCli } = await import('../cli-app.js');
81
+ await runCli(['build', entryPath, '--app', 'gjs', '--globals', globals, '--outfile', outPath]);
82
+ if (args.buildOnly) {
83
+ console.log(`[gjsify browse] built ${outPath}`);
84
+ return;
85
+ }
86
+ // runGjsBundle spreads process.env into the child, so set the devtools
87
+ // gate here (RunGjsBundleOptions has no env field).
88
+ if (args.devtools)
89
+ process.env.GJSIFY_DEVTOOLS = '1';
90
+ const { runGjsBundle } = await import('../utils/run-gjs.js');
91
+ await runGjsBundle(outPath, [], { exitOnSuccess: true });
92
+ },
93
+ };
@@ -99,7 +99,12 @@ export const buildCommand = {
99
99
  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.",
100
100
  type: 'string',
101
101
  normalize: true,
102
- default: 'auto',
102
+ // No yargs `default: 'auto'` — a yargs default is
103
+ // indistinguishable from a user-set value and would always
104
+ // clobber `package.json#gjsify.globals` / `.gjsifyrc.*` in
105
+ // config.ts (same footgun as `entryPoints`/`bundler.input`).
106
+ // The 'auto' fallback is applied post-merge in `Config.forBuild`
107
+ // so precedence is: CLI flag > config file > 'auto'.
103
108
  })
104
109
  .option('shebang', {
105
110
  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`).',
@@ -0,0 +1,10 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface DebugCliOptions {
3
+ busName?: string;
4
+ profile?: string;
5
+ globals?: string;
6
+ out?: string;
7
+ buildOnly: boolean;
8
+ }
9
+ export declare const debugCommand: Command<unknown, DebugCliOptions>;
10
+ export {};
@@ -0,0 +1,139 @@
1
+ // `gjsify debug` — launch an MCP↔devtools bridge for a running, devtools-enabled
2
+ // gjsify app (its `org.gjsify.Devtools` DBus control plane). An MCP client (e.g.
3
+ // Claude Code) uses this as its server command; the bridge speaks JSON-RPC on
4
+ // stdio and translates tool calls to the app's DBus interface.
5
+ //
6
+ // Dispatch is IN-PROCESS (runCli + runGjsBundle), like `gjsify storybook`. The
7
+ // generated entry resolves `@gjsify/devtools-mcp` from the CONSUMER's
8
+ // node_modules (so it stays out of the CLI's own deps). The bridge is built
9
+ // `--app gjs --shebang` and launched with stdio inherited, so its JSON-RPC
10
+ // stdout reaches the client untouched.
11
+ //
12
+ // CRITICAL: this command logs ONLY to stderr — stdout is the MCP channel.
13
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
+ import { join, resolve } from 'node:path';
15
+ function readPackage(cwd) {
16
+ try {
17
+ return JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ }
23
+ /** Mirror of `commands/storybook.ts` deriveAppId — the storybook app's DBus id. */
24
+ function deriveStorybookAppId(name) {
25
+ if (!name)
26
+ return 'org.gjsify.Storybook';
27
+ const slug = name
28
+ .replace(/^@/, '')
29
+ .replace(/[^a-zA-Z0-9]+/g, '.')
30
+ .replace(/^\.+|\.+$/g, '');
31
+ return `org.gjsify.storybook.${slug}`;
32
+ }
33
+ /** Mirror of `commands/browse.ts` deriveBrowserAppId — the browser app's DBus id. */
34
+ function deriveBrowserAppId(name) {
35
+ if (!name)
36
+ return 'org.gjsify.Browser';
37
+ const slug = name
38
+ .replace(/^@/, '')
39
+ .replace(/[^a-zA-Z0-9]+/g, '.')
40
+ .replace(/^\.+|\.+$/g, '');
41
+ return `org.gjsify.browser.${slug}`;
42
+ }
43
+ function hasDep(pkg, name) {
44
+ return Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
45
+ }
46
+ function generateEntry(profile, busNameBase, name, version) {
47
+ if (profile === 'storybook') {
48
+ return `// Auto-generated by \`gjsify debug\` — do not edit by hand.
49
+ import { runDevtoolsMcp, storybookProfile } from '@gjsify/devtools-mcp';
50
+
51
+ await runDevtoolsMcp(storybookProfile(${JSON.stringify(busNameBase)}));
52
+ `;
53
+ }
54
+ if (profile === 'browser') {
55
+ return `// Auto-generated by \`gjsify debug\` — do not edit by hand.
56
+ import { runDevtoolsMcp, browserProfile } from '@gjsify/devtools-mcp';
57
+
58
+ await runDevtoolsMcp(browserProfile(${JSON.stringify(busNameBase)}));
59
+ `;
60
+ }
61
+ return `// Auto-generated by \`gjsify debug\` — do not edit by hand.
62
+ import { runDevtoolsMcp } from '@gjsify/devtools-mcp';
63
+
64
+ await runDevtoolsMcp({
65
+ name: ${JSON.stringify(name)},
66
+ version: ${JSON.stringify(version)},
67
+ busNameBase: ${JSON.stringify(busNameBase)},
68
+ });
69
+ `;
70
+ }
71
+ export const debugCommand = {
72
+ command: 'debug',
73
+ description: 'Launch an MCP bridge for a devtools-enabled gjsify app (org.gjsify.Devtools).',
74
+ builder: (yargs) => yargs
75
+ .option('bus-name', {
76
+ description: 'App DBus base name (default: gjsify.devtools.busNameBase, or the storybook app-id)',
77
+ type: 'string',
78
+ })
79
+ .option('profile', {
80
+ description: 'Tool profile: generic | storybook | browser (default: auto — storybook if @gjsify/storybook is a dep, browser if @gjsify/devtools-browser is, else generic)',
81
+ type: 'string',
82
+ })
83
+ .option('globals', {
84
+ description: 'Value for `gjsify build --globals` (default: auto)',
85
+ type: 'string',
86
+ })
87
+ .option('out', {
88
+ description: 'Output bundle path (default: node_modules/.cache/gjsify-debug)',
89
+ type: 'string',
90
+ })
91
+ .option('build-only', {
92
+ description: 'Build the bridge bundle but do not launch it (for a .mcp.json command pointing at the bundle)',
93
+ type: 'boolean',
94
+ default: false,
95
+ }),
96
+ handler: async (args) => {
97
+ const cwd = process.cwd();
98
+ const pkg = readPackage(cwd);
99
+ const config = pkg.gjsify?.devtools ?? {};
100
+ const profile = args.profile ??
101
+ config.profile ??
102
+ (hasDep(pkg, '@gjsify/storybook')
103
+ ? 'storybook'
104
+ : hasDep(pkg, '@gjsify/devtools-browser')
105
+ ? 'browser'
106
+ : 'generic');
107
+ const busNameBase = args.busName ??
108
+ config.busNameBase ??
109
+ (profile === 'storybook'
110
+ ? (pkg.gjsify?.storybook?.applicationId ?? deriveStorybookAppId(pkg.name))
111
+ : profile === 'browser'
112
+ ? (pkg.gjsify?.browse?.applicationId ?? deriveBrowserAppId(pkg.name))
113
+ : undefined);
114
+ if (!busNameBase) {
115
+ console.error('gjsify debug: no bus name — set package.json#gjsify.devtools.busNameBase or pass --bus-name <org.example.App>');
116
+ process.exit(1);
117
+ }
118
+ const globals = args.globals ?? config.globals ?? 'auto';
119
+ const name = pkg.name ? `${pkg.name}-devtools` : 'gjsify-devtools';
120
+ const version = '0.8.0';
121
+ const cacheDir = join(cwd, 'node_modules', '.cache', 'gjsify-debug');
122
+ mkdirSync(cacheDir, { recursive: true });
123
+ const entryPath = join(cacheDir, 'entry.ts');
124
+ const outPath = args.out ? resolve(cwd, args.out) : join(cacheDir, 'bridge.gjs.mjs');
125
+ writeFileSync(entryPath, generateEntry(profile, busNameBase, name, version));
126
+ // stderr only — stdout is the MCP JSON-RPC channel.
127
+ console.error(`[gjsify debug] profile=${profile} bus=${busNameBase}`);
128
+ // In-process build (runtime-agnostic), then launch with stdio inherited
129
+ // so the bridge's JSON-RPC stdin/stdout are the client's pipes.
130
+ const { runCli } = await import('../cli-app.js');
131
+ await runCli(['build', entryPath, '--app', 'gjs', '--shebang', '--globals', globals, '--outfile', outPath]);
132
+ if (args.buildOnly) {
133
+ console.error(`[gjsify debug] built ${outPath}`);
134
+ return;
135
+ }
136
+ const { runGjsBundle } = await import('../utils/run-gjs.js');
137
+ await runGjsBundle(outPath, [], { exitOnSuccess: true });
138
+ },
139
+ };
@@ -14,6 +14,7 @@ import { cpus } from 'node:os';
14
14
  import { buildDependencyGraph, discoverWorkspaces, filterWorkspaces, topologicalSort, } from '@gjsify/workspace';
15
15
  import { findWorkspaceRoot } from '../utils/workspace-root.js';
16
16
  import { prefixLines } from '../utils/prefixed-output.js';
17
+ import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
17
18
  // Every child spawned by spawnPrefixed registers here so fail-fast can
18
19
  // terminate the whole in-flight set instead of waiting on it. On CI a
19
20
  // single failed package used to leave foreach awaiting its still-running
@@ -520,6 +521,11 @@ async function runOne(ws, script, args, prefixOutput, verbose, exec) {
520
521
  await spawnPrefixed(runner, argv, ws.location, prefixOutput ? `[${ws.name}] ` : null);
521
522
  }
522
523
  function detectPackageManager() {
524
+ // Under GJS there is no Node — neither `npm` nor `yarn` can run scripts, so
525
+ // the runner is `gjsify` itself via the GJS shim on PATH
526
+ // (`ensureGjsifyShimOnPath`). Makes node-free `gjsify foreach` work.
527
+ if (isGjs())
528
+ return 'gjsify';
523
529
  // `npm_config_user_agent` is set by npm/yarn/pnpm — first token is
524
530
  // `<name>/<version>`. Reuse it so `gjsify foreach build` invoked
525
531
  // through `yarn run` keeps using yarn, etc.
@@ -31,3 +31,5 @@ export * from './barrels.js';
31
31
  export * from './tsc.js';
32
32
  export * from './affected.js';
33
33
  export * from './storybook.js';
34
+ export * from './debug.js';
35
+ export * from './browse.js';
@@ -31,3 +31,5 @@ export * from './barrels.js';
31
31
  export * from './tsc.js';
32
32
  export * from './affected.js';
33
33
  export * from './storybook.js';
34
+ export * from './debug.js';
35
+ export * from './browse.js';
@@ -127,6 +127,15 @@ async function runScript(script, extraArgs) {
127
127
  if (monorepoRoot && monorepoRoot !== cwd) {
128
128
  binDirs.push(join(monorepoRoot, 'node_modules', '.bin'));
129
129
  }
130
+ // Under GJS, the GJS-runnable `gjsify` shim (see `ensureGjsifyShimOnPath`)
131
+ // MUST shadow the workspace `node_modules/.bin/gjsify`, which is the Node
132
+ // entry and unusable in a node-free sandbox. So put the shim dir ahead of
133
+ // the `.bin` dirs in the child's PATH — otherwise a compound script like
134
+ // `gjsify run a && gjsify run b` resolves `gjsify` to the Node bin and
135
+ // fails. `GJSIFY_SHIM_DIR` is only set under GJS, so this is a no-op on Node.
136
+ if (process.env.GJSIFY_SHIM_DIR) {
137
+ binDirs.unshift(process.env.GJSIFY_SHIM_DIR);
138
+ }
130
139
  // Default FORCE_COLOR=1 when not already set, matching yarn / npm
131
140
  // script-runner behaviour. Without this, tools that check
132
141
  // `process.stdout.isTTY` (chalk, picocolors, biome, …) disable colors
@@ -17,6 +17,7 @@
17
17
  import { spawn } from 'node:child_process';
18
18
  import { buildDependencyGraph, discoverWorkspaces, topologicalSort, } from '@gjsify/workspace';
19
19
  import { findWorkspaceRoot } from '../utils/workspace-root.js';
20
+ import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
20
21
  export const workspaceCommand = {
21
22
  command: 'workspace <name> <script> [args..]',
22
23
  description: 'Run a workspace script (`yarn workspace <name> run <script>` equivalent).',
@@ -203,6 +204,12 @@ async function runOne(ws, script, extraArgs, runner, verbose) {
203
204
  });
204
205
  }
205
206
  function detectPackageManager() {
207
+ // Under GJS there is no Node, so neither `npm` nor `yarn` can run scripts —
208
+ // the only option is `gjsify` itself, resolved via the GJS shim on PATH
209
+ // (see `ensureGjsifyShimOnPath`). This is what makes node-free multi-package
210
+ // orchestration work in the Flatpak sandbox.
211
+ if (isGjs())
212
+ return 'gjsify';
206
213
  const ua = process.env.npm_config_user_agent ?? '';
207
214
  if (ua.startsWith('yarn/'))
208
215
  return 'yarn';
package/lib/config.js CHANGED
@@ -137,6 +137,12 @@ export class Config {
137
137
  configData.consoleShim = cliArgs.consoleShim;
138
138
  if (cliArgs.globals !== undefined)
139
139
  configData.globals = cliArgs.globals;
140
+ // Fallback applied post-merge (not as a yargs `default:`) so a
141
+ // `globals` declared in package.json#gjsify / `.gjsifyrc.*` survives —
142
+ // a yargs default is indistinguishable from a user value and would
143
+ // overwrite the config one. Precedence: CLI flag > config file > 'auto'
144
+ // (mirrors the `bundler.input` fallback below).
145
+ configData.globals ??= 'auto';
140
146
  if (cliArgs.shebang !== undefined)
141
147
  configData.shebang = cliArgs.shebang;
142
148
  if (cliArgs.excludeGlobals) {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Ensure a GJS-runnable `gjsify` is on `process.env.PATH` for child processes.
3
+ * Idempotent per process; children inherit the shim via `GJSIFY_SHIM_DIR` and
4
+ * reuse it instead of creating their own. No-op when not running under GJS.
5
+ */
6
+ export declare function ensureGjsifyShimOnPath(): void;
@@ -0,0 +1,52 @@
1
+ // Node-free orchestration: a GJS-runnable `gjsify` on PATH.
2
+ //
3
+ // The CLI's npm `bin` (`node_modules/.bin/gjsify`) is the NODE entry — useless
4
+ // in a node-free environment (the Flatpak sandbox has no Node). But multi-
5
+ // package orchestration and compound package scripts resolve `gjsify` from
6
+ // PATH and would hit that Node bin:
7
+ // - `gjsify workspace`/`foreach` spawn `<pm> run <script>` per package; under
8
+ // GJS the package manager is `gjsify` itself, and `spawn('gjsify', …)`
9
+ // searches PATH.
10
+ // - a compound script like `gjsify run a && gjsify run b` is executed via
11
+ // `/bin/sh -c …`, and the shell resolves each `gjsify` from PATH.
12
+ // Both fail without Node (`spawn npm ENOENT`, `gjsify: not found`).
13
+ //
14
+ // Fix: under GJS, write a tiny wrapper that re-invokes the running GJS bundle
15
+ // under `gjs -m`, and prepend its directory to `process.env.PATH` so every
16
+ // child process (and grandchild, via the inherited `GJSIFY_SHIM_DIR` marker)
17
+ // resolves `gjsify` to a GJS-runnable executable. No-op on Node.
18
+ import { mkdtempSync, writeFileSync, chmodSync, existsSync } from 'node:fs';
19
+ import { tmpdir } from 'node:os';
20
+ import { join, delimiter } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
23
+ let _ensured = false;
24
+ /**
25
+ * Ensure a GJS-runnable `gjsify` is on `process.env.PATH` for child processes.
26
+ * Idempotent per process; children inherit the shim via `GJSIFY_SHIM_DIR` and
27
+ * reuse it instead of creating their own. No-op when not running under GJS.
28
+ */
29
+ export function ensureGjsifyShimOnPath() {
30
+ if (_ensured || !isGjs())
31
+ return;
32
+ _ensured = true;
33
+ // A parent gjsify (also under GJS) already created the shim — reuse it so
34
+ // nested orchestration doesn't pile up temp dirs / PATH entries.
35
+ const inherited = process.env.GJSIFY_SHIM_DIR;
36
+ if (inherited && existsSync(join(inherited, 'gjsify'))) {
37
+ if (!(process.env.PATH ?? '').split(delimiter).includes(inherited)) {
38
+ process.env.PATH = inherited + delimiter + (process.env.PATH ?? '');
39
+ }
40
+ return;
41
+ }
42
+ // `import.meta.url` resolves to the running GJS bundle (cli.gjs.mjs) — the
43
+ // whole CLI collapses into that single file, so this module's URL is it.
44
+ const selfBundle = fileURLToPath(import.meta.url);
45
+ const gjs = process.env.GJS_CONSOLE || 'gjs';
46
+ const dir = mkdtempSync(join(tmpdir(), 'gjsify-shim-'));
47
+ const shim = join(dir, 'gjsify');
48
+ writeFileSync(shim, `#!/bin/sh\nexec "${gjs}" -m "${selfBundle}" "$@"\n`, { mode: 0o755 });
49
+ chmodSync(shim, 0o755);
50
+ process.env.GJSIFY_SHIM_DIR = dir;
51
+ process.env.PATH = dir + delimiter + (process.env.PATH ?? '');
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -120,19 +120,19 @@
120
120
  "cli"
121
121
  ],
122
122
  "dependencies": {
123
- "@gjsify/buffer": "^0.9.0",
124
- "@gjsify/create-app": "^0.9.0",
125
- "@gjsify/node-globals": "^0.9.0",
126
- "@gjsify/node-polyfills": "^0.9.0",
127
- "@gjsify/npm-registry": "^0.9.0",
128
- "@gjsify/resolve-npm": "^0.9.0",
129
- "@gjsify/rolldown-plugin-gjsify": "^0.9.0",
130
- "@gjsify/rolldown-plugin-pnp": "^0.9.0",
131
- "@gjsify/semver": "^0.9.0",
132
- "@gjsify/tar": "^0.9.0",
133
- "@gjsify/tsc": "^0.9.0",
134
- "@gjsify/web-polyfills": "^0.9.0",
135
- "@gjsify/workspace": "^0.9.0",
123
+ "@gjsify/buffer": "^0.11.0",
124
+ "@gjsify/create-app": "^0.11.0",
125
+ "@gjsify/node-globals": "^0.11.0",
126
+ "@gjsify/node-polyfills": "^0.11.0",
127
+ "@gjsify/npm-registry": "^0.11.0",
128
+ "@gjsify/resolve-npm": "^0.11.0",
129
+ "@gjsify/rolldown-plugin-gjsify": "^0.11.0",
130
+ "@gjsify/rolldown-plugin-pnp": "^0.11.0",
131
+ "@gjsify/semver": "^0.11.0",
132
+ "@gjsify/tar": "^0.11.0",
133
+ "@gjsify/tsc": "^0.11.0",
134
+ "@gjsify/web-polyfills": "^0.11.0",
135
+ "@gjsify/workspace": "^0.11.0",
136
136
  "cosmiconfig": "^9.0.2",
137
137
  "get-tsconfig": "^4.14.0",
138
138
  "pkg-types": "^2.3.1",
@@ -140,13 +140,13 @@
140
140
  "yargs": "^18.0.0"
141
141
  },
142
142
  "devDependencies": {
143
- "@gjsify/unit": "^0.9.0",
143
+ "@gjsify/unit": "^0.11.0",
144
144
  "@types/yargs": "^17.0.35",
145
145
  "typescript": "^6.0.3"
146
146
  },
147
147
  "peerDependencies": {
148
- "@gjsify/oxfmt-native": "^0.9.0",
149
- "@gjsify/rolldown-native": "^0.9.0"
148
+ "@gjsify/oxfmt-native": "^0.11.0",
149
+ "@gjsify/rolldown-native": "^0.11.0"
150
150
  },
151
151
  "peerDependenciesMeta": {
152
152
  "@gjsify/oxfmt-native": {