@gjsify/cli 0.1.2 → 0.1.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.
@@ -0,0 +1,6 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface CheckOptions {
3
+ json: boolean;
4
+ }
5
+ export declare const checkCommand: Command<any, CheckOptions>;
6
+ export {};
@@ -0,0 +1,44 @@
1
+ import { runAllChecks, detectPackageManager, buildInstallCommand } from '../utils/check-system-deps.js';
2
+ export const checkCommand = {
3
+ command: 'check',
4
+ description: 'Check that all required system dependencies (GJS, GTK4, Blueprint Compiler, etc.) are installed.',
5
+ builder: (yargs) => {
6
+ return yargs
7
+ .option('json', {
8
+ description: 'Output results as JSON',
9
+ type: 'boolean',
10
+ default: false,
11
+ });
12
+ },
13
+ handler: async (args) => {
14
+ const cwd = process.cwd();
15
+ const results = runAllChecks(cwd);
16
+ const pm = detectPackageManager();
17
+ const missing = results.filter(r => !r.found);
18
+ if (args.json) {
19
+ console.log(JSON.stringify({ packageManager: pm, deps: results }, null, 2));
20
+ process.exit(missing.length > 0 ? 1 : 0);
21
+ return;
22
+ }
23
+ console.log('System dependency check\n');
24
+ for (const dep of results) {
25
+ const icon = dep.found ? '✓' : '✗';
26
+ const ver = dep.version ? ` (${dep.version})` : '';
27
+ console.log(` ${icon} ${dep.name}${ver}`);
28
+ }
29
+ console.log(`\nPackage manager: ${pm}`);
30
+ if (missing.length === 0) {
31
+ console.log('\nAll dependencies found.');
32
+ return;
33
+ }
34
+ console.log(`\nMissing: ${missing.map(d => d.name).join(', ')}`);
35
+ const cmd = buildInstallCommand(pm, missing);
36
+ if (cmd) {
37
+ console.log(`\nTo install missing dependencies:\n ${cmd}`);
38
+ }
39
+ else {
40
+ console.log('\nNo install command available for your package manager. Install manually.');
41
+ }
42
+ process.exit(1);
43
+ },
44
+ };
@@ -1,3 +1,5 @@
1
1
  export * from './build.js';
2
2
  export * from './run.js';
3
3
  export * from './info.js';
4
+ export * from './check.js';
5
+ export * from './showcase.js';
@@ -1,3 +1,5 @@
1
1
  export * from './build.js';
2
2
  export * from './run.js';
3
3
  export * from './info.js';
4
+ export * from './check.js';
5
+ export * from './showcase.js';
@@ -1,6 +1,5 @@
1
- import { spawn } from 'node:child_process';
2
1
  import { resolve } from 'node:path';
