@gjsify/cli 0.4.12 → 0.4.14

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.
@@ -35,9 +35,19 @@ export declare class BuildAction {
35
35
  */
36
36
  private applyShebang;
37
37
  /** Application mode */
38
- buildApp(app?: App): Promise<RolldownOutput[]>;
38
+ buildApp(app?: App, opts?: {
39
+ watch?: boolean;
40
+ }): Promise<RolldownOutput[]>;
41
+ /**
42
+ * Drive `rolldown.watch(...)`: rebuild on source change, apply the
43
+ * post-bundle shebang hook on each successful build, surface errors
44
+ * without exiting, clean up on SIGINT/SIGTERM. Resolves only when the
45
+ * watcher closes — keeps the CLI process alive across rebuilds.
46
+ */
47
+ private runWatchLoop;
39
48
  start(buildType?: {
40
49
  library?: boolean;
41
50
  app?: App;
51
+ watch?: boolean;
42
52
  }): Promise<RolldownOutput[]>;
43
53
  }
@@ -1,4 +1,4 @@
1
- import { runBundle, bundleToChunks } from "../bundler-pick.js";
1
+ import { runBundle, runWatch, bundleToChunks } from "../bundler-pick.js";
2
2
  import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine } from "@gjsify/rolldown-plugin-gjsify";
3
3
  import { resolveUserPlugins } from "../utils/resolve-plugin-by-name.js";
4
4
  import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals, } from "@gjsify/rolldown-plugin-gjsify/globals";
@@ -178,7 +178,7 @@ export class BuildAction {
178
178
  console.debug(`[gjsify] --shebang: wrote ${line} + chmod 0o755 to ${outfile}`);
179
179
  }
180
180
  /** Application mode */
181
- async buildApp(app = "gjs") {
181
+ async buildApp(app = "gjs", opts = {}) {
182
182
  const { verbose, typescript, exclude, library: pkg, aliases, excludeGlobals, } = this.configData;
183
183
  const userBundler = normalizeBundlerOptions(this.configData);
184
184
  const formatRaw = userBundler.output?.format ??
@@ -281,17 +281,93 @@ export class BuildAction {
281
281
  // Rolldown doesn't understand) would crash the build.
282
282
  plugins: [...pnpPlugins, ...userPlugins, ...cfg.plugins],
283
283
  };
284
+ if (opts.watch) {
285
+ await this.runWatchLoop(finalOpts, app, outfile, verbose);
286
+ return [];
287
+ }
284
288
  const writeResult = await runBundle(finalOpts);
285
289
  if (app === "gjs" && this.configData.shebang) {
286
290
  await this.applyShebang(outfile, verbose);
287
291
  }
288
292
  return [writeResult];
289
293
  }
294
+ /**
295
+ * Drive `rolldown.watch(...)`: rebuild on source change, apply the
296
+ * post-bundle shebang hook on each successful build, surface errors
297
+ * without exiting, clean up on SIGINT/SIGTERM. Resolves only when the
298
+ * watcher closes — keeps the CLI process alive across rebuilds.
299
+ */
300
+ async runWatchLoop(finalOpts, app, outfile, verbose) {
301
+ const watcher = await runWatch(finalOpts);
302
+ const closed = new Promise((resolve) => {
303
+ watcher.on("close", () => resolve());
304
+ });
305
+ let closing = false;
306
+ const shutdown = async () => {
307
+ if (closing)
308
+ return;
309
+ closing = true;
310
+ console.log("\n[gjsify build --watch] stopping watcher…");
311
+ try {
312
+ await watcher.close();
313
+ }
314
+ catch (err) {
315
+ console.error("[gjsify build --watch] watcher close error:", err);
316
+ }
317
+ };
318
+ process.on("SIGINT", shutdown);
319
+ process.on("SIGTERM", shutdown);
320
+ watcher.on("event", async (event) => {
321
+ switch (event.code) {
322
+ case "START":
323
+ if (verbose)
324
+ console.log("[gjsify build --watch] rebuild start");
325
+ break;
326
+ case "BUNDLE_START":
327
+ console.log("[gjsify build --watch] building…");
328
+ break;
329
+ case "BUNDLE_END":
330
+ console.log(`[gjsify build --watch] built in ${event.duration}ms`);
331
+ try {
332
+ if (app === "gjs" && this.configData.shebang) {
333
+ await this.applyShebang(outfile, verbose);
334
+ }
335
+ }
336
+ finally {
337
+ await event.result.close();
338
+ }
339
+ break;
340
+ case "END":
341
+ console.log("[gjsify build --watch] waiting for changes…");
342
+ break;
343
+ case "ERROR":
344
+ console.error("[gjsify build --watch] build failed:", event.error?.message ?? event.error);
345
+ if (verbose && event.error?.stack)
346
+ console.error(event.error.stack);
347
+ try {
348
+ await event.result.close();
349
+ }
350
+ catch {
351
+ // best-effort cleanup
352
+ }
353
+ break;
354
+ }
355
+ });
356
+ if (verbose) {
357
+ watcher.on("change", (id, change) => {
358
+ console.log(`[gjsify build --watch] ${change.event}: ${id}`);
359
+ });
360
+ }
361
+ await closed;
362
+ }
290
363
  async start(buildType = { app: "gjs" }) {
291
364
  if (buildType.library) {
365
+ if (buildType.watch) {
366
+ throw new Error("gjsify build: --watch is not supported with --library (library mode would emit watcher rebuilds for every produced format; use --app gjs|node|browser instead).");
367
+ }
292
368
  return await this.buildLibrary();
293
369
  }
294
- return await this.buildApp(buildType.app);
370
+ return await this.buildApp(buildType.app, { watch: buildType.watch });
295
371
  }
296
372
  }
297
373
  async function runOneLibraryBuild(args) {
@@ -65,6 +65,13 @@ export declare function bundleToChunks(input: {
65
65
  rolldownInput: import('rolldown').InputOptions;
66
66
  format: 'esm' | 'cjs' | 'iife';
67
67
  }): Promise<string[]>;
68
+ /**
69
+ * Watch source files and rebuild on change. Only npm rolldown supports
70
+ * this path — `@gjsify/rolldown-native` does not surface a watcher API yet.
71
+ * Returns the watcher; the caller registers `event` / `close` listeners
72
+ * and is responsible for invoking `watcher.close()` on shutdown.
73
+ */
74
+ export declare function runWatch(finalOpts: BundlerOptions): Promise<import('rolldown').RolldownWatcher>;
68
75
  /**
69
76
  * Run a bundle with the picked engine. Drop-in replacement for the
70
77
  * `rolldown(opts).write(opts.output)` flow used directly in build.ts.
@@ -86,6 +86,23 @@ export async function bundleToChunks(input) {
86
86
  await build.close();
87
87
  }
88
88
  }
89
+ /**
90
+ * Watch source files and rebuild on change. Only npm rolldown supports
91
+ * this path — `@gjsify/rolldown-native` does not surface a watcher API yet.
92
+ * Returns the watcher; the caller registers `event` / `close` listeners
93
+ * and is responsible for invoking `watcher.close()` on shutdown.
94
+ */
95
+ export async function runWatch(finalOpts) {
96
+ if (await shouldUseNative()) {
97
+ throw new Error('`gjsify build --watch` requires the npm `rolldown` engine. The native engine ' +
98
+ '(`@gjsify/rolldown-native`) does not expose a watcher API. Run the watch loop ' +
99
+ 'under Node (`node lib/index.js build … --watch`) or set `GJSIFY_BUNDLER=npm`.');
100
+ }
101
+ const specifier = 'rolldown';
102
+ const mod = (await import(/* @vite-ignore */ specifier));
103
+ const output = finalOpts.output ?? {};
104
+ return mod.watch({ ...finalOpts, output });
105
+ }
89
106
  /**
90
107
  * Run a bundle with the picked engine. Drop-in replacement for the
91
108
  * `rolldown(opts).write(opts.output)` flow used directly in build.ts.
@@ -130,6 +130,13 @@ export const buildCommand = {
130
130
  description: "Comma-separated global identifiers to remove from auto-detection results. Use for false positives from dead browser-compat code whose polyfills require unavailable native libraries (e.g. --exclude-globals fetch,XMLHttpRequest).",
131
131
  type: 'string',
132
132
  normalize: true,
133
+ })
134
+ .option('watch', {
135
+ alias: 'w',
136
+ description: "Watch source files and rebuild on change. Logs each rebuild with duration; clean SIGINT shutdown. Only valid with --app gjs|node|browser (rejected with --library). Requires the npm `rolldown` engine — run under Node, not the GJS-bundled CLI.",
137
+ type: 'boolean',
138
+ normalize: true,
139
+ default: false,
133
140
  });
134
141
  },
135
142
  handler: async (args) => {
@@ -139,6 +146,7 @@ export const buildCommand = {
139
146
  await action.start({
140
147
  library: args.library,
141
148
  app: args.app,
149
+ watch: args.watch,
142
150
  });
143
151
  }
144
152
  };
@@ -0,0 +1,9 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface FixOptions {
3
+ paths?: string[];
4
+ write?: boolean;
5
+ configPath?: string;
6
+ verbose?: boolean;
7
+ }
8
+ export declare const fixCommand: Command<unknown, FixOptions>;
9
+ export {};
@@ -0,0 +1,60 @@
1
+ // `gjsify fix` — runs biome's combined `check --write` mode.
2
+ //
3
+ // Equivalent to biome's `check` (format + safe-lint-fix + organize-imports).
4
+ // Default writes fixes in-place; pass `--no-write` to report-only.
5
+ // Naming: deliberately distinct from `gjsify check` (which verifies
6
+ // system dependencies).
7
+ import { resolve } from 'node:path';
8
+ import { BiomeNotFoundError, findBiomeConfig, printBiomeNotFound, runBiome, } from '../utils/biome-resolve.js';
9
+ export const fixCommand = {
10
+ command: 'fix [paths..]',
11
+ description: 'Run Biome check --write — format + safe-lint-fix + organize-imports in one pass.',
12
+ builder: (yargs) => {
13
+ return yargs
14
+ .positional('paths', {
15
+ description: 'Files or directories to fix. Default: `.`',
16
+ type: 'string',
17
+ array: true,
18
+ })
19
+ .option('write', {
20
+ description: 'Apply fixes in place (default: true). Pass --no-write to report only.',
21
+ type: 'boolean',
22
+ default: true,
23
+ })
24
+ .option('config-path', {
25
+ description: 'Path to a biome.json. Default: walks up from cwd to find one.',
26
+ type: 'string',
27
+ normalize: true,
28
+ })
29
+ .option('verbose', {
30
+ description: 'Echo the resolved biome binary + args before spawning.',
31
+ type: 'boolean',
32
+ default: false,
33
+ });
34
+ },
35
+ handler: async (args) => {
36
+ const cwd = process.cwd();
37
+ const paths = args.paths?.length
38
+ ? args.paths
39
+ : ['.'];
40
+ const biomeArgs = ['check'];
41
+ if (args.write !== false)
42
+ biomeArgs.push('--write');
43
+ const configPath = args.configPath ?? findBiomeConfig(cwd) ?? undefined;
44
+ if (configPath)
45
+ biomeArgs.push(`--config-path=${resolve(configPath, '..')}`);
46
+ biomeArgs.push(...paths);
47
+ try {
48
+ const code = await runBiome(biomeArgs, { cwd, verbose: args.verbose });
49
+ process.exitCode = code;
50
+ }
51
+ catch (err) {
52
+ if (err instanceof BiomeNotFoundError) {
53
+ printBiomeNotFound(err);
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ throw err;
58
+ }
59
+ },
60
+ };
@@ -0,0 +1,12 @@
1
+ import type { Command } from '../../types/index.js';
2
+ interface DiffOptions {
3
+ version?: string;
4
+ appId?: string;
5
+ flathubRepo?: string;
6
+ against?: string;
7
+ detail?: boolean;
8
+ sourceIndex?: number;
9
+ verbose?: boolean;
10
+ }
11
+ export declare const flatpakDiffCommand: Command<unknown, DiffOptions>;
12
+ export {};
@@ -0,0 +1,165 @@
1
+ // `gjsify flatpak diff` — report drift between the local config + git
2
+ // state and the per-app Flathub tracking-repo manifest.
3
+ //
4
+ // The most common failure mode is *version drift*: a release was cut
5
+ // locally (a new `git tag`), but the Flathub-repo manifest still pins
6
+ // the previous tag, so end-users on flatpak install never see the
7
+ // release. This command surfaces that drift before
8
+ // `gjsify flatpak sync-flathub` is needed (the symmetric command that
9
+ // fixes it).
10
+ //
11
+ // Workflow:
12
+ // 1. Resolve appId + flathub-repo (same precedence as sync-flathub)
13
+ // 2. Fetch the Flathub manifest (or read `--against <local>`)
14
+ // 3. Resolve the local latest git tag (`git describe --tags --abbrev=0`)
15
+ // 4. Compare module[0].sources's `tag` + `commit` to the local state
16
+ // 5. Exit 0 when in sync, 1 when there's drift, with a clear message
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import { execFile } from 'node:child_process';
19
+ import { promisify } from 'node:util';
20
+ import { Config } from '../../config.js';
21
+ import { readPackageJson } from './utils.js';
22
+ const execFileAsync = promisify(execFile);
23
+ export const flatpakDiffCommand = {
24
+ command: 'diff',
25
+ description: 'Compare the per-app Flathub tracking-repo manifest against the local git state and report version / commit drift.',
26
+ builder: (yargs) => {
27
+ return yargs
28
+ .version(false)
29
+ .option('version', {
30
+ description: 'Local version to compare against. Default: `git describe --tags --abbrev=0` in cwd.',
31
+ type: 'string',
32
+ })
33
+ .option('app-id', {
34
+ description: 'Reverse-DNS app id. Default: `gjsify.flatpak.appId`.',
35
+ type: 'string',
36
+ })
37
+ .option('flathub-repo', {
38
+ description: 'Flathub tracking-repo (owner/name). Default: `gjsify.flatpak.flathubRepo` or `flathub/<appId>`.',
39
+ type: 'string',
40
+ })
41
+ .option('against', {
42
+ description: 'Read the Flathub manifest from a local file instead of fetching it. Useful in CI or offline.',
43
+ type: 'string',
44
+ })
45
+ .option('detail', {
46
+ description: 'Also print the full Flathub manifest source entry alongside the resolved local version.',
47
+ type: 'boolean',
48
+ default: false,
49
+ })
50
+ .option('source-index', {
51
+ description: 'Index into modules[0].sources[] to inspect (when the manifest has multiple sources).',
52
+ type: 'number',
53
+ })
54
+ .option('verbose', {
55
+ description: 'Echo fetch URL + resolved values.',
56
+ type: 'boolean',
57
+ default: false,
58
+ });
59
+ },
60
+ handler: async (args) => {
61
+ const cwd = process.cwd();
62
+ const cfg = new Config();
63
+ const configData = await cfg.forBuild({}).catch(() => ({}));
64
+ const flatpak = configData.flatpak ?? {};
65
+ const appId = args.appId ??
66
+ flatpak.appId ??
67
+ readPackageJson(cwd).name;
68
+ if (!appId) {
69
+ throw new Error('[gjsify flatpak diff] no app id available — pass --app-id or set gjsify.flatpak.appId.');
70
+ }
71
+ const flathubRepo = args.flathubRepo ??
72
+ flatpak.flathubRepo ??
73
+ `flathub/${appId}`;
74
+ const localVersion = args.version ?? (await resolveLatestTag(cwd, args.verbose));
75
+ const remoteSource = await loadFlathubSource({ appId, flathubRepo, against: args.against, verbose: args.verbose }, args.sourceIndex);
76
+ const remoteTag = remoteSource?.tag;
77
+ const remoteCommit = remoteSource?.commit;
78
+ console.log(`[gjsify flatpak diff] appId=${appId}`);
79
+ console.log(`[gjsify flatpak diff] flathubRepo=${flathubRepo}`);
80
+ console.log(`[gjsify flatpak diff] flathub: tag=${remoteTag ?? '(missing)'} commit=${remoteCommit ?? '(missing)'}`);
81
+ console.log(`[gjsify flatpak diff] local: tag=${localVersion ?? '(none)'}`);
82
+ if (args.detail && remoteSource) {
83
+ console.log('[gjsify flatpak diff] flathub manifest source:');
84
+ console.log(JSON.stringify(remoteSource, null, 2));
85
+ }
86
+ if (!localVersion) {
87
+ console.warn('[gjsify flatpak diff] no local git tag found — cut a release (`git tag vX.Y.Z`) ' +
88
+ 'or pass --version vX.Y.Z to compare against an explicit value.');
89
+ // Still surface remote state, but exit cleanly so this isn't a fatal CI step.
90
+ return;
91
+ }
92
+ if (!remoteTag) {
93
+ console.warn('[gjsify flatpak diff] flathub manifest has no `tag` field on the inspected source.');
94
+ process.exit(1);
95
+ }
96
+ if (remoteTag === localVersion) {
97
+ console.log(`✅ in sync (${localVersion})`);
98
+ return;
99
+ }
100
+ console.log(`❌ drift detected — flathub=${remoteTag} vs local=${localVersion}`);
101
+ console.log(` run \`gjsify flatpak sync-flathub --version ${localVersion}\` to update the Flathub manifest.`);
102
+ process.exit(1);
103
+ },
104
+ };
105
+ // ─── Internal helpers ────────────────────────────────────────────────────
106
+ async function loadFlathubSource(args, sourceIndex) {
107
+ let raw;
108
+ if (args.against) {
109
+ if (!existsSync(args.against)) {
110
+ throw new Error(`[gjsify flatpak diff] --against path ${args.against} does not exist`);
111
+ }
112
+ raw = readFileSync(args.against, 'utf-8');
113
+ }
114
+ else {
115
+ // Try main + master raw URLs on GitHub. Flathub repos historically use
116
+ // master; newer ones may use main. No XDG cache: fresh fetch each run.
117
+ raw = await fetchFlathubManifest(args.flathubRepo, args.appId, args.verbose);
118
+ }
119
+ let manifest;
120
+ try {
121
+ manifest = JSON.parse(raw);
122
+ }
123
+ catch (err) {
124
+ throw new Error(`[gjsify flatpak diff] failed to parse Flathub manifest as JSON: ${err.message}`);
125
+ }
126
+ const modules = manifest.modules ?? [];
127
+ const sources = modules[0]?.sources ?? [];
128
+ if (sources.length === 0)
129
+ return null;
130
+ const idx = sourceIndex ?? sources.findIndex((s) => s?.type === 'git');
131
+ if (idx < 0 || idx >= sources.length)
132
+ return null;
133
+ return sources[idx] ?? null;
134
+ }
135
+ async function fetchFlathubManifest(flathubRepo, appId, verbose) {
136
+ for (const branch of ['master', 'main']) {
137
+ const url = `https://raw.githubusercontent.com/${flathubRepo}/${branch}/${appId}.json`;
138
+ if (verbose)
139
+ console.log(`[gjsify flatpak diff] fetch ${url}`);
140
+ try {
141
+ const res = await fetch(url);
142
+ if (res.ok)
143
+ return await res.text();
144
+ if (verbose)
145
+ console.log(`[gjsify flatpak diff] ${branch} → HTTP ${res.status}`);
146
+ }
147
+ catch (err) {
148
+ if (verbose)
149
+ console.log(`[gjsify flatpak diff] ${branch} fetch error: ${err.message}`);
150
+ }
151
+ }
152
+ throw new Error(`[gjsify flatpak diff] could not fetch flathub manifest from ${flathubRepo} on master or main`);
153
+ }
154
+ async function resolveLatestTag(cwd, verbose) {
155
+ try {
156
+ const { stdout } = await execFileAsync('git', ['describe', '--tags', '--abbrev=0'], { cwd });
157
+ const tag = stdout.trim();
158
+ if (verbose)
159
+ console.log(`[gjsify flatpak diff] local latest tag → ${tag}`);
160
+ return tag || null;
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ }
@@ -4,5 +4,8 @@ import { flatpakBuildCommand } from './build.js';
4
4
  import { flatpakDepsCommand } from './deps.js';
5
5
  import { flatpakCiCommand } from './ci.js';
6
6
  import { flatpakCheckCommand } from './check.js';
7
+ import { flatpakSyncFlathubCommand } from './sync-flathub.js';
8
+ import { flatpakDiffCommand } from './diff.js';
9
+ import { flatpakReleaseCommand } from './release.js';
7
10
  export declare const flatpakCommand: Command;
8
- export { flatpakInitCommand, flatpakBuildCommand, flatpakDepsCommand, flatpakCiCommand, flatpakCheckCommand, };
11
+ export { flatpakInitCommand, flatpakBuildCommand, flatpakDepsCommand, flatpakCiCommand, flatpakCheckCommand, flatpakSyncFlathubCommand, flatpakDiffCommand, flatpakReleaseCommand, };
@@ -1,16 +1,19 @@
1
1
  // `gjsify flatpak` — yargs subcommand-group dispatcher.
2
2
  //
3
- // Wires {init, build, deps, ci, check}. Each subcommand is a self-contained
4
- // `Command<>` so it composes the same way as `gresource` / `gettext` /
5
- // `gsettings` at the top level.
3
+ // Wires {init, build, deps, ci, check, sync-flathub, diff, release}.
4
+ // Each subcommand is a self-contained `Command<>` so it composes the
5
+ // same way as `gresource` / `gettext` / `gsettings` at the top level.
6
6
  import { flatpakInitCommand } from './init.js';
7
7
  import { flatpakBuildCommand } from './build.js';
8
8
  import { flatpakDepsCommand } from './deps.js';
9
9
  import { flatpakCiCommand } from './ci.js';
10
10
  import { flatpakCheckCommand } from './check.js';
11
+ import { flatpakSyncFlathubCommand } from './sync-flathub.js';
12
+ import { flatpakDiffCommand } from './diff.js';
13
+ import { flatpakReleaseCommand } from './release.js';
11
14
  export const flatpakCommand = {
12
15
  command: 'flatpak <subcommand>',
13
- description: 'Flatpak toolchain: init/build/deps/ci/check subcommands for shipping GJS apps and CLIs as Flatpaks.',
16
+ description: 'Flatpak toolchain: init/build/deps/ci/check/sync-flathub/diff/release subcommands for shipping GJS apps and CLIs as Flatpaks.',
14
17
  builder: (yargs) => {
15
18
  return yargs
16
19
  .command(flatpakInitCommand.command, flatpakInitCommand.description, flatpakInitCommand.builder, flatpakInitCommand.handler)
@@ -18,8 +21,11 @@ export const flatpakCommand = {
18
21
  .command(flatpakDepsCommand.command, flatpakDepsCommand.description, flatpakDepsCommand.builder, flatpakDepsCommand.handler)
19
22
  .command(flatpakCiCommand.command, flatpakCiCommand.description, flatpakCiCommand.builder, flatpakCiCommand.handler)
20
23
  .command(flatpakCheckCommand.command, flatpakCheckCommand.description, flatpakCheckCommand.builder, flatpakCheckCommand.handler)
24
+ .command(flatpakSyncFlathubCommand.command, flatpakSyncFlathubCommand.description, flatpakSyncFlathubCommand.builder, flatpakSyncFlathubCommand.handler)
25
+ .command(flatpakDiffCommand.command, flatpakDiffCommand.description, flatpakDiffCommand.builder, flatpakDiffCommand.handler)
26
+ .command(flatpakReleaseCommand.command, flatpakReleaseCommand.description, flatpakReleaseCommand.builder, flatpakReleaseCommand.handler)
21
27
  .demandCommand(1)
22
28
  .strict();
23
29
  },
24
30
  };
25
- export { flatpakInitCommand, flatpakBuildCommand, flatpakDepsCommand, flatpakCiCommand, flatpakCheckCommand, };
31
+ export { flatpakInitCommand, flatpakBuildCommand, flatpakDepsCommand, flatpakCiCommand, flatpakCheckCommand, flatpakSyncFlathubCommand, flatpakDiffCommand, flatpakReleaseCommand, };
@@ -14,6 +14,7 @@ interface FlatpakInitOptions {
14
14
  sdkExtension?: string[];
15
15
  finishArg?: string[];
16
16
  verbose?: boolean;
17
+ format?: boolean;
17
18
  }
18
19
  export declare const flatpakInitCommand: Command<unknown, FlatpakInitOptions>;
19
20
  export {};
@@ -16,6 +16,7 @@ import { dirname, resolve } from 'node:path';
16
16
  import { DEFAULT_CLI_FINISH_ARGS, DEFAULT_GUI_FINISH_ARGS, looksLikeAppId, readPackageJson, resolveRuntime, } from './utils.js';
17
17
  import { renderDesktop, renderFlathubJson, renderMetainfoApp, renderMetainfoCli, validateScaffoldInputs, } from './scaffold.js';
18
18
  import { Config } from '../../config.js';
19
+ import { BiomeNotFoundError, hasBiomeDevDep, runBiome, } from '../../utils/biome-resolve.js';
19
20
  export const flatpakInitCommand = {
20
21
  command: 'init',
21
22
  description: 'Generate Flatpak manifest + MetaInfo XML + .desktop + flathub.json from `gjsify.flatpak` config.',
@@ -85,6 +86,12 @@ export const flatpakInitCommand = {
85
86
  description: 'Print the resolved manifest fields before writing',
86
87
  type: 'boolean',
87
88
  default: false,
89
+ })
90
+ .option('format', {
91
+ description: 'Run `gjsify format --write` on the generated files when `@biomejs/biome` is detected in the project. ' +
92
+ 'Default: true. Pass --no-format to skip.',
93
+ type: 'boolean',
94
+ default: true,
88
95
  });
89
96
  },
90
97
  handler: async (args) => {
@@ -140,13 +147,18 @@ export const flatpakInitCommand = {
140
147
  sources: [{ type: 'dir', path: '.' }],
141
148
  });
142
149
  manifest.modules = modules;
150
+ const writtenFiles = [];
151
+ const trackWrite = (p) => {
152
+ if (p)
153
+ writtenFiles.push(p);
154
+ };
143
155
  const manifestOut = args.manifest ?? `${appId}.json`;
144
156
  const manifestPath = resolve(cwd, manifestOut);
145
- writeIfFresh(manifestPath, JSON.stringify(manifest, null, 4) + '\n', args.force ?? false, 'manifest');
146
- const name = pkg.name ?? appId;
157
+ trackWrite(writeIfFresh(manifestPath, JSON.stringify(manifest, null, 2) + '\n', args.force ?? false, 'manifest'));
158
+ const pkgName = pkg.name ?? appId;
147
159
  const scaffold = {
148
160
  appId,
149
- name: friendlyName(name, appId),
161
+ name: flatpak.name ?? friendlyName(pkgName, appId),
150
162
  command,
151
163
  kind,
152
164
  flatpak,
@@ -161,17 +173,38 @@ export const flatpakInitCommand = {
161
173
  else {
162
174
  const metainfoXml = kind === 'cli' ? renderMetainfoCli(scaffold) : renderMetainfoApp(scaffold);
163
175
  const metainfoOut = args.metainfo ?? `data/${appId}.metainfo.xml.in`;
164
- writeIfFresh(resolve(cwd, metainfoOut), metainfoXml, args.force ?? false, 'metainfo');
176
+ trackWrite(writeIfFresh(resolve(cwd, metainfoOut), metainfoXml, args.force ?? false, 'metainfo'));
165
177
  if (kind === 'app') {
166
178
  const desktopOut = args.desktop ?? `data/${appId}.desktop.in`;
167
- writeIfFresh(resolve(cwd, desktopOut), renderDesktop(scaffold), args.force ?? false, 'desktop');
179
+ trackWrite(writeIfFresh(resolve(cwd, desktopOut), renderDesktop(scaffold), args.force ?? false, 'desktop'));
168
180
  if (!flatpak.icon) {
169
181
  console.warn(`[gjsify flatpak init] No gjsify.flatpak.icon set. Flathub requires a scalable SVG at\n` +
170
182
  ` data/icons/hicolor/scalable/apps/${appId}.svg`);
171
183
  }
172
184
  }
173
185
  const flathubOut = args.flathubJson ?? 'flathub.json';
174
- writeIfFresh(resolve(cwd, flathubOut), renderFlathubJson(kind), args.force ?? false, 'flathub.json');
186
+ trackWrite(writeIfFresh(resolve(cwd, flathubOut), renderFlathubJson(kind), args.force ?? false, 'flathub.json'));
187
+ }
188
+ // Optional post-format: when biome is configured in the project,
189
+ // run `biome format --write` on the generated files so they match
190
+ // the project's prettier/biome style. Default behaviour (2-space
191
+ // JSON) already matches biome/prettier/Flathub defaults; this
192
+ // step harmonises edge-case fields (line endings, trailing commas
193
+ // in JSONC, key sort order if biome's organize-imports has rules).
194
+ if (writtenFiles.length > 0 && args.format !== false && hasBiomeDevDep(cwd)) {
195
+ try {
196
+ await runBiome(['format', '--write', ...writtenFiles], { cwd });
197
+ }
198
+ catch (err) {
199
+ if (err instanceof BiomeNotFoundError) {
200
+ // Biome configured but binary missing — non-fatal warning.
201
+ console.warn(`[gjsify flatpak init] post-format skipped: @biomejs/biome declared but binary not installed. ` +
202
+ `Run \`gjsify install\` then re-run with --force, or pass --no-format.`);
203
+ }
204
+ else {
205
+ throw err;
206
+ }
207
+ }
175
208
  }
176
209
  if (args.verbose) {
177
210
  console.log(`[gjsify flatpak init] kind=${kind} runtime=${runtimeId} ${runtimeVersion} sdk=${sdk}`);
@@ -183,11 +216,12 @@ export const flatpakInitCommand = {
183
216
  function writeIfFresh(path, content, force, label) {
184
217
  if (existsSync(path) && !force) {
185
218
  console.log(`[gjsify flatpak init] skipped ${label}: ${path} (exists; --force to overwrite)`);
186
- return;
219
+ return null;
187
220
  }
188
221
  mkdirSync(dirname(path), { recursive: true });
189
222
  writeFileSync(path, content, 'utf-8');
190
223
  console.log(`[gjsify flatpak init] wrote ${label}: ${path}`);
224
+ return path;
191
225
  }
192
226
  function friendlyName(pkgName, appId) {
193
227
  if (pkgName.startsWith('@')) {
@@ -0,0 +1,13 @@
1
+ import type { Command } from '../../types/index.js';
2
+ interface ReleaseOptions {
3
+ version: string;
4
+ skipTag?: boolean;
5
+ skipCheck?: boolean;
6
+ skipInit?: boolean;
7
+ pushTag?: boolean;
8
+ dryRun?: boolean;
9
+ flathubRepo?: string;
10
+ verbose?: boolean;
11
+ }
12
+ export declare const flatpakReleaseCommand: Command<unknown, ReleaseOptions>;
13
+ export {};