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