3
- import { detectNativePackages, buildNativeEnv } from '../utils/detect-native-packages.js';
2
+ import { runGjsBundle } from '../utils/run-gjs.js';
4
3
  export const runCommand = {
5
4
  command: 'run <file> [args..]',
6
5
  description: 'Run a GJS bundle, automatically setting LD_LIBRARY_PATH and GI_TYPELIB_PATH for any installed native gjsify packages (e.g. @gjsify/webgl).',
@@ -22,28 +21,6 @@ export const runCommand = {
22
21
  handler: async (args) => {
23
22
  const file = resolve(args.file);
24
23
  const extraArgs = args.args ?? [];
25
- const cwd = process.cwd();
26
- const nativePackages = detectNativePackages(cwd);
27
- const nativeEnv = buildNativeEnv(nativePackages);
28
- const env = {
29
- ...process.env,
30
- ...nativeEnv,
31
- };
32
- const gjsArgs = ['-m', file, ...extraArgs];
33
- const child = spawn('gjs', gjsArgs, { env, stdio: 'inherit' });
34
- await new Promise((resolvePromise, reject) => {
35
- child.on('close', (code) => {
36
- if (code !== 0) {
37
- reject(new Error(`gjs exited with code ${code}`));
38
- }
39
- else {
40
- resolvePromise();
41
- }
42
- });
43
- child.on('error', reject);
44
- }).catch((err) => {
45
- console.error(err.message);
46
- process.exit(1);
47
- });
24
+ await runGjsBundle(file, extraArgs);
48
25
  },
49
26
  };
@@ -0,0 +1,8 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface ShowcaseOptions {
3
+ name?: string;
4
+ json: boolean;
5
+ list: boolean;
6
+ }
7
+ export declare const showcaseCommand: Command<any, ShowcaseOptions>;
8
+ export {};
@@ -0,0 +1,83 @@
1
+ import { discoverExamples, findExample } from '../utils/discover-examples.js';
2
+ import { runAllChecks, detectPackageManager, buildInstallCommand } from '../utils/check-system-deps.js';
3
+ import { runGjsBundle } from '../utils/run-gjs.js';
4
+ export const showcaseCommand = {
5
+ command: 'showcase [name]',
6
+ description: 'List or run built-in gjsify example applications.',
7
+ builder: (yargs) => {
8
+ return yargs
9
+ .positional('name', {
10
+ description: 'Example name to run (omit to list all)',
11
+ type: 'string',
12
+ })
13
+ .option('json', {
14
+ description: 'Output as JSON',
15
+ type: 'boolean',
16
+ default: false,
17
+ })
18
+ .option('list', {
19
+ description: 'List available examples',
20
+ type: 'boolean',
21
+ default: false,
22
+ });
23
+ },
24
+ handler: async (args) => {
25
+ // List mode: no name given, or --list flag
26
+ if (!args.name || args.list) {
27
+ const examples = discoverExamples();
28
+ if (args.json) {
29
+ console.log(JSON.stringify(examples, null, 2));
30
+ return;
31
+ }
32
+ if (examples.length === 0) {
33
+ console.log('No examples found. Example packages may not be installed.');
34
+ return;
35
+ }
36
+ // Group by category
37
+ const grouped = new Map();
38
+ for (const ex of examples) {
39
+ const list = grouped.get(ex.category) ?? [];
40
+ list.push(ex);
41
+ grouped.set(ex.category, list);
42
+ }
43
+ console.log('Available gjsify examples:\n');
44
+ for (const [category, list] of grouped) {
45
+ console.log(` ${category.toUpperCase()}:`);
46
+ const maxNameLen = Math.max(...list.map(e => e.name.length));
47
+ for (const ex of list) {
48
+ const pad = ' '.repeat(maxNameLen - ex.name.length + 2);
49
+ const desc = ex.description ? `${pad}${ex.description}` : '';
50
+ console.log(` ${ex.name}${desc}`);
51
+ }
52
+ console.log('');
53
+ }
54
+ console.log('Run an example: gjsify showcase <name>');
55
+ return;
56
+ }
57
+ // Run mode: find the example
58
+ const example = findExample(args.name);
59
+ if (!example) {
60
+ console.error(`Unknown example: "${args.name}"`);
61
+ console.error('Run "gjsify showcase" to list available examples.');
62
+ process.exit(1);
63
+ }
64
+ // System dependency check before running
65
+ const results = runAllChecks(process.cwd());
66
+ const missing = results.filter(r => !r.found);
67
+ if (missing.length > 0) {
68
+ console.error('Missing system dependencies:\n');
69
+ for (const dep of missing) {
70
+ console.error(` ✗ ${dep.name}`);
71
+ }
72
+ const pm = detectPackageManager();
73
+ const cmd = buildInstallCommand(pm, missing);
74
+ if (cmd) {
75
+ console.error(`\nInstall with:\n ${cmd}`);
76
+ }
77
+ process.exit(1);
78
+ }
79
+ // Run the example via shared GJS runner
80
+ console.log(`Running example: ${example.name}\n`);
81
+ await runGjsBundle(example.bundlePath);
82
+ },
83
+ };
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import yargs from 'yargs';
3
3
  import { hideBin } from 'yargs/helpers';
4
- import { buildCommand as build, runCommand as run, infoCommand as info } from './commands/index.js';
4
+ import { buildCommand as build, runCommand as run, infoCommand as info, checkCommand as check, showcaseCommand as showcase } from './commands/index.js';
5
5
  import { APP_NAME } from './constants.js';
6
6
  void yargs(hideBin(process.argv))
7
7
  .scriptName(APP_NAME)
@@ -10,5 +10,7 @@ void yargs(hideBin(process.argv))
10
10
  .command(build.command, build.description, build.builder, build.handler)
11
11
  .command(run.command, run.description, run.builder, run.handler)
12
12
  .command(info.command, info.description, info.builder, info.handler)
13
+ .command(check.command, check.description, check.builder, check.handler)
14
+ .command(showcase.command, showcase.description, showcase.builder, showcase.handler)
13
15
  .demandCommand(1)
14
16
  .help().argv;
@@ -0,0 +1,18 @@
1
+ export interface DepCheck {
2
+ /** Stable key used for install command lookup, e.g. "gjs" */
3
+ id: string;
4
+ /** Human-readable display name, e.g. "GJS" */
5
+ name: string;
6
+ found: boolean;
7
+ /** Version string when found, e.g. "1.86.0" */
8
+ version?: string;
9
+ }
10
+ export type PackageManager = 'apt' | 'dnf' | 'pacman' | 'zypper' | 'apk' | 'unknown';
11
+ export declare function detectPackageManager(): PackageManager;
12
+ export declare function runAllChecks(cwd: string): DepCheck[];
13
+ /**
14
+ * Build a suggested install command for missing dependencies.
15
+ * gwebgl is an npm package, not a system package — handled separately.
16
+ * Returns null when package manager is unknown or no installable deps are missing.
17
+ */
18
+ export declare function buildInstallCommand(pm: PackageManager, missing: DepCheck[]): string | null;
@@ -0,0 +1,185 @@
1
+ // System dependency checker for gjsify CLI.
2
+ // Uses execFileSync with explicit argument arrays — no shell injection possible.
3
+ // All binary names are hardcoded constants, never derived from user input.
4
+ import { execFileSync } from 'node:child_process';
5
+ import { join } from 'node:path';
6
+ import { createRequire } from 'node:module';
7
+ import { pathToFileURL } from 'node:url';
8
+ /** Run a binary and return its stdout trimmed, or null if it fails. */
9
+ function tryExecFile(binary, args) {
10
+ try {
11
+ return execFileSync(binary, args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ /** Check if a binary exists and optionally capture its version output. */
18
+ function checkBinary(id, name, binary, versionArgs, parseVersion) {
19
+ const out = tryExecFile(binary, versionArgs);
20
+ if (out === null)
21
+ return { id, name, found: false };
22
+ const version = parseVersion ? parseVersion(out) : out.split('\n')[0] ?? out;
23
+ return { id, name, found: true, version };
24
+ }
25
+ /** Check a pkg-config library. pkg-config --modversion returns version on stdout. */
26
+ function checkPkgConfig(id, name, libName) {
27
+ const version = tryExecFile('pkg-config', ['--modversion', libName]);
28
+ if (version === null)
29
+ return { id, name, found: false };
30
+ return { id, name, found: true, version: version.split('\n')[0] };
31
+ }
32
+ /**
33
+ * Check for an npm package by resolving it via Node.js module resolution from cwd.
34
+ * Mirrors the resolution logic used by esbuild / the build command.
35
+ */
36
+ function checkNpmPackage(id, name, packageName, cwd) {
37
+ try {
38
+ const requireFromCwd = createRequire(pathToFileURL(join(cwd, '_check_.js')).href);
39
+ requireFromCwd.resolve(packageName);
40
+ return { id, name, found: true };
41
+ }
42
+ catch {
43
+ return { id, name, found: false };
44
+ }
45
+ }
46
+ export function detectPackageManager() {
47
+ const managers = ['apt', 'dnf', 'pacman', 'zypper', 'apk'];
48
+ for (const pm of managers) {
49
+ const result = tryExecFile('which', [pm]);
50
+ if (result !== null)
51
+ return pm;
52
+ }
53
+ return 'unknown';
54
+ }
55
+ export function runAllChecks(cwd) {
56
+ const results = [];
57
+ // Node.js — always present
58
+ results.push({ id: 'nodejs', name: 'Node.js', found: true, version: process.version });
59
+ // GJS
60
+ results.push(checkBinary('gjs', 'GJS', 'gjs', ['--version'], (out) => out.replace(/^GJS\s+/i, '').split('\n')[0] ?? out));
61
+ // Blueprint Compiler
62
+ results.push(checkBinary('blueprint-compiler', 'Blueprint Compiler', 'blueprint-compiler', ['--version']));
63
+ // pkg-config (needed for library checks)
64
+ results.push(checkBinary('pkg-config', 'pkg-config', 'pkg-config', ['--version']));
65
+ // Meson (for building native extensions)
66
+ results.push(checkBinary('meson', 'Meson', 'meson', ['--version']));
67
+ // GTK4
68
+ results.push(checkPkgConfig('gtk4', 'GTK4', 'gtk4'));
69
+ // libadwaita
70
+ results.push(checkPkgConfig('libadwaita', 'libadwaita', 'libadwaita-1'));
71
+ // libsoup3
72
+ results.push(checkPkgConfig('libsoup3', 'libsoup3', 'libsoup-3.0'));
73
+ // WebKitGTK — try 4.1 first, fall back to 4.0
74
+ const webkitCheck = checkPkgConfig('webkitgtk', 'WebKitGTK', 'webkit2gtk-4.1');
75
+ if (webkitCheck.found) {
76
+ results.push(webkitCheck);
77
+ }
78
+ else {
79
+ results.push(checkPkgConfig('webkitgtk', 'WebKitGTK', 'webkitgtk-6.0'));
80
+ }
81
+ // GObject Introspection
82
+ results.push(checkPkgConfig('gobject-introspection', 'GObject Introspection', 'gobject-introspection-1.0'));
83
+ // gwebgl — resolve via Node.js module resolution from cwd, same as esbuild would.
84
+ results.push(checkNpmPackage('gwebgl', 'gwebgl (@gjsify/webgl)', '@gjsify/webgl', cwd));
85
+ return results;
86
+ }
87
+ // Per-package-manager install package names, keyed by dep id.
88
+ // Entries with multiple space-separated packages are intentional (one dep needs multiple system pkgs).
89
+ const PM_PACKAGES = {
90
+ apt: {
91
+ gjs: 'gjs',
92
+ 'blueprint-compiler': 'blueprint-compiler',
93
+ 'pkg-config': 'pkg-config',
94
+ meson: 'meson',
95
+ gtk4: 'libgtk-4-dev',
96
+ libadwaita: 'libadwaita-1-dev',
97
+ libsoup3: 'libsoup-3.0-dev',
98
+ webkitgtk: 'libwebkit2gtk-4.1-dev',
99
+ 'gobject-introspection': 'gobject-introspection libgirepository1.0-dev',
100
+ },
101
+ dnf: {
102
+ gjs: 'gjs',
103
+ 'blueprint-compiler': 'blueprint-compiler',
104
+ 'pkg-config': 'pkgconf-pkg-config',
105
+ meson: 'meson',
106
+ gtk4: 'gtk4-devel',
107
+ libadwaita: 'libadwaita-devel',
108
+ libsoup3: 'libsoup3-devel',
109
+ webkitgtk: 'webkitgtk6.0-devel',
110
+ 'gobject-introspection': 'gobject-introspection-devel',
111
+ },
112
+ pacman: {
113
+ gjs: 'gjs',
114
+ 'blueprint-compiler': 'blueprint-compiler',
115
+ 'pkg-config': 'pkgconf',
116
+ meson: 'meson',
117
+ gtk4: 'gtk4',
118
+ libadwaita: 'libadwaita',
119
+ libsoup3: 'libsoup3',
120
+ webkitgtk: 'webkit2gtk-4.1',
121
+ 'gobject-introspection': 'gobject-introspection',
122
+ },
123
+ zypper: {
124
+ gjs: 'gjs',
125
+ 'blueprint-compiler': 'blueprint-compiler',
126
+ 'pkg-config': 'pkg-config',
127
+ meson: 'meson',
128
+ gtk4: 'gtk4-devel',
129
+ libadwaita: 'libadwaita-devel',
130
+ libsoup3: 'libsoup-3_0-devel',
131
+ webkitgtk: 'webkit2gtk3-devel',
132
+ 'gobject-introspection': 'gobject-introspection-devel',
133
+ },
134
+ apk: {
135
+ gjs: 'gjs',
136
+ 'blueprint-compiler': 'blueprint-compiler',
137
+ 'pkg-config': 'pkgconf',
138
+ meson: 'meson',
139
+ gtk4: 'gtk4.0-dev',
140
+ libadwaita: 'libadwaita-dev',
141
+ libsoup3: 'libsoup3-dev',
142
+ webkitgtk: 'webkit2gtk-4.1-dev',
143
+ 'gobject-introspection': 'gobject-introspection-dev',
144
+ },
145
+ unknown: {},
146
+ };
147
+ const PM_INSTALL_PREFIX = {
148
+ apt: 'sudo apt install',
149
+ dnf: 'sudo dnf install',
150
+ pacman: 'sudo pacman -S',
151
+ zypper: 'sudo zypper install',
152
+ apk: 'sudo apk add',
153
+ unknown: '',
154
+ };
155
+ /**
156
+ * Build a suggested install command for missing dependencies.
157
+ * gwebgl is an npm package, not a system package — handled separately.
158
+ * Returns null when package manager is unknown or no installable deps are missing.
159
+ */
160
+ export function buildInstallCommand(pm, missing) {
161
+ if (pm === 'unknown')
162
+ return null;
163
+ const pkgMap = PM_PACKAGES[pm];
164
+ const pkgs = [];
165
+ const npmDeps = [];
166
+ for (const dep of missing) {
167
+ if (dep.id === 'gwebgl') {
168
+ npmDeps.push('@gjsify/webgl');
169
+ continue;
170
+ }
171
+ if (dep.id === 'nodejs')
172
+ continue; // can't be missing if we're running
173
+ const pkg = pkgMap[dep.id];
174
+ if (pkg)
175
+ pkgs.push(pkg);
176
+ }
177
+ const lines = [];
178
+ if (pkgs.length > 0) {
179
+ lines.push(`${PM_INSTALL_PREFIX[pm]} ${pkgs.join(' ')}`);
180
+ }
181
+ if (npmDeps.length > 0) {
182
+ lines.push(`npm install ${npmDeps.join(' ')}`);
183
+ }
184
+ return lines.length > 0 ? lines.join('\n ') : null;
185
+ }
@@ -12,6 +12,15 @@ export interface NativePackage {
12
12
  * resolves packages from the perspective of the calling project.
13
13
  */
14
14
  export declare function detectNativePackages(startDir: string): NativePackage[];
15
+ /**
16
+ * Resolve native packages using Node.js module resolution from a given file path.
17
+ * Reads the nearest package.json to discover dependencies, then checks each
18
+ * for gjsify native prebuilds metadata.
19
+ *
20
+ * This complements detectNativePackages() (filesystem walk from CWD) by using
21
+ * require.resolve() — which handles hoisting, workspaces, and nested node_modules.
22
+ */
23
+ export declare function resolveNativePackages(fromFilePath: string): NativePackage[];
15
24
  /**
16
25
  * Build the LD_LIBRARY_PATH and GI_TYPELIB_PATH env var values for the detected native packages.
17
26
  * Prepends the new paths to any existing values from the environment.
@@ -2,7 +2,9 @@
2
2
  // Packages declare: "gjsify": { "prebuilds": "<dir>" } in their package.json.
3
3
  // The CLI uses this to auto-set LD_LIBRARY_PATH / GI_TYPELIB_PATH before running gjs.
4
4
  import { readdirSync, existsSync, readFileSync } from 'node:fs';
5
- import { join, resolve } from 'node:path';
5
+ import { createRequire } from 'node:module';
6
+ import { dirname, join, resolve } from 'node:path';
7
+ import { pathToFileURL } from 'node:url';
6
8
  /** Map Node.js process.arch values to the convention used in prebuilds/ directories. */
7
9
  function nodeArchToLinuxArch(arch) {
8
10
  const map = {
@@ -105,6 +107,65 @@ export function detectNativePackages(startDir) {
105
107
  }
106
108
  return [];
107
109
  }
110
+ /** Walk up from dir to find the nearest package.json. */
111
+ function findNearestPackageJson(startDir) {
112
+ let dir = resolve(startDir);
113
+ while (true) {
114
+ const candidate = join(dir, 'package.json');
115
+ if (existsSync(candidate))
116
+ return candidate;
117
+ const parent = resolve(dir, '..');
118
+ if (parent === dir)
119
+ return null;
120
+ dir = parent;
121
+ }
122
+ }
123
+ /**
124
+ * Resolve native packages using Node.js module resolution from a given file path.
125
+ * Reads the nearest package.json to discover dependencies, then checks each
126
+ * for gjsify native prebuilds metadata.
127
+ *
128
+ * This complements detectNativePackages() (filesystem walk from CWD) by using
129
+ * require.resolve() — which handles hoisting, workspaces, and nested node_modules.
130
+ */
131
+ export function resolveNativePackages(fromFilePath) {
132
+ const arch = nodeArchToLinuxArch(process.arch);
133
+ const results = [];
134
+ try {
135
+ const req = createRequire(pathToFileURL(fromFilePath).href);
136
+ // Find the nearest package.json to get the dependency list
137
+ const nearestPkgJson = findNearestPackageJson(dirname(resolve(fromFilePath)));
138
+ if (!nearestPkgJson)
139
+ return results;
140
+ const pkg = readPackageJson(nearestPkgJson);
141
+ if (!pkg)
142
+ return results;
143
+ const deps = pkg['dependencies'];
144
+ if (!deps)
145
+ return results;
146
+ for (const depName of Object.keys(deps)) {
147
+ try {
148
+ // Resolve the package's main entry, then walk up to find its package.json.
149
+ // We cannot use require.resolve(name + '/package.json') because packages
150
+ // with an "exports" field may not expose ./package.json as a subpath.
151
+ const entryPath = req.resolve(depName);
152
+ const depPkgJson = findNearestPackageJson(dirname(entryPath));
153
+ if (!depPkgJson)
154
+ continue;
155
+ const native = checkPackage(dirname(depPkgJson), depName, arch);
156
+ if (native)
157
+ results.push(native);
158
+ }
159
+ catch {
160
+ // Dependency not resolvable — skip
161
+ }
162
+ }
163
+ }
164
+ catch {
165
+ // Resolution failed — return empty
166
+ }
167
+ return results;
168
+ }
108
169
  /**
109
170
  * Build the LD_LIBRARY_PATH and GI_TYPELIB_PATH env var values for the detected native packages.
110
171
  * Prepends the new paths to any existing values from the environment.
@@ -0,0 +1,19 @@
1
+ export interface ExampleInfo {
2
+ /** Short name, e.g. "three-geometry-shapes" */
3
+ name: string;
4
+ /** Full npm package name, e.g. "@gjsify/example-dom-three-geometry-shapes" */
5
+ packageName: string;
6
+ /** Category: "dom" or "node" */
7
+ category: string;
8
+ /** Description from example's package.json */
9
+ description: string;
10
+ /** Absolute path to the GJS bundle (resolved from "main" field) */
11
+ bundlePath: string;
12
+ }
13
+ /**
14
+ * Discover all installed example packages by scanning the CLI's own dependencies.
15
+ * Returns examples sorted by category then name.
16
+ */
17
+ export declare function discoverExamples(): ExampleInfo[];
18
+ /** Find a single example by short name. */
19
+ export declare function findExample(name: string): ExampleInfo | undefined;
@@ -0,0 +1,69 @@
1
+ // Dynamic discovery of installed @gjsify/example-* packages.
2
+ // Scans the CLI's own package.json dependencies at runtime.
3
+ import { readFileSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { createRequire } from 'node:module';
6
+ import { fileURLToPath } from 'node:url';
7
+ const EXAMPLE_PREFIX = '@gjsify/example-';
8
+ /** Extract short name and category from package name. */
9
+ function parseExampleName(packageName) {
10
+ // @gjsify/example-dom-three-geometry-shapes → category=dom, name=three-geometry-shapes
11
+ // @gjsify/example-node-cli-node-path → category=node, name=cli-node-path
12
+ const suffix = packageName.slice(EXAMPLE_PREFIX.length);
13
+ const dashIdx = suffix.indexOf('-');
14
+ if (dashIdx === -1)
15
+ return null;
16
+ return {
17
+ category: suffix.slice(0, dashIdx),
18
+ name: suffix.slice(dashIdx + 1),
19
+ };
20
+ }
21
+ /**
22
+ * Discover all installed example packages by scanning the CLI's own dependencies.
23
+ * Returns examples sorted by category then name.
24
+ */
25
+ export function discoverExamples() {
26
+ const require = createRequire(import.meta.url);
27
+ const cliPkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
28
+ let cliPkg;
29
+ try {
30
+ cliPkg = JSON.parse(readFileSync(cliPkgPath, 'utf-8'));
31
+ }
32
+ catch {
33
+ return [];
34
+ }
35
+ const deps = cliPkg['dependencies'];
36
+ if (!deps)
37
+ return [];
38
+ const examples = [];
39
+ for (const packageName of Object.keys(deps)) {
40
+ if (!packageName.startsWith(EXAMPLE_PREFIX))
41
+ continue;
42
+ const parsed = parseExampleName(packageName);
43
+ if (!parsed)
44
+ continue;
45
+ try {
46
+ const pkgJsonPath = require.resolve(`${packageName}/package.json`);
47
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
48
+ const main = pkg['main'];
49
+ if (!main)
50
+ continue;
51
+ examples.push({
52
+ name: parsed.name,
53
+ packageName,
54
+ category: parsed.category,
55
+ description: pkg['description'] ?? '',
56
+ bundlePath: join(dirname(pkgJsonPath), main),
57
+ });
58
+ }
59
+ catch {
60
+ // Package listed as dep but not resolvable — skip silently
61
+ }
62
+ }
63
+ examples.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
64
+ return examples;
65
+ }
66
+ /** Find a single example by short name. */
67
+ export function findExample(name) {
68
+ return discoverExamples().find(e => e.name === name);
69
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Run a GJS bundle, automatically setting LD_LIBRARY_PATH and GI_TYPELIB_PATH
3
+ * for any installed native gjsify packages.
4
+ *
5
+ * Detection uses two strategies:
6
+ * 1. Filesystem walk from CWD (finds packages in the user's project)
7
+ * 2. require.resolve from the bundle's location (finds packages in the CLI's dependency tree)
8
+ */
9
+ export declare function runGjsBundle(bundlePath: string, extraArgs?: string[]): Promise<void>;
@@ -0,0 +1,47 @@
1
+ // Shared utility for running a GJS bundle with native package env vars.
2
+ // Used by both the `run` and `showcase` commands.
3
+ import { spawn } from 'node:child_process';
4
+ import { resolve } from 'node:path';
5
+ import { detectNativePackages, resolveNativePackages, buildNativeEnv } from './detect-native-packages.js';
6
+ /**
7
+ * Run a GJS bundle, automatically setting LD_LIBRARY_PATH and GI_TYPELIB_PATH
8
+ * for any installed native gjsify packages.
9
+ *
10
+ * Detection uses two strategies:
11
+ * 1. Filesystem walk from CWD (finds packages in the user's project)
12
+ * 2. require.resolve from the bundle's location (finds packages in the CLI's dependency tree)
13
+ */
14
+ export async function runGjsBundle(bundlePath, extraArgs = []) {
15
+ const cwd = process.cwd();
16
+ const resolvedBundle = resolve(bundlePath);
17
+ // Detect from CWD (filesystem walk) + bundle location (require.resolve)
18
+ const cwdPackages = detectNativePackages(cwd);
19
+ const bundlePackages = resolveNativePackages(resolvedBundle);
20
+ // Merge, deduplicating by name (CWD takes precedence)
21
+ const seen = new Set(cwdPackages.map(p => p.name));
22
+ const nativePackages = [
23
+ ...cwdPackages,
24
+ ...bundlePackages.filter(p => !seen.has(p.name)),
25
+ ];
26
+ const nativeEnv = buildNativeEnv(nativePackages);
27
+ const env = {
28
+ ...process.env,
29
+ ...nativeEnv,
30
+ };
31
+ const gjsArgs = ['-m', bundlePath, ...extraArgs];
32
+ const child = spawn('gjs', gjsArgs, { env, stdio: 'inherit' });
33
+ await new Promise((resolvePromise, reject) => {
34
+ child.on('close', (code) => {
35
+ if (code !== 0) {
36
+ reject(new Error(`gjs exited with code ${code}`));
37
+ }
38
+ else {
39
+ resolvePromise();
40
+ }
41
+ });
42
+ child.on('error', reject);
43
+ }).catch((err) => {
44
+ console.error(err.message);
45
+ process.exit(1);
46
+ });
47
+ }