@gjsify/cli 0.4.13 → 0.4.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 (41) hide show
  1. package/dist/cli.gjs.mjs +86 -78
  2. package/lib/actions/barrels-generate.d.ts +31 -0
  3. package/lib/actions/barrels-generate.js +78 -0
  4. package/lib/actions/build.d.ts +11 -1
  5. package/lib/actions/build.js +79 -3
  6. package/lib/bundler-pick.d.ts +7 -0
  7. package/lib/bundler-pick.js +17 -0
  8. package/lib/commands/barrels.d.ts +15 -0
  9. package/lib/commands/barrels.js +103 -0
  10. package/lib/commands/build.js +8 -0
  11. package/lib/commands/fix.d.ts +9 -0
  12. package/lib/commands/fix.js +60 -0
  13. package/lib/commands/flatpak/diff.d.ts +12 -0
  14. package/lib/commands/flatpak/diff.js +165 -0
  15. package/lib/commands/flatpak/index.d.ts +4 -1
  16. package/lib/commands/flatpak/index.js +11 -5
  17. package/lib/commands/flatpak/init.d.ts +1 -0
  18. package/lib/commands/flatpak/init.js +39 -5
  19. package/lib/commands/flatpak/release.d.ts +13 -0
  20. package/lib/commands/flatpak/release.js +152 -0
  21. package/lib/commands/flatpak/sync-flathub.d.ts +26 -0
  22. package/lib/commands/flatpak/sync-flathub.js +311 -0
  23. package/lib/commands/format.d.ts +12 -0
  24. package/lib/commands/format.js +98 -0
  25. package/lib/commands/index.d.ts +6 -0
  26. package/lib/commands/index.js +6 -0
  27. package/lib/commands/install.js +11 -9
  28. package/lib/commands/lint.d.ts +9 -0
  29. package/lib/commands/lint.js +60 -0
  30. package/lib/commands/test.d.ts +12 -0
  31. package/lib/commands/test.js +206 -0
  32. package/lib/commands/upgrade.d.ts +13 -0
  33. package/lib/commands/upgrade.js +402 -0
  34. package/lib/index.js +7 -1
  35. package/lib/templates/biome.json.tmpl +79 -0
  36. package/lib/types/cli-build-options.d.ts +7 -0
  37. package/lib/types/config-data.d.ts +39 -0
  38. package/lib/utils/biome-resolve.d.ts +47 -0
  39. package/lib/utils/biome-resolve.js +204 -0
  40. package/package.json +16 -16
  41. package/showcases.json +14 -0
@@ -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,9 +147,14 @@ 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');
157
+ trackWrite(writeIfFresh(manifestPath, JSON.stringify(manifest, null, 2) + '\n', args.force ?? false, 'manifest'));
146
158
  const pkgName = pkg.name ?? appId;
147
159
  const scaffold = {
148
160
  appId,
@@ -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 {};
@@ -0,0 +1,152 @@
1
+ // `gjsify flatpak release` — orchestrate the full release-to-Flathub flow.
2
+ //
3
+ // One command chains the same steps a maintainer otherwise runs by hand
4
+ // after cutting a release:
5
+ // 1. Regenerate Flathub assets from `gjsify.flatpak` config (delegates to
6
+ // `gjsify flatpak init --force`)
7
+ // 2. Run Flathub linters (delegates to `gjsify flatpak check`)
8
+ // 3. Create + push the git tag (unless `--skip-tag`)
9
+ // 4. Sync the per-app Flathub tracking-repo (delegates to
10
+ // `gjsify flatpak sync-flathub`)
11
+ //
12
+ // Each step shells out to the same CLI binary so the user sees identical
13
+ // behaviour to running the sub-commands directly. The orchestrator
14
+ // stops at the first failure and reports which step blocked, with a
15
+ // clear command the user can re-run by hand.
16
+ import { execFile, spawn } from 'node:child_process';
17
+ import { promisify } from 'node:util';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { resolve, dirname } from 'node:path';
20
+ const execFileAsync = promisify(execFile);
21
+ export const flatpakReleaseCommand = {
22
+ command: 'release <version>',
23
+ description: 'Cut a release end-to-end: regenerate Flathub assets, run linters, create + push the git tag, then open the Flathub PR. Each step delegates to the equivalent `gjsify flatpak <sub>` command.',
24
+ builder: (yargs) => {
25
+ return yargs
26
+ // yargs' built-in `--version` flag would otherwise consume the
27
+ // positional value.
28
+ .version(false)
29
+ .positional('version', {
30
+ description: 'Release tag, e.g. `v0.6.6`.',
31
+ type: 'string',
32
+ demandOption: true,
33
+ })
34
+ .option('skip-init', {
35
+ description: 'Skip the `flatpak init --force` regen step.',
36
+ type: 'boolean',
37
+ default: false,
38
+ })
39
+ .option('skip-check', {
40
+ description: 'Skip the `flatpak check` linter step.',
41
+ type: 'boolean',
42
+ default: false,
43
+ })
44
+ .option('skip-tag', {
45
+ description: 'Skip the `git tag` + `git push --tags` step (use when the tag was already created out-of-band).',
46
+ type: 'boolean',
47
+ default: false,
48
+ })
49
+ .option('push-tag', {
50
+ description: 'Push the created tag after creating it. Default: true.',
51
+ type: 'boolean',
52
+ default: true,
53
+ })
54
+ .option('flathub-repo', {
55
+ description: 'Flathub tracking-repo override forwarded to sync-flathub.',
56
+ type: 'string',
57
+ })
58
+ .option('dry-run', {
59
+ description: 'Print each step that would run without executing any of them.',
60
+ type: 'boolean',
61
+ default: false,
62
+ })
63
+ .option('verbose', {
64
+ description: 'Echo every sub-command invocation.',
65
+ type: 'boolean',
66
+ default: false,
67
+ });
68
+ },
69
+ handler: async (args) => {
70
+ const version = typeof args.version === 'string' ? args.version.trim() : '';
71
+ if (!version) {
72
+ throw new Error('[gjsify flatpak release] missing <version> positional');
73
+ }
74
+ const cliEntry = resolveCliEntry();
75
+ const cwd = process.cwd();
76
+ const steps = [];
77
+ if (!args.skipInit) {
78
+ steps.push({ name: 'init', args: [cliEntry, 'flatpak', 'init', '--force'] });
79
+ }
80
+ if (!args.skipCheck) {
81
+ steps.push({ name: 'check', args: [cliEntry, 'flatpak', 'check'] });
82
+ }
83
+ const syncArgs = [cliEntry, 'flatpak', 'sync-flathub', '--version', version];
84
+ if (args.flathubRepo)
85
+ syncArgs.push('--flathub-repo', args.flathubRepo);
86
+ if (args.verbose)
87
+ syncArgs.push('--verbose');
88
+ console.log(`[gjsify flatpak release] starting release of ${version}`);
89
+ if (args.dryRun) {
90
+ console.log('[gjsify flatpak release] --dry-run set; printing plan only:');
91
+ for (const s of steps)
92
+ console.log(` · ${s.name}: node ${s.args.join(' ')}`);
93
+ if (!args.skipTag)
94
+ console.log(` · tag: git tag ${version}${args.pushTag !== false ? ' && git push origin ' + version : ''}`);
95
+ console.log(` · sync: node ${syncArgs.join(' ')}`);
96
+ return;
97
+ }
98
+ // Run init + check in sequence (must succeed before any git mutation).
99
+ for (const step of steps) {
100
+ if (args.verbose)
101
+ console.log(`[gjsify flatpak release] step ${step.name}: node ${step.args.join(' ')}`);
102
+ await runNode(step.args, cwd);
103
+ console.log(`[gjsify flatpak release] ${step.name} ✔`);
104
+ }
105
+ // Tag creation — only after init + check succeed so we don't end up
106
+ // with a tag pointing at a broken release.
107
+ if (!args.skipTag) {
108
+ if (args.verbose)
109
+ console.log(`[gjsify flatpak release] git tag ${version}`);
110
+ try {
111
+ await execFileAsync('git', ['tag', version], { cwd });
112
+ console.log(`[gjsify flatpak release] tag ${version} created`);
113
+ }
114
+ catch (err) {
115
+ throw new Error(`[gjsify flatpak release] git tag failed (${err.message}). ` +
116
+ `If the tag already exists, re-run with --skip-tag.`);
117
+ }
118
+ if (args.pushTag !== false) {
119
+ if (args.verbose)
120
+ console.log(`[gjsify flatpak release] git push origin ${version}`);
121
+ await execFileAsync('git', ['push', 'origin', version], { cwd });
122
+ console.log(`[gjsify flatpak release] tag pushed`);
123
+ }
124
+ }
125
+ // Sync flathub last.
126
+ if (args.verbose)
127
+ console.log(`[gjsify flatpak release] sync: node ${syncArgs.join(' ')}`);
128
+ await runNode(syncArgs, cwd);
129
+ console.log(`[gjsify flatpak release] ✅ release ${version} complete`);
130
+ },
131
+ };
132
+ /** Resolve a path to the gjsify CLI entry to invoke sub-commands. */
133
+ function resolveCliEntry() {
134
+ // `commands/flatpak/release.ts` → compiled into
135
+ // `lib/commands/flatpak/release.js`. The CLI entry is `lib/index.js`.
136
+ const here = fileURLToPath(import.meta.url);
137
+ const cliRoot = resolve(dirname(here), '..', '..');
138
+ return resolve(cliRoot, 'index.js');
139
+ }
140
+ /** Spawn `node <args>` and reject on non-zero exit. */
141
+ async function runNode(args, cwd) {
142
+ await new Promise((resolvePromise, reject) => {
143
+ const child = spawn('node', args, { cwd, stdio: 'inherit' });
144
+ child.on('error', reject);
145
+ child.on('exit', (code) => {
146
+ if (code === 0)
147
+ resolvePromise();
148
+ else
149
+ reject(new Error(`sub-command exited with code ${code}: node ${args.join(' ')}`));
150
+ });
151
+ });
152
+ }
@@ -0,0 +1,26 @@
1
+ import type { Command } from '../../types/index.js';
2
+ interface SyncFlathubOptions {
3
+ version?: string;
4
+ appId?: string;
5
+ flathubRepo?: string;
6
+ commit?: string;
7
+ branch?: string;
8
+ sourceIndex?: number;
9
+ pr?: boolean;
10
+ dryRun?: boolean;
11
+ verbose?: boolean;
12
+ }
13
+ export declare const flatpakSyncFlathubCommand: Command<unknown, SyncFlathubOptions>;
14
+ /**
15
+ * Surgically edit a Flathub manifest to update the git source's `tag` +
16
+ * `commit` (and inject `x-checker-data` if missing). Preserves the
17
+ * original indent + whitespace + key ordering of the surrounding JSON
18
+ * by parsing through JSON.parse + re-stringifying with the detected
19
+ * indent.
20
+ */
21
+ export declare function editManifest(original: string, args: {
22
+ tag: string;
23
+ commit: string;
24
+ sourceIndex?: number;
25
+ }): string;
26
+ export {};
@@ -0,0 +1,311 @@
1
+ // `gjsify flatpak sync-flathub` — sync the per-app Flathub tracking-repo.
2
+ //
3
+ // Flathub distributes each app via a separate `flathub/<app-id>` repo
4
+ // whose manifest pins a specific git tag + commit-SHA of the upstream
5
+ // source. After cutting a new release in the source repo, the Flathub
6
+ // repo's manifest needs to be updated to reference the new tag/commit.
7
+ // This command automates that workflow:
8
+ //
9
+ // 1. Resolve appId + flathub-repo (from gjsify.flatpak config / flags)
10
+ // 2. Resolve --version (explicit flag or `git describe --tags --abbrev=0`)
11
+ // 3. Resolve commit SHA via `git rev-list -n 1 <version>`
12
+ // 4. Clone (or update) the flathub tracking-repo into $XDG_CACHE_HOME
13
+ // 5. Surgically edit the manifest: modules[].sources[].{tag,commit}
14
+ // plus x-checker-data block (inject if missing)
15
+ // 6. Create a branch, commit the changes
16
+ // 7. (unless --no-pr) push + open a PR via `gh pr create`
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
18
+ import { join, resolve } from 'node:path';
19
+ import { homedir } from 'node:os';
20
+ import { execFile, spawn } from 'node:child_process';
21
+ import { promisify } from 'node:util';
22
+ import { Config } from '../../config.js';
23
+ import { readPackageJson } from './utils.js';
24
+ const execFileAsync = promisify(execFile);
25
+ export const flatpakSyncFlathubCommand = {
26
+ command: 'sync-flathub',
27
+ description: 'Update the per-app Flathub tracking-repo manifest to a new git tag + commit. Clones, edits, commits, optionally opens a PR.',
28
+ builder: (yargs) => {
29
+ return yargs
30
+ // Disable yargs' built-in `--version` (which would otherwise
31
+ // print the package version) so this command's `--version
32
+ // <tag>` flag works.
33
+ .version(false)
34
+ .option('version', {
35
+ description: 'Git tag to sync to. Default: `git describe --tags --abbrev=0` in cwd.',
36
+ type: 'string',
37
+ })
38
+ .option('app-id', {
39
+ description: 'Reverse-DNS app id. Default: `gjsify.flatpak.appId`.',
40
+ type: 'string',
41
+ })
42
+ .option('flathub-repo', {
43
+ description: 'Flathub tracking-repo (owner/name). Default: `flathub/<appId>`.',
44
+ type: 'string',
45
+ })
46
+ .option('commit', {
47
+ description: 'Commit SHA to pin. Default: resolved via `git rev-list -n 1 <version>` in cwd.',
48
+ type: 'string',
49
+ })
50
+ .option('branch', {
51
+ description: 'Branch name in the flathub-repo. Default: `update-to-<version>`.',
52
+ type: 'string',
53
+ })
54
+ .option('source-index', {
55
+ description: 'Index into modules[0].sources[] to update (when manifest has multiple sources). Default: first `type: git` source.',
56
+ type: 'number',
57
+ })
58
+ .option('pr', {
59
+ description: 'After commit + push, open a PR via `gh pr create`. Pass `--no-pr` to skip and stop after push.',
60
+ type: 'boolean',
61
+ default: true,
62
+ })
63
+ .option('dry-run', {
64
+ description: 'Show what would be edited; touch no files, run no git commands.',
65
+ type: 'boolean',
66
+ default: false,
67
+ })
68
+ .option('verbose', {
69
+ description: 'Echo every git / gh invocation before running.',
70
+ type: 'boolean',
71
+ default: false,
72
+ });
73
+ },
74
+ handler: async (args) => {
75
+ const cwd = process.cwd();
76
+ const cfg = new Config();
77
+ const configData = await cfg.forBuild({}).catch(() => ({}));
78
+ const flatpak = configData.flatpak ?? {};
79
+ const appId = args.appId ??
80
+ flatpak.appId ??
81
+ readPackageJson(cwd).name;
82
+ if (!appId) {
83
+ throw new Error('[gjsify flatpak sync-flathub] no app id available — pass --app-id or set gjsify.flatpak.appId.');
84
+ }
85
+ const flathubRepo = args.flathubRepo ??
86
+ flatpak.flathubRepo ??
87
+ `flathub/${appId}`;
88
+ const version = args.version ?? (await resolveLatestTag(cwd, args.verbose));
89
+ if (!version) {
90
+ throw new Error('[gjsify flatpak sync-flathub] no version resolved — pass --version vX.Y.Z or create a git tag locally.');
91
+ }
92
+ const commitSha = args.commit ?? (await resolveCommitForTag(cwd, version, args.verbose));
93
+ const branch = args.branch ?? `update-to-${normaliseBranchSegment(version)}`;
94
+ console.log(`[gjsify flatpak sync-flathub] appId=${appId}`);
95
+ console.log(`[gjsify flatpak sync-flathub] flathubRepo=${flathubRepo}`);
96
+ console.log(`[gjsify flatpak sync-flathub] version=${version}`);
97
+ console.log(`[gjsify flatpak sync-flathub] commit=${commitSha}`);
98
+ console.log(`[gjsify flatpak sync-flathub] branch=${branch}`);
99
+ if (args.dryRun) {
100
+ console.log(`[gjsify flatpak sync-flathub] --dry-run set; not cloning / writing / pushing.`);
101
+ return;
102
+ }
103
+ const cacheRoot = flathubCacheRoot();
104
+ const cloneDir = join(cacheRoot, flathubRepo.replace('/', '__'));
105
+ mkdirSync(cacheRoot, { recursive: true });
106
+ await ensureClone(cloneDir, flathubRepo, args.verbose);
107
+ const manifestPath = join(cloneDir, `${appId}.json`);
108
+ if (!existsSync(manifestPath)) {
109
+ throw new Error(`[gjsify flatpak sync-flathub] Flathub manifest not found at ${manifestPath} — wrong appId / wrong flathub-repo?`);
110
+ }
111
+ const original = readFileSync(manifestPath, 'utf-8');
112
+ const updated = editManifest(original, {
113
+ tag: version,
114
+ commit: commitSha,
115
+ sourceIndex: args.sourceIndex,
116
+ });
117
+ if (updated === original) {
118
+ console.log(`[gjsify flatpak sync-flathub] manifest already at ${version} — nothing to do.`);
119
+ return;
120
+ }
121
+ writeFileSync(manifestPath, updated, 'utf-8');
122
+ console.log(`[gjsify flatpak sync-flathub] manifest patched at ${manifestPath}`);
123
+ await gitInRepo(cloneDir, ['checkout', '-B', branch], args.verbose);
124
+ await gitInRepo(cloneDir, ['add', '.'], args.verbose);
125
+ await gitInRepo(cloneDir, ['commit', '-m', `Update to ${version}`], args.verbose);
126
+ if (args.pr === false) {
127
+ console.log(`[gjsify flatpak sync-flathub] --no-pr set; branch ${branch} committed locally in ${cloneDir}.`);
128
+ return;
129
+ }
130
+ await gitInRepo(cloneDir, ['push', '-u', 'origin', branch, '--force-with-lease'], args.verbose);
131
+ const prBody = `Auto-generated by \`gjsify flatpak sync-flathub\`.\n\n- Version: \`${version}\`\n- Commit: \`${commitSha}\`\n`;
132
+ await ghCreate(cloneDir, flathubRepo, branch, version, prBody, args.verbose);
133
+ },
134
+ };
135
+ // ─── Internal helpers ────────────────────────────────────────────────────
136
+ async function resolveLatestTag(cwd, verbose) {
137
+ try {
138
+ const { stdout } = await execFileAsync('git', ['describe', '--tags', '--abbrev=0'], { cwd });
139
+ if (verbose)
140
+ console.log(`[gjsify flatpak sync-flathub] resolved latest tag → ${stdout.trim()}`);
141
+ return stdout.trim() || null;
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ }
147
+ async function resolveCommitForTag(cwd, tag, verbose) {
148
+ try {
149
+ const { stdout } = await execFileAsync('git', ['rev-list', '-n', '1', tag], { cwd });
150
+ const sha = stdout.trim();
151
+ if (!sha)
152
+ throw new Error(`empty rev-list output`);
153
+ if (verbose)
154
+ console.log(`[gjsify flatpak sync-flathub] resolved ${tag} → ${sha}`);
155
+ return sha;
156
+ }
157
+ catch (err) {
158
+ throw new Error(`[gjsify flatpak sync-flathub] tag ${tag} not found locally. Run \`git fetch --tags\` or pass --commit <sha>.\n` +
159
+ ` underlying error: ${err?.message ?? err}`);
160
+ }
161
+ }
162
+ function normaliseBranchSegment(version) {
163
+ return version.replace(/^v/, '').replace(/[^A-Za-z0-9._-]/g, '-');
164
+ }
165
+ function flathubCacheRoot() {
166
+ const base = process.env.XDG_CACHE_HOME || join(homedir(), '.cache');
167
+ return join(base, 'gjsify', 'flathub-sync');
168
+ }
169
+ async function ensureClone(cloneDir, flathubRepo, verbose) {
170
+ if (existsSync(join(cloneDir, '.git'))) {
171
+ if (verbose)
172
+ console.log(`[gjsify flatpak sync-flathub] reusing clone at ${cloneDir}`);
173
+ // Hard-reset to remote HEAD to clear stale state from a previous run
174
+ await gitInRepo(cloneDir, ['fetch', 'origin'], verbose);
175
+ const defaultBranch = await detectDefaultBranch(cloneDir, verbose);
176
+ await gitInRepo(cloneDir, ['checkout', defaultBranch], verbose);
177
+ await gitInRepo(cloneDir, ['reset', '--hard', `origin/${defaultBranch}`], verbose);
178
+ return;
179
+ }
180
+ mkdirSync(resolve(cloneDir, '..'), { recursive: true });
181
+ const url = `https://github.com/${flathubRepo}.git`;
182
+ if (verbose)
183
+ console.log(`[gjsify flatpak sync-flathub] git clone ${url} ${cloneDir}`);
184
+ try {
185
+ await execFileAsync('git', ['clone', url, cloneDir]);
186
+ }
187
+ catch (err) {
188
+ if (err?.code === 'ENOENT') {
189
+ throw new Error('[gjsify flatpak sync-flathub] `git` not found. Install git from your distro (Fedora: `dnf install git`, Debian: `apt install git`).');
190
+ }
191
+ throw err;
192
+ }
193
+ }
194
+ async function detectDefaultBranch(cloneDir, verbose) {
195
+ // Most flathub repos use `master`; some newer ones use `main`. Probe
196
+ // via `git remote show` which surfaces the HEAD branch.
197
+ try {
198
+ const { stdout } = await execFileAsync('git', ['remote', 'show', 'origin'], { cwd: cloneDir });
199
+ const m = stdout.match(/HEAD branch: (\S+)/);
200
+ if (m && m[1] && m[1] !== '(unknown)') {
201
+ if (verbose)
202
+ console.log(`[gjsify flatpak sync-flathub] default branch → ${m[1]}`);
203
+ return m[1];
204
+ }
205
+ }
206
+ catch {
207
+ // fall through to master fallback
208
+ }
209
+ return 'master';
210
+ }
211
+ function gitInRepo(cwd, args, verbose) {
212
+ if (verbose)
213
+ console.log(`[gjsify flatpak sync-flathub] git ${args.join(' ')} (in ${cwd})`);
214
+ return new Promise((res, rej) => {
215
+ const child = spawn('git', args, { cwd, stdio: 'inherit' });
216
+ child.on('error', (err) => {
217
+ if (err.code === 'ENOENT') {
218
+ rej(new Error('[gjsify flatpak sync-flathub] `git` not found. Install it from your distro.'));
219
+ }
220
+ else {
221
+ rej(err);
222
+ }
223
+ });
224
+ child.on('exit', (code) => {
225
+ if (code === 0)
226
+ res();
227
+ else
228
+ rej(new Error(`git ${args[0]} exited ${code}`));
229
+ });
230
+ });
231
+ }
232
+ function ghCreate(cloneDir, flathubRepo, branch, version, body, verbose) {
233
+ const args = [
234
+ 'pr',
235
+ 'create',
236
+ '--repo',
237
+ flathubRepo,
238
+ '--head',
239
+ branch,
240
+ '--title',
241
+ `Update to ${version}`,
242
+ '--body',
243
+ body,
244
+ ];
245
+ if (verbose)
246
+ console.log(`[gjsify flatpak sync-flathub] gh ${args.join(' ')} (in ${cloneDir})`);
247
+ return new Promise((res, rej) => {
248
+ const child = spawn('gh', args, { cwd: cloneDir, stdio: 'inherit' });
249
+ child.on('error', (err) => {
250
+ if (err.code === 'ENOENT') {
251
+ rej(new Error('[gjsify flatpak sync-flathub] `gh` (GitHub CLI) not found. Install via `dnf install gh` (Fedora), `apt install gh` (Debian/Ubuntu), or `flatpak install -y flathub com.github.cli`.'));
252
+ }
253
+ else {
254
+ rej(err);
255
+ }
256
+ });
257
+ child.on('exit', (code) => {
258
+ if (code === 0)
259
+ res();
260
+ else
261
+ rej(new Error(`gh pr create exited ${code}`));
262
+ });
263
+ });
264
+ }
265
+ /**
266
+ * Surgically edit a Flathub manifest to update the git source's `tag` +
267
+ * `commit` (and inject `x-checker-data` if missing). Preserves the
268
+ * original indent + whitespace + key ordering of the surrounding JSON
269
+ * by parsing through JSON.parse + re-stringifying with the detected
270
+ * indent.
271
+ */
272
+ export function editManifest(original, args) {
273
+ const manifest = JSON.parse(original);
274
+ const modules = manifest.modules ?? [];
275
+ if (modules.length === 0) {
276
+ throw new Error('[gjsify flatpak sync-flathub] manifest has no modules');
277
+ }
278
+ const mainModule = modules[0];
279
+ const sources = mainModule.sources ?? [];
280
+ if (sources.length === 0) {
281
+ throw new Error('[gjsify flatpak sync-flathub] modules[0] has no sources');
282
+ }
283
+ let idx = args.sourceIndex ?? sources.findIndex((s) => s.type === 'git');
284
+ if (idx < 0 || idx >= sources.length) {
285
+ throw new Error(`[gjsify flatpak sync-flathub] no git source found in modules[0].sources (use --source-index <n>)`);
286
+ }
287
+ const source = sources[idx];
288
+ if (source.type !== 'git') {
289
+ throw new Error(`[gjsify flatpak sync-flathub] modules[0].sources[${idx}].type is "${source.type}", expected "git"`);
290
+ }
291
+ source.tag = args.tag;
292
+ source.commit = args.commit;
293
+ if (!source['x-checker-data']) {
294
+ source['x-checker-data'] = {
295
+ type: 'git',
296
+ 'tag-pattern': '^v(\\d+\\.\\d+\\.\\d+)$',
297
+ 'version-scheme': 'semantic',
298
+ };
299
+ }
300
+ // Detect the original indent (2 vs 4 spaces) by inspecting the second
301
+ // line — Flathub manifests are all 2-space in practice, but some
302
+ // older ones might be 4. Preserve original convention.
303
+ const indent = detectIndent(original);
304
+ return JSON.stringify(manifest, null, indent) + (original.endsWith('\n') ? '\n' : '');
305
+ }
306
+ function detectIndent(json) {
307
+ const match = json.match(/^\{\n( +)/);
308
+ if (match)
309
+ return match[1].length;
310
+ return 2;
311
+ }
@@ -0,0 +1,12 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface FormatOptions {
3
+ paths?: string[];
4
+ write?: boolean;
5
+ check?: boolean;
6
+ configPath?: string;
7
+ init?: boolean;
8
+ force?: boolean;
9
+ verbose?: boolean;
10
+ }
11
+ export declare const formatCommand: Command<unknown, FormatOptions>;
12
+ export {};