@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.
- package/dist/cli.gjs.mjs +86 -78
- package/lib/actions/barrels-generate.d.ts +31 -0
- package/lib/actions/barrels-generate.js +78 -0
- package/lib/actions/build.d.ts +11 -1
- package/lib/actions/build.js +79 -3
- package/lib/bundler-pick.d.ts +7 -0
- package/lib/bundler-pick.js +17 -0
- package/lib/commands/barrels.d.ts +15 -0
- package/lib/commands/barrels.js +103 -0
- package/lib/commands/build.js +8 -0
- package/lib/commands/fix.d.ts +9 -0
- package/lib/commands/fix.js +60 -0
- package/lib/commands/flatpak/diff.d.ts +12 -0
- package/lib/commands/flatpak/diff.js +165 -0
- package/lib/commands/flatpak/index.d.ts +4 -1
- package/lib/commands/flatpak/index.js +11 -5
- package/lib/commands/flatpak/init.d.ts +1 -0
- package/lib/commands/flatpak/init.js +39 -5
- package/lib/commands/flatpak/release.d.ts +13 -0
- package/lib/commands/flatpak/release.js +152 -0
- package/lib/commands/flatpak/sync-flathub.d.ts +26 -0
- package/lib/commands/flatpak/sync-flathub.js +311 -0
- package/lib/commands/format.d.ts +12 -0
- package/lib/commands/format.js +98 -0
- package/lib/commands/index.d.ts +6 -0
- package/lib/commands/index.js +6 -0
- package/lib/commands/install.js +11 -9
- package/lib/commands/lint.d.ts +9 -0
- package/lib/commands/lint.js +60 -0
- package/lib/commands/test.d.ts +12 -0
- package/lib/commands/test.js +206 -0
- package/lib/commands/upgrade.d.ts +13 -0
- package/lib/commands/upgrade.js +402 -0
- package/lib/index.js +7 -1
- package/lib/templates/biome.json.tmpl +79 -0
- package/lib/types/cli-build-options.d.ts +7 -0
- package/lib/types/config-data.d.ts +39 -0
- package/lib/utils/biome-resolve.d.ts +47 -0
- package/lib/utils/biome-resolve.js +204 -0
- package/package.json +16 -16
- 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
|
|
4
|
-
// `Command<>` so it composes the
|
|
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, };
|
|
@@ -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,
|
|
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 {};
|