@gjsify/cli 0.3.14 → 0.3.15

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 (43) hide show
  1. package/lib/actions/build.d.ts +6 -2
  2. package/lib/actions/build.js +37 -8
  3. package/lib/commands/flatpak/build.d.ts +16 -0
  4. package/lib/commands/flatpak/build.js +187 -0
  5. package/lib/commands/flatpak/ci.d.ts +13 -0
  6. package/lib/commands/flatpak/ci.js +133 -0
  7. package/lib/commands/flatpak/deps.d.ts +12 -0
  8. package/lib/commands/flatpak/deps.js +96 -0
  9. package/lib/commands/flatpak/index.d.ts +7 -0
  10. package/lib/commands/flatpak/index.js +23 -0
  11. package/lib/commands/flatpak/init.d.ts +15 -0
  12. package/lib/commands/flatpak/init.js +154 -0
  13. package/lib/commands/flatpak/utils.d.ts +32 -0
  14. package/lib/commands/flatpak/utils.js +63 -0
  15. package/lib/commands/gsettings.d.ts +9 -0
  16. package/lib/commands/gsettings.js +72 -0
  17. package/lib/commands/index.d.ts +2 -0
  18. package/lib/commands/index.js +2 -0
  19. package/lib/commands/install.d.ts +1 -0
  20. package/lib/commands/install.js +66 -11
  21. package/lib/config.js +54 -2
  22. package/lib/index.js +3 -1
  23. package/lib/types/config-data.d.ts +145 -4
  24. package/lib/utils/install-global.d.ts +54 -0
  25. package/lib/utils/install-global.js +153 -0
  26. package/lib/utils/resolve-plugin-by-name.d.ts +21 -0
  27. package/lib/utils/resolve-plugin-by-name.js +75 -0
  28. package/package.json +10 -10
  29. package/src/actions/build.ts +41 -8
  30. package/src/commands/flatpak/build.ts +225 -0
  31. package/src/commands/flatpak/ci.ts +173 -0
  32. package/src/commands/flatpak/deps.ts +120 -0
  33. package/src/commands/flatpak/index.ts +53 -0
  34. package/src/commands/flatpak/init.ts +191 -0
  35. package/src/commands/flatpak/utils.ts +76 -0
  36. package/src/commands/gsettings.ts +87 -0
  37. package/src/commands/index.ts +2 -0
  38. package/src/commands/install.ts +90 -11
  39. package/src/config.ts +58 -2
  40. package/src/index.ts +4 -0
  41. package/src/types/config-data.ts +142 -4
  42. package/src/utils/install-global.ts +182 -0
  43. package/src/utils/resolve-plugin-by-name.ts +106 -0
@@ -0,0 +1,154 @@
1
+ // `gjsify flatpak init` — generate a Flatpak manifest from package.json
2
+ // + the `gjsify.flatpak` config namespace.
3
+ //
4
+ // Defaults are designed for the two real-world shapes:
5
+ // * GTK4 + Adwaita apps (Learn6502): `gnome` runtime, GUI finish-args
6
+ // * Headless CLI tools (ts-for-gir): same `gnome` runtime (GJS bundles
7
+ // need GLib/GIO at runtime — Freedesktop ships no GJS), but lean
8
+ // finish-args via `--cli-only`. Memory file
9
+ // `project_flatpak_runtime_choice.md` documents this trade-off.
10
+ import { existsSync, writeFileSync } from 'node:fs';
11
+ import { resolve } from 'node:path';
12
+ import { DEFAULT_CLI_FINISH_ARGS, DEFAULT_GUI_FINISH_ARGS, looksLikeAppId, readPackageJson, resolveRuntime, } from './utils.js';
13
+ import { Config } from '../../config.js';
14
+ export const flatpakInitCommand = {
15
+ command: 'init',
16
+ description: 'Generate a Flatpak manifest from package.json + `gjsify.flatpak` config.',
17
+ builder: (yargs) => {
18
+ return yargs
19
+ .option('app-id', {
20
+ description: 'Reverse-DNS app id (default: `gjsify.flatpak.appId` or package.json#name)',
21
+ type: 'string',
22
+ })
23
+ .option('runtime', {
24
+ description: 'Runtime family',
25
+ choices: ['gnome', 'freedesktop'],
26
+ })
27
+ .option('runtime-version', {
28
+ description: 'Runtime version (default: gnome -> 50, freedesktop -> 24.08)',
29
+ type: 'string',
30
+ })
31
+ .option('cli-only', {
32
+ description: 'Strip GUI finish-args; keep `gnome` runtime so GJS is available at runtime',
33
+ type: 'boolean',
34
+ default: false,
35
+ })
36
+ .option('manifest', {
37
+ description: 'Output path. Default: `<app-id>.json` in cwd.',
38
+ type: 'string',
39
+ normalize: true,
40
+ })
41
+ .option('command', {
42
+ description: 'Binary name in /app/bin (default: app id)',
43
+ type: 'string',
44
+ })
45
+ .option('sdk-extension', {
46
+ description: 'Extra SDK extension (repeatable)',
47
+ type: 'string',
48
+ array: true,
49
+ })
50
+ .option('finish-arg', {
51
+ description: 'Extra finish-arg (repeatable). Override defaults entirely with multiple --finish-arg.',
52
+ type: 'string',
53
+ array: true,
54
+ })
55
+ .option('force', {
56
+ description: 'Overwrite an existing manifest',
57
+ type: 'boolean',
58
+ default: false,
59
+ })
60
+ .option('verbose', {
61
+ description: 'Print the resolved manifest fields before writing',
62
+ type: 'boolean',
63
+ default: false,
64
+ });
65
+ },
66
+ handler: async (args) => {
67
+ const cfg = new Config();
68
+ const configData = await cfg.forBuild({}).catch(() => ({}));
69
+ const flatpak = configData.flatpak ?? {};
70
+ const cwd = process.cwd();
71
+ const pkg = readPackageJson(cwd);
72
+ const appId = args.appId ??
73
+ flatpak.appId ??
74
+ (looksLikeAppId(pkg.name) ? pkg.name : undefined);
75
+ if (!appId) {
76
+ throw new Error('gjsify flatpak init: no app id available. Pass --app-id, set gjsify.flatpak.appId in package.json, ' +
77
+ 'or rename the package to a reverse-DNS id like org.example.MyApp.');
78
+ }
79
+ const { runtime, runtimeId, sdk, runtimeVersion } = resolveRuntime(flatpak, {
80
+ runtime: args.runtime,
81
+ runtimeVersion: args.runtimeVersion,
82
+ });
83
+ const sdkExtensions = mergeArrays(flatpak.sdkExtensions, args.sdkExtension);
84
+ const appendPath = flatpak.appendPath ?? (sdkExtensions?.length ? deriveAppendPath(sdkExtensions) : undefined);
85
+ const command = args.command ?? flatpak.command ?? appId;
86
+ const explicitFinishArgs = args.finishArg;
87
+ const cliOnly = args.cliOnly === true;
88
+ const finishArgs = explicitFinishArgs !== undefined
89
+ ? explicitFinishArgs
90
+ : flatpak.finishArgs ??
91
+ (cliOnly ? DEFAULT_CLI_FINISH_ARGS : DEFAULT_GUI_FINISH_ARGS);
92
+ const manifest = {
93
+ id: appId,
94
+ runtime: runtimeId,
95
+ 'runtime-version': runtimeVersion,
96
+ sdk,
97
+ };
98
+ if (sdkExtensions?.length)
99
+ manifest['sdk-extensions'] = sdkExtensions;
100
+ if (appendPath?.length) {
101
+ manifest['build-options'] = { 'append-path': appendPath.join(':') };
102
+ }
103
+ manifest.command = command;
104
+ manifest['finish-args'] = finishArgs;
105
+ const cleanup = flatpak.cleanup;
106
+ if (cleanup?.length)
107
+ manifest.cleanup = cleanup;
108
+ // Modules: caller-supplied `extraModules` first, then the app's own
109
+ // meson module pointing at the source dir.
110
+ const modules = [];
111
+ if (flatpak.extraModules?.length)
112
+ modules.push(...flatpak.extraModules);
113
+ modules.push({
114
+ name: deriveModuleName(appId),
115
+ buildsystem: 'meson',
116
+ sources: [{ type: 'dir', path: '.' }],
117
+ });
118
+ manifest.modules = modules;
119
+ const out = args.manifest ?? `${appId}.json`;
120
+ const outPath = resolve(cwd, out);
121
+ if (existsSync(outPath) && !args.force) {
122
+ throw new Error(`gjsify flatpak init: ${outPath} exists. Pass --force to overwrite.`);
123
+ }
124
+ const json = JSON.stringify(manifest, null, 4) + '\n';
125
+ writeFileSync(outPath, json, 'utf-8');
126
+ if (args.verbose) {
127
+ console.log(`[gjsify flatpak init] runtime=${runtimeId} ${runtimeVersion} sdk=${sdk}`);
128
+ console.log(`[gjsify flatpak init] command=${command} finish-args=${JSON.stringify(finishArgs)}`);
129
+ }
130
+ console.log(`[gjsify flatpak init] wrote ${outPath}`);
131
+ },
132
+ };
133
+ /** Concatenate two optional arrays, dropping `undefined`. */
134
+ function mergeArrays(a, b) {
135
+ if (!a?.length && !b?.length)
136
+ return undefined;
137
+ return [...(a ?? []), ...(b ?? [])];
138
+ }
139
+ /** Map known SDK extension ids to their /usr/lib/sdk/<name>/bin paths. */
140
+ function deriveAppendPath(sdkExtensions) {
141
+ const out = [];
142
+ for (const ext of sdkExtensions) {
143
+ const m = /^org\.freedesktop\.Sdk\.Extension\.([A-Za-z0-9-]+)$/.exec(ext);
144
+ if (m)
145
+ out.push(`/usr/lib/sdk/${m[1]}/bin`);
146
+ }
147
+ out.push('/app/bin');
148
+ return out;
149
+ }
150
+ /** Last segment of the reverse-DNS id, used as the meson-module name. */
151
+ function deriveModuleName(appId) {
152
+ const parts = appId.split('.');
153
+ return parts[parts.length - 1] || appId;
154
+ }
@@ -0,0 +1,32 @@
1
+ import type { ConfigDataFlatpak } from '../../types/config-data.js';
2
+ /**
3
+ * Default GNOME-Platform runtime version. Bumped per release window.
4
+ * GNOME 50 = April 2026 stable; tracked in
5
+ * https://docs.flathub.org/docs/for-app-authors/requirements.
6
+ */
7
+ export declare const DEFAULT_GNOME_RUNTIME_VERSION = "50";
8
+ /** Default Freedesktop-Platform runtime version (LTS-ish). */
9
+ export declare const DEFAULT_FREEDESKTOP_RUNTIME_VERSION = "24.08";
10
+ /** Permissive GUI defaults for GTK4 + Adwaita apps. */
11
+ export declare const DEFAULT_GUI_FINISH_ARGS: string[];
12
+ /** Lean defaults for headless CLI tools — no display, no GPU. */
13
+ export declare const DEFAULT_CLI_FINISH_ARGS: string[];
14
+ /** Read package.json from a directory. Throws a helpful error if missing/invalid. */
15
+ export declare function readPackageJson(dir: string): Record<string, unknown>;
16
+ /** True if a name string looks like a reverse-DNS Flatpak app id. */
17
+ export declare function looksLikeAppId(value: unknown): value is string;
18
+ /**
19
+ * Pick the runtime + sdk + version triple from config + CLI overrides.
20
+ * `--runtime` and `--runtime-version` flags win over config values.
21
+ */
22
+ export declare function resolveRuntime(flatpak: ConfigDataFlatpak | undefined, overrides: {
23
+ runtime?: string;
24
+ runtimeVersion?: string;
25
+ }): {
26
+ runtime: 'gnome' | 'freedesktop';
27
+ runtimeId: string;
28
+ sdk: string;
29
+ runtimeVersion: string;
30
+ };
31
+ /** Default container image for the GitHub Actions workflow. */
32
+ export declare function defaultCiContainer(runtime: 'gnome' | 'freedesktop', runtimeVersion: string): string;
@@ -0,0 +1,63 @@
1
+ // Shared helpers for the `gjsify flatpak <sub>` subcommand group.
2
+ import { readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ /**
5
+ * Default GNOME-Platform runtime version. Bumped per release window.
6
+ * GNOME 50 = April 2026 stable; tracked in
7
+ * https://docs.flathub.org/docs/for-app-authors/requirements.
8
+ */
9
+ export const DEFAULT_GNOME_RUNTIME_VERSION = '50';
10
+ /** Default Freedesktop-Platform runtime version (LTS-ish). */
11
+ export const DEFAULT_FREEDESKTOP_RUNTIME_VERSION = '24.08';
12
+ /** Permissive GUI defaults for GTK4 + Adwaita apps. */
13
+ export const DEFAULT_GUI_FINISH_ARGS = [
14
+ '--device=dri',
15
+ '--share=ipc',
16
+ '--socket=fallback-x11',
17
+ '--socket=wayland',
18
+ ];
19
+ /** Lean defaults for headless CLI tools — no display, no GPU. */
20
+ export const DEFAULT_CLI_FINISH_ARGS = [];
21
+ /** Read package.json from a directory. Throws a helpful error if missing/invalid. */
22
+ export function readPackageJson(dir) {
23
+ const path = resolve(dir, 'package.json');
24
+ let raw;
25
+ try {
26
+ raw = readFileSync(path, 'utf-8');
27
+ }
28
+ catch {
29
+ throw new Error(`gjsify flatpak: no package.json found at ${path}`);
30
+ }
31
+ try {
32
+ return JSON.parse(raw);
33
+ }
34
+ catch (err) {
35
+ throw new Error(`gjsify flatpak: package.json at ${path} is not valid JSON: ${err.message}`);
36
+ }
37
+ }
38
+ /** True if a name string looks like a reverse-DNS Flatpak app id. */
39
+ export function looksLikeAppId(value) {
40
+ return typeof value === 'string' && /^[A-Za-z][A-Za-z0-9_-]*(\.[A-Za-z][A-Za-z0-9_-]*){2,}$/.test(value);
41
+ }
42
+ /**
43
+ * Pick the runtime + sdk + version triple from config + CLI overrides.
44
+ * `--runtime` and `--runtime-version` flags win over config values.
45
+ */
46
+ export function resolveRuntime(flatpak, overrides) {
47
+ const runtime = (overrides.runtime ?? flatpak?.runtime ?? 'gnome');
48
+ if (runtime !== 'gnome' && runtime !== 'freedesktop') {
49
+ throw new Error(`gjsify flatpak: unknown runtime "${runtime}" (expected "gnome" or "freedesktop")`);
50
+ }
51
+ const runtimeVersion = overrides.runtimeVersion ??
52
+ flatpak?.runtimeVersion ??
53
+ (runtime === 'gnome' ? DEFAULT_GNOME_RUNTIME_VERSION : DEFAULT_FREEDESKTOP_RUNTIME_VERSION);
54
+ if (runtime === 'gnome') {
55
+ return { runtime, runtimeId: 'org.gnome.Platform', sdk: 'org.gnome.Sdk', runtimeVersion };
56
+ }
57
+ return { runtime, runtimeId: 'org.freedesktop.Platform', sdk: 'org.freedesktop.Sdk', runtimeVersion };
58
+ }
59
+ /** Default container image for the GitHub Actions workflow. */
60
+ export function defaultCiContainer(runtime, runtimeVersion) {
61
+ const tag = `${runtime}-${runtimeVersion}`;
62
+ return `ghcr.io/flathub-infra/flatpak-github-actions:${tag}`;
63
+ }
@@ -0,0 +1,9 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface GSettingsOptions {
3
+ schemadir: string;
4
+ targetdir?: string;
5
+ strict?: boolean;
6
+ verbose?: boolean;
7
+ }
8
+ export declare const gsettingsCommand: Command<any, GSettingsOptions>;
9
+ export {};
@@ -0,0 +1,72 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ const execFileAsync = promisify(execFile);
6
+ export const gsettingsCommand = {
7
+ command: 'gsettings <schemadir>',
8
+ description: 'Compile GSettings schema XML files into a binary gschemas.compiled (wraps `glib-compile-schemas`).',
9
+ builder: (yargs) => {
10
+ return yargs
11
+ .positional('schemadir', {
12
+ description: 'Directory containing *.gschema.xml descriptors',
13
+ type: 'string',
14
+ normalize: true,
15
+ demandOption: true,
16
+ })
17
+ .option('targetdir', {
18
+ alias: 't',
19
+ description: 'Directory to write gschemas.compiled (default: <schemadir>)',
20
+ type: 'string',
21
+ normalize: true,
22
+ })
23
+ .option('strict', {
24
+ description: 'Abort on any schema warning (passes --strict to glib-compile-schemas)',
25
+ type: 'boolean',
26
+ default: true,
27
+ })
28
+ .option('verbose', {
29
+ description: 'Print the underlying glib-compile-schemas invocation',
30
+ type: 'boolean',
31
+ default: false,
32
+ });
33
+ },
34
+ handler: async (args) => {
35
+ const schemadir = resolve(args.schemadir);
36
+ const targetdir = args.targetdir
37
+ ? resolve(args.targetdir)
38
+ : schemadir;
39
+ const cmdArgs = [];
40
+ if (args.strict)
41
+ cmdArgs.push('--strict');
42
+ cmdArgs.push(`--targetdir=${targetdir}`);
43
+ cmdArgs.push(schemadir);
44
+ if (args.verbose) {
45
+ console.log(`[gjsify gsettings] glib-compile-schemas ${cmdArgs.join(' ')}`);
46
+ }
47
+ // glib-compile-schemas writes a temporary file in `targetdir` before
48
+ // renaming to gschemas.compiled — fails with ENOENT if missing.
49
+ await mkdir(targetdir, { recursive: true });
50
+ try {
51
+ const { stdout, stderr } = await execFileAsync('glib-compile-schemas', cmdArgs);
52
+ if (stdout)
53
+ process.stdout.write(stdout);
54
+ if (stderr)
55
+ process.stderr.write(stderr);
56
+ if (args.verbose) {
57
+ console.log(`[gjsify gsettings] wrote ${targetdir}/gschemas.compiled`);
58
+ }
59
+ }
60
+ catch (err) {
61
+ if (err?.code === 'ENOENT') {
62
+ console.error('[gjsify gsettings] glib-compile-schemas not found. Install it via your distro (package: glib2-devel / libglib2.0-dev).');
63
+ }
64
+ else {
65
+ if (err?.stderr)
66
+ process.stderr.write(err.stderr);
67
+ console.error(`[gjsify gsettings] glib-compile-schemas failed${err?.code !== undefined ? ` (exit ${err.code})` : ''}`);
68
+ }
69
+ process.exitCode = typeof err?.code === 'number' ? err.code : 1;
70
+ }
71
+ },
72
+ };
@@ -6,5 +6,7 @@ export * from './showcase.js';
6
6
  export * from './create.js';
7
7
  export * from './gresource.js';
8
8
  export * from './gettext.js';
9
+ export * from './gsettings.js';
10
+ export { flatpakCommand } from './flatpak/index.js';
9
11
  export * from './dlx.js';
10
12
  export * from './install.js';
@@ -6,5 +6,7 @@ export * from './showcase.js';
6
6
  export * from './create.js';
7
7
  export * from './gresource.js';
8
8
  export * from './gettext.js';
9
+ export * from './gsettings.js';
10
+ export { flatpakCommand } from './flatpak/index.js';
9
11
  export * from './dlx.js';
10
12
  export * from './install.js';
@@ -1,6 +1,7 @@
1
1
  import type { Command } from '../types/index.js';
2
2
  interface InstallOptions {
3
3
  packages?: string[];
4
+ global?: boolean;
4
5
  'save-dev'?: boolean;
5
6
  'save-peer'?: boolean;
6
7
  'save-optional'?: boolean;
@@ -1,36 +1,65 @@
1
- // `gjsify install [pkg...]` — thin npm wrapper with gjsify-aware post-checks.
2
- //
3
- // The actual install is delegated to `npm install` in the user's project root
4
- // (no `--prefix` rewrite, unlike `gjsify dlx`). After install completes we run
5
- // `runMinimalChecks()` so missing system deps (gjs, gtk4, libsoup, ...) surface
6
- // immediately, and report any installed `@gjsify/*` packages that ship native
7
- // prebuilds so users know they can use `gjsify run` to wire `LD_LIBRARY_PATH` /
8
- // `GI_TYPELIB_PATH` automatically.
1
+ // `gjsify install [pkg...]` — install packages with gjsify-aware post-checks.
9
2
  //
10
3
  // Modes:
11
4
  // gjsify install → project install (npm install)
12
- // gjsify install <pkg> [<pkg>...] → add package(s) (npm install <pkg>...)
5
+ // gjsify install <pkg> [<pkg>...] → add package(s) to project (npm install <pkg>...)
6
+ // gjsify install -g <pkg> [...] → user-global install (XDG, GJS-runnable bin)
7
+ //
8
+ // Project mode delegates to `npm install` in cwd and runs `runMinimalChecks()`
9
+ // + `detectNativePackages()` to surface missing system deps and `@gjsify/*`
10
+ // packages with native prebuilds.
11
+ //
12
+ // Global mode is the GJS equivalent of `npm i -g`: extracts the package tree
13
+ // into `${XDG_DATA_HOME}/gjsify/global/node_modules/<pkg>/` via the native
14
+ // install backend (no Node/npm required at runtime), then symlinks the bins
15
+ // declared by `gjsify.bin` (preferred) or `bin` (fallback) into
16
+ // `~/.local/bin/`. Subsequent commands invoked by name resolve to the
17
+ // extracted package, so package-relative assets like `@ts-for-gir/cli`'s
18
+ // `dist-templates/` are found by ordinary `__dirname/..` resolution — no
19
+ // embedded asset stores, no separate release tarballs.
13
20
  import { spawn } from 'node:child_process';
21
+ import { mkdirSync } from 'node:fs';
14
22
  import { buildInstallCommand, detectPackageManager, runMinimalChecks, } from '../utils/check-system-deps.js';
15
23
  import { detectNativePackages } from '../utils/detect-native-packages.js';
24
+ import { installPackages } from '../utils/install-backend.js';
25
+ import { binDirOnPath, defaultGlobalLayout, linkGlobalBins, specToPackageName, } from '../utils/install-global.js';
16
26
  export const installCommand = {
17
27
  command: 'install [packages..]',
18
- description: 'Install npm dependencies in the current project, then run gjsify-aware post-checks.',
28
+ description: 'Install npm dependencies in the current project (or globally with -g), then run gjsify-aware post-checks.',
19
29
  builder: (yargs) => yargs
20
30
  .positional('packages', {
21
31
  description: 'Optional package specs. With none, runs a full project install.',
22
32
  type: 'string',
23
33
  array: true,
34
+ })
35
+ .option('global', {
36
+ description: 'Install into the user-global XDG location and symlink bins into ~/.local/bin.',
37
+ type: 'boolean',
38
+ alias: 'g',
39
+ default: false,
24
40
  })
25
41
  .option('save-dev', { type: 'boolean', alias: 'D' })
26
42
  .option('save-peer', { type: 'boolean' })
27
43
  .option('save-optional', { type: 'boolean', alias: 'O' })
28
44
  .option('verbose', {
29
- description: 'Verbose npm logging.',
45
+ description: 'Verbose install logging.',
30
46
  type: 'boolean',
31
47
  default: false,
32
48
  }),
33
49
  handler: async (args) => {
50
+ if (args.global) {
51
+ if (!args.packages || args.packages.length === 0) {
52
+ console.error('gjsify install --global requires at least one <pkg> argument.');
53
+ process.exit(1);
54
+ }
55
+ for (const flag of ['save-dev', 'save-peer', 'save-optional']) {
56
+ if (args[flag]) {
57
+ console.warn(`gjsify install --global ignores --${flag}: global installs do not modify a project package.json.`);
58
+ }
59
+ }
60
+ await installGlobalAndLink(args.packages, { verbose: args.verbose });
61
+ return;
62
+ }
34
63
  const npmArgs = ['install'];
35
64
  if (args['save-dev'])
36
65
  npmArgs.push('--save-dev');
@@ -68,6 +97,32 @@ async function spawnNpm(npmArgs) {
68
97
  process.exit(1);
69
98
  });
70
99
  }
100
+ async function installGlobalAndLink(specs, opts) {
101
+ const layout = defaultGlobalLayout();
102
+ mkdirSync(layout.prefix, { recursive: true });
103
+ console.log(`gjsify install --global → ${layout.prefix}`);
104
+ console.log(` bins → ${layout.binDir}`);
105
+ await installPackages({
106
+ prefix: layout.prefix,
107
+ specs,
108
+ verbose: opts.verbose,
109
+ });
110
+ const packageNames = specs.map(specToPackageName);
111
+ const created = linkGlobalBins(packageNames, layout);
112
+ if (created.length === 0) {
113
+ console.warn('\nNo bins declared (neither `gjsify.bin` nor `bin` in package.json) — nothing was symlinked.');
114
+ }
115
+ else {
116
+ console.log(`\nLinked ${created.length} bin(s):`);
117
+ for (const e of created) {
118
+ console.log(` • ${e.link} → ${e.target}`);
119
+ }
120
+ }
121
+ if (created.length > 0 && !binDirOnPath(layout.binDir)) {
122
+ console.warn(`\nNote: ${layout.binDir} is not on your PATH.\n` +
123
+ `Add it to your shell rc file:\n export PATH="${layout.binDir}:$PATH"`);
124
+ }
125
+ }
71
126
  async function runPostInstallChecks() {
72
127
  console.log('\n--- gjsify post-install checks ---');
73
128
  // 1. System deps that GJS apps typically need.
package/lib/config.js CHANGED
@@ -43,6 +43,23 @@ function merge(target, ...sources) {
43
43
  function isPlainObject(val) {
44
44
  return typeof val === 'object' && val !== null && !Array.isArray(val) && Object.getPrototypeOf(val) === Object.prototype;
45
45
  }
46
+ /**
47
+ * Read a dotted path (`a.b.c`) from a plain object. Returns `undefined` for
48
+ * any missing segment. Intentionally narrow — only used for surfacing
49
+ * `package.json` fields into compile-time defines, not for arbitrary deep
50
+ * traversal.
51
+ */
52
+ function readDottedPath(obj, path) {
53
+ if (!path.includes('.'))
54
+ return obj[path];
55
+ let cursor = obj;
56
+ for (const segment of path.split('.')) {
57
+ if (cursor === null || cursor === undefined || typeof cursor !== 'object')
58
+ return undefined;
59
+ cursor = cursor[segment];
60
+ }
61
+ return cursor;
62
+ }
46
63
  export class Config {
47
64
  loadOptions = {};
48
65
  constructor(loadOptions = {}) {
@@ -155,6 +172,34 @@ export class Config {
155
172
  if (Object.keys(aliasMap).length) {
156
173
  configData.aliases = { ...(configData.aliases ?? {}), ...aliasMap };
157
174
  }
175
+ // Resolve `defineFromPackageJson` / `defineFromEnv` into raw
176
+ // KEY=<JSON-stringified value> entries that get merged into the
177
+ // bundler's `transform.define` map below. Both produce JS expressions
178
+ // (the value side of a Rolldown define is substituted at the call
179
+ // site, not stringified again) — so a missing env variable resolves
180
+ // to the literal `undefined`, letting consumer code use
181
+ // `typeof X === 'undefined'` or `X ?? fallback` guards.
182
+ const fromPkgDefines = {};
183
+ if (configData.defineFromPackageJson) {
184
+ for (const [name, spec] of Object.entries(configData.defineFromPackageJson)) {
185
+ if (!spec || typeof spec.field !== 'string' || !spec.field) {
186
+ throw new Error(`gjsify config: defineFromPackageJson["${name}"] is missing a "field" string`);
187
+ }
188
+ const value = readDottedPath(pkg, spec.field);
189
+ fromPkgDefines[name] = value === undefined ? 'undefined' : JSON.stringify(value);
190
+ }
191
+ }
192
+ const fromEnvDefines = {};
193
+ if (configData.defineFromEnv) {
194
+ for (const [name, spec] of Object.entries(configData.defineFromEnv)) {
195
+ if (!spec || typeof spec.env !== 'string' || !spec.env) {
196
+ throw new Error(`gjsify config: defineFromEnv["${name}"] is missing an "env" string`);
197
+ }
198
+ const raw = process.env[spec.env];
199
+ const value = raw !== undefined ? raw : spec.default;
200
+ fromEnvDefines[name] = value === undefined ? 'undefined' : JSON.stringify(value);
201
+ }
202
+ }
158
203
  // Merge CLI flags into the Rolldown-shape `bundler` field. Mappings:
159
204
  // --entry-points → bundler.input
160
205
  // --outfile → bundler.output.file
@@ -202,8 +247,15 @@ export class Config {
202
247
  const userExternal = Array.isArray(bundler.external) ? bundler.external : [];
203
248
  bundler.external = [...userExternal, ...cliArgs.external];
204
249
  }
205
- if (Object.keys(defineMap).length) {
206
- transform.define = { ...(transform.define ?? {}), ...defineMap };
250
+ if (Object.keys(defineMap).length || Object.keys(fromPkgDefines).length || Object.keys(fromEnvDefines).length) {
251
+ // CLI --define wins over package.json/env (manual overrides during
252
+ // debugging beat declarative config).
253
+ transform.define = {
254
+ ...(transform.define ?? {}),
255
+ ...fromPkgDefines,
256
+ ...fromEnvDefines,
257
+ ...defineMap,
258
+ };
207
259
  }
208
260
  if (configData.verbose)
209
261
  console.debug("configData", configData);
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, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, dlxCommand as dlx, installCommand as install, } from './commands/index.js';
4
+ import { buildCommand as build, runCommand as run, infoCommand as info, 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, } from './commands/index.js';
5
5
  import { APP_NAME } from './constants.js';
6
6
  void yargs(hideBin(process.argv))
7
7
  .scriptName(APP_NAME)
@@ -16,5 +16,7 @@ void yargs(hideBin(process.argv))
16
16
  .command(showcase.command, showcase.description, showcase.builder, showcase.handler)
17
17
  .command(gresource.command, gresource.description, gresource.builder, gresource.handler)
18
18
  .command(gettext.command, gettext.description, gettext.builder, gettext.handler)
19
+ .command(gsettings.command, gsettings.description, gsettings.builder, gsettings.handler)
20
+ .command(flatpak.command, flatpak.description, flatpak.builder, flatpak.handler)
19
21
  .demandCommand(1)
20
22
  .help().argv;