@gjsify/cli 0.4.10 → 0.4.12

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.
@@ -0,0 +1,11 @@
1
+ import type { Command } from '../../types/index.js';
2
+ interface FlatpakCheckOptions {
3
+ manifest?: string;
4
+ repo?: string;
5
+ metainfo?: string;
6
+ appstream?: boolean;
7
+ builderLint?: boolean;
8
+ verbose?: boolean;
9
+ }
10
+ export declare const flatpakCheckCommand: Command<unknown, FlatpakCheckOptions>;
11
+ export {};
@@ -0,0 +1,163 @@
1
+ // `gjsify flatpak check` — run Flathub's pre-submission linters locally.
2
+ //
3
+ // Wraps two tools:
4
+ // - `appstreamcli validate --strict <metainfo>` — MetaInfo XML
5
+ // correctness check
6
+ // - `flatpak-builder-lint manifest <manifest.json>` — manifest
7
+ // correctness check (and optionally `repo` for built artefacts)
8
+ //
9
+ // Both ship inside the `org.flatpak.Builder` flatpak. We exec them from
10
+ // the host PATH; if missing, print an install hint pointing at
11
+ // `flatpak install -y flathub org.flatpak.Builder`.
12
+ import { existsSync, readdirSync } from 'node:fs';
13
+ import { resolve, join } from 'node:path';
14
+ import { spawn } from 'node:child_process';
15
+ import { readPackageJson, looksLikeAppId } from './utils.js';
16
+ import { Config } from '../../config.js';
17
+ export const flatpakCheckCommand = {
18
+ command: 'check [manifest]',
19
+ description: 'Run Flathub pre-submission linters: appstreamcli validate + flatpak-builder-lint.',
20
+ builder: (yargs) => {
21
+ return yargs
22
+ .positional('manifest', {
23
+ description: 'Path to the Flatpak manifest. Auto-detects `<app-id>.json` if omitted.',
24
+ type: 'string',
25
+ normalize: true,
26
+ })
27
+ .option('repo', {
28
+ description: 'Built ostree-repo path. If given, also runs `flatpak-builder-lint repo`.',
29
+ type: 'string',
30
+ normalize: true,
31
+ })
32
+ .option('metainfo', {
33
+ description: 'MetaInfo XML path. Default: `data/<app-id>.metainfo.xml.in`.',
34
+ type: 'string',
35
+ normalize: true,
36
+ })
37
+ .option('appstream', {
38
+ description: 'Run `appstreamcli validate --strict` (default: true).',
39
+ type: 'boolean',
40
+ default: true,
41
+ })
42
+ .option('builder-lint', {
43
+ description: 'Run `flatpak-builder-lint manifest` (default: true).',
44
+ type: 'boolean',
45
+ default: true,
46
+ })
47
+ .option('verbose', {
48
+ description: 'Stream linter stdout/stderr verbatim.',
49
+ type: 'boolean',
50
+ default: false,
51
+ });
52
+ },
53
+ handler: async (args) => {
54
+ const cfg = new Config();
55
+ const configData = await cfg.forBuild({}).catch(() => ({}));
56
+ const flatpak = configData.flatpak ?? {};
57
+ const cwd = process.cwd();
58
+ const appId = resolveAppId(args.manifest, flatpak, cwd);
59
+ const manifestPath = resolveManifestPath(args.manifest, appId, cwd);
60
+ const metainfoPath = args.metainfo ?? `data/${appId ?? 'unknown'}.metainfo.xml.in`;
61
+ const metainfoAbs = resolve(cwd, metainfoPath);
62
+ let failures = 0;
63
+ if (args.appstream !== false) {
64
+ if (!existsSync(metainfoAbs)) {
65
+ console.warn(`[gjsify flatpak check] skipping appstreamcli — ${metainfoAbs} not found`);
66
+ }
67
+ else {
68
+ const ok = await runLinter('appstreamcli', ['validate', '--strict', metainfoAbs], args.verbose ?? false);
69
+ if (!ok)
70
+ failures++;
71
+ }
72
+ }
73
+ if (args.builderLint !== false) {
74
+ if (!existsSync(manifestPath)) {
75
+ console.error(`[gjsify flatpak check] manifest not found: ${manifestPath}`);
76
+ process.exit(1);
77
+ return;
78
+ }
79
+ const ok = await runLinter('flatpak-builder-lint', ['manifest', manifestPath], args.verbose ?? false);
80
+ if (!ok)
81
+ failures++;
82
+ if (args.repo) {
83
+ const repoPath = resolve(cwd, args.repo);
84
+ const okRepo = await runLinter('flatpak-builder-lint', ['repo', repoPath], args.verbose ?? false);
85
+ if (!okRepo)
86
+ failures++;
87
+ }
88
+ }
89
+ if (failures > 0) {
90
+ console.error(`\n[gjsify flatpak check] ${failures} check(s) failed.`);
91
+ process.exit(1);
92
+ }
93
+ console.log('[gjsify flatpak check] all checks passed.');
94
+ },
95
+ };
96
+ function resolveAppId(explicit, flatpak, cwd) {
97
+ if (flatpak.appId)
98
+ return flatpak.appId;
99
+ try {
100
+ const pkg = readPackageJson(cwd);
101
+ if (looksLikeAppId(pkg.name))
102
+ return pkg.name;
103
+ }
104
+ catch { /* no pkg.json */ }
105
+ if (explicit) {
106
+ // strip `.json` extension if present
107
+ return explicit.replace(/\.json$/, '');
108
+ }
109
+ return undefined;
110
+ }
111
+ function resolveManifestPath(explicit, appId, cwd) {
112
+ if (explicit)
113
+ return resolve(cwd, explicit);
114
+ if (appId)
115
+ return resolve(cwd, `${appId}.json`);
116
+ // Last resort: find a single *.json that looks like a manifest
117
+ const candidates = readdirSync(cwd).filter((f) => f.endsWith('.json') && !f.startsWith('package'));
118
+ if (candidates.length === 1)
119
+ return resolve(cwd, candidates[0]);
120
+ throw new Error('gjsify flatpak check: no manifest path given and could not auto-detect. ' +
121
+ 'Pass the manifest as a positional argument.');
122
+ }
123
+ function runLinter(bin, args, verbose) {
124
+ return new Promise((resolveP) => {
125
+ const child = spawn(bin, args, {
126
+ stdio: verbose ? 'inherit' : ['ignore', 'pipe', 'pipe'],
127
+ });
128
+ let stdout = '';
129
+ let stderr = '';
130
+ if (!verbose) {
131
+ child.stdout?.setEncoding('utf-8');
132
+ child.stderr?.setEncoding('utf-8');
133
+ child.stdout?.on('data', (c) => { stdout += c; });
134
+ child.stderr?.on('data', (c) => { stderr += c; });
135
+ }
136
+ child.on('error', (err) => {
137
+ const e = err;
138
+ if (e.code === 'ENOENT') {
139
+ console.error(`[gjsify flatpak check] ${bin} not found in PATH.\n` +
140
+ `Install via:\n` +
141
+ ` flatpak install -y flathub org.flatpak.Builder\n` +
142
+ ` alias ${bin}="flatpak run --command=${bin} org.flatpak.Builder"`);
143
+ }
144
+ else {
145
+ console.error(`[gjsify flatpak check] ${bin} failed to spawn: ${e.message}`);
146
+ }
147
+ resolveP(false);
148
+ });
149
+ child.on('exit', (code) => {
150
+ const ok = code === 0;
151
+ const tag = ok ? 'OK' : 'FAIL';
152
+ console.log(`[gjsify flatpak check] ${tag}: ${bin} ${args.join(' ')}`);
153
+ if (!ok && !verbose) {
154
+ if (stdout.trim())
155
+ console.log(stdout.trimEnd());
156
+ if (stderr.trim())
157
+ console.error(stderr.trimEnd());
158
+ }
159
+ resolveP(ok);
160
+ });
161
+ });
162
+ }
163
+ void join;
@@ -3,5 +3,6 @@ import { flatpakInitCommand } from './init.js';
3
3
  import { flatpakBuildCommand } from './build.js';
4
4
  import { flatpakDepsCommand } from './deps.js';
5
5
  import { flatpakCiCommand } from './ci.js';
6
+ import { flatpakCheckCommand } from './check.js';
6
7
  export declare const flatpakCommand: Command;
7
- export { flatpakInitCommand, flatpakBuildCommand, flatpakDepsCommand, flatpakCiCommand, };
8
+ export { flatpakInitCommand, flatpakBuildCommand, flatpakDepsCommand, flatpakCiCommand, flatpakCheckCommand, };
@@ -1,23 +1,25 @@
1
1
  // `gjsify flatpak` — yargs subcommand-group dispatcher.
2
2
  //
3
- // Wires {init, build, deps, ci}. Each subcommand is a self-contained
3
+ // Wires {init, build, deps, ci, check}. Each subcommand is a self-contained
4
4
  // `Command<>` so it composes the same way as `gresource` / `gettext` /
5
5
  // `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
+ import { flatpakCheckCommand } from './check.js';
10
11
  export const flatpakCommand = {
11
12
  command: 'flatpak <subcommand>',
12
- description: 'Flatpak toolchain: init/build/deps/ci subcommands for shipping GJS apps and CLIs as Flatpaks.',
13
+ description: 'Flatpak toolchain: init/build/deps/ci/check subcommands for shipping GJS apps and CLIs as Flatpaks.',
13
14
  builder: (yargs) => {
14
15
  return yargs
15
16
  .command(flatpakInitCommand.command, flatpakInitCommand.description, flatpakInitCommand.builder, flatpakInitCommand.handler)
16
17
  .command(flatpakBuildCommand.command, flatpakBuildCommand.description, flatpakBuildCommand.builder, flatpakBuildCommand.handler)
17
18
  .command(flatpakDepsCommand.command, flatpakDepsCommand.description, flatpakDepsCommand.builder, flatpakDepsCommand.handler)
18
19
  .command(flatpakCiCommand.command, flatpakCiCommand.description, flatpakCiCommand.builder, flatpakCiCommand.handler)
20
+ .command(flatpakCheckCommand.command, flatpakCheckCommand.description, flatpakCheckCommand.builder, flatpakCheckCommand.handler)
19
21
  .demandCommand(1)
20
22
  .strict();
21
23
  },
22
24
  };
23
- export { flatpakInitCommand, flatpakBuildCommand, flatpakDepsCommand, flatpakCiCommand, };
25
+ export { flatpakInitCommand, flatpakBuildCommand, flatpakDepsCommand, flatpakCiCommand, flatpakCheckCommand, };
@@ -3,8 +3,12 @@ interface FlatpakInitOptions {
3
3
  appId?: string;
4
4
  runtime?: string;
5
5
  runtimeVersion?: string;
6
+ kind?: string;
6
7
  cliOnly?: boolean;
7
8
  manifest?: string;
9
+ metainfo?: string;
10
+ desktop?: string;
11
+ flathubJson?: string;
8
12
  command?: string;
9
13
  force?: boolean;
10
14
  sdkExtension?: string[];
@@ -1,19 +1,24 @@
1
1
  // `gjsify flatpak init` — generate a Flatpak manifest from package.json
2
- // + the `gjsify.flatpak` config namespace.
2
+ // + the `gjsify.flatpak` config namespace, plus (Phase F.9) MetaInfo XML,
3
+ // `.desktop` (app kind only), and `flathub.json` policy stub in the same
4
+ // invocation.
3
5
  //
4
6
  // Defaults are designed for the two real-world shapes:
5
- // * GTK4 + Adwaita apps (Learn6502): `gnome` runtime, GUI finish-args
6
- // * Headless CLI tools (ts-for-gir): same `gnome` runtime (GJS bundles
7
- // need GLib/GIO at runtime — Freedesktop ships no GJS), but lean
8
- // finish-args via `--cli-only`. Memory file
9
- // `project_flatpak_runtime_choice.md` documents this trade-off.
10
- import { existsSync, writeFileSync } from 'node:fs';
11
- import { resolve } from 'node:path';
7
+ // * `--kind app` (default) — GTK4 + Adwaita apps (Learn6502): `gnome`
8
+ // runtime, GUI finish-args, desktop-application MetaInfo, .desktop +
9
+ // icon required.
10
+ // * `--kind cli` — headless CLI tools (ts-for-gir): same `gnome` runtime
11
+ // (GJS bundles need GLib/GIO at runtime — Freedesktop ships no GJS),
12
+ // but lean finish-args + console-application MetaInfo + flathub.json
13
+ // with `skip-icons-check`.
14
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
15
+ import { dirname, resolve } from 'node:path';
12
16
  import { DEFAULT_CLI_FINISH_ARGS, DEFAULT_GUI_FINISH_ARGS, looksLikeAppId, readPackageJson, resolveRuntime, } from './utils.js';
17
+ import { renderDesktop, renderFlathubJson, renderMetainfoApp, renderMetainfoCli, validateScaffoldInputs, } from './scaffold.js';
13
18
  import { Config } from '../../config.js';
14
19
  export const flatpakInitCommand = {
15
20
  command: 'init',
16
- description: 'Generate a Flatpak manifest from package.json + `gjsify.flatpak` config.',
21
+ description: 'Generate Flatpak manifest + MetaInfo XML + .desktop + flathub.json from `gjsify.flatpak` config.',
17
22
  builder: (yargs) => {
18
23
  return yargs
19
24
  .option('app-id', {
@@ -27,14 +32,33 @@ export const flatpakInitCommand = {
27
32
  .option('runtime-version', {
28
33
  description: 'Runtime version (default: gnome -> 50, freedesktop -> 24.08)',
29
34
  type: 'string',
35
+ })
36
+ .option('kind', {
37
+ description: 'App kind: "app" (default, desktop) or "cli" (console-application MetaInfo, no .desktop)',
38
+ choices: ['app', 'cli'],
30
39
  })
31
40
  .option('cli-only', {
32
- description: 'Strip GUI finish-args; keep `gnome` runtime so GJS is available at runtime',
41
+ description: '(Deprecated) Alias for `--kind cli`. Use --kind instead.',
33
42
  type: 'boolean',
34
43
  default: false,
35
44
  })
36
45
  .option('manifest', {
37
- description: 'Output path. Default: `<app-id>.json` in cwd.',
46
+ description: 'Output path for the manifest. Default: `<app-id>.json` in cwd.',
47
+ type: 'string',
48
+ normalize: true,
49
+ })
50
+ .option('metainfo', {
51
+ description: 'Output path for the MetaInfo XML. Default: `data/<app-id>.metainfo.xml.in` in cwd.',
52
+ type: 'string',
53
+ normalize: true,
54
+ })
55
+ .option('desktop', {
56
+ description: 'Output path for the .desktop entry (app kind only). Default: `data/<app-id>.desktop.in`.',
57
+ type: 'string',
58
+ normalize: true,
59
+ })
60
+ .option('flathub-json', {
61
+ description: 'Output path for the flathub.json policy stub. Default: `flathub.json` in cwd.',
38
62
  type: 'string',
39
63
  normalize: true,
40
64
  })
@@ -53,7 +77,7 @@ export const flatpakInitCommand = {
53
77
  array: true,
54
78
  })
55
79
  .option('force', {
56
- description: 'Overwrite an existing manifest',
80
+ description: 'Overwrite existing output files (manifest, metainfo, desktop, flathub.json)',
57
81
  type: 'boolean',
58
82
  default: false,
59
83
  })
@@ -76,6 +100,9 @@ export const flatpakInitCommand = {
76
100
  throw new Error('gjsify flatpak init: no app id available. Pass --app-id, set gjsify.flatpak.appId in package.json, ' +
77
101
  'or rename the package to a reverse-DNS id like org.example.MyApp.');
78
102
  }
103
+ const kind = args.kind ??
104
+ flatpak.kind ??
105
+ (args.cliOnly ? 'cli' : 'app');
79
106
  const { runtime, runtimeId, sdk, runtimeVersion } = resolveRuntime(flatpak, {
80
107
  runtime: args.runtime,
81
108
  runtimeVersion: args.runtimeVersion,
@@ -84,11 +111,10 @@ export const flatpakInitCommand = {
84
111
  const appendPath = flatpak.appendPath ?? (sdkExtensions?.length ? deriveAppendPath(sdkExtensions) : undefined);
85
112
  const command = args.command ?? flatpak.command ?? appId;
86
113
  const explicitFinishArgs = args.finishArg;
87
- const cliOnly = args.cliOnly === true;
88
114
  const finishArgs = explicitFinishArgs !== undefined
89
115
  ? explicitFinishArgs
90
116
  : flatpak.finishArgs ??
91
- (cliOnly ? DEFAULT_CLI_FINISH_ARGS : DEFAULT_GUI_FINISH_ARGS);
117
+ (kind === 'cli' ? DEFAULT_CLI_FINISH_ARGS : DEFAULT_GUI_FINISH_ARGS);
92
118
  const manifest = {
93
119
  id: appId,
94
120
  runtime: runtimeId,
@@ -105,8 +131,6 @@ export const flatpakInitCommand = {
105
131
  const cleanup = flatpak.cleanup;
106
132
  if (cleanup?.length)
107
133
  manifest.cleanup = cleanup;
108
- // Modules: caller-supplied `extraModules` first, then the app's own
109
- // meson module pointing at the source dir.
110
134
  const modules = [];
111
135
  if (flatpak.extraModules?.length)
112
136
  modules.push(...flatpak.extraModules);
@@ -116,27 +140,71 @@ export const flatpakInitCommand = {
116
140
  sources: [{ type: 'dir', path: '.' }],
117
141
  });
118
142
  manifest.modules = modules;
119
- const out = args.manifest ?? `${appId}.json`;
120
- const outPath = resolve(cwd, out);
121
- if (existsSync(outPath) && !args.force) {
122
- throw new Error(`gjsify flatpak init: ${outPath} exists. Pass --force to overwrite.`);
143
+ const manifestOut = args.manifest ?? `${appId}.json`;
144
+ const manifestPath = resolve(cwd, manifestOut);
145
+ writeIfFresh(manifestPath, JSON.stringify(manifest, null, 4) + '\n', args.force ?? false, 'manifest');
146
+ const name = pkg.name ?? appId;
147
+ const scaffold = {
148
+ appId,
149
+ name: friendlyName(name, appId),
150
+ command,
151
+ kind,
152
+ flatpak,
153
+ };
154
+ const missing = validateScaffoldInputs(scaffold);
155
+ if (missing.length > 0) {
156
+ console.warn('[gjsify flatpak init] Manifest written, but MetaInfo / .desktop are skipped — config gaps:');
157
+ for (const m of missing)
158
+ console.warn(` - ${m.field}: ${m.hint}`);
159
+ console.warn('\nFill these fields in package.json#gjsify.flatpak (or .gjsifyrc.*) and re-run with --force.');
160
+ }
161
+ else {
162
+ const metainfoXml = kind === 'cli' ? renderMetainfoCli(scaffold) : renderMetainfoApp(scaffold);
163
+ const metainfoOut = args.metainfo ?? `data/${appId}.metainfo.xml.in`;
164
+ writeIfFresh(resolve(cwd, metainfoOut), metainfoXml, args.force ?? false, 'metainfo');
165
+ if (kind === 'app') {
166
+ const desktopOut = args.desktop ?? `data/${appId}.desktop.in`;
167
+ writeIfFresh(resolve(cwd, desktopOut), renderDesktop(scaffold), args.force ?? false, 'desktop');
168
+ if (!flatpak.icon) {
169
+ console.warn(`[gjsify flatpak init] No gjsify.flatpak.icon set. Flathub requires a scalable SVG at\n` +
170
+ ` data/icons/hicolor/scalable/apps/${appId}.svg`);
171
+ }
172
+ }
173
+ const flathubOut = args.flathubJson ?? 'flathub.json';
174
+ writeIfFresh(resolve(cwd, flathubOut), renderFlathubJson(kind), args.force ?? false, 'flathub.json');
123
175
  }
124
- const json = JSON.stringify(manifest, null, 4) + '\n';
125
- writeFileSync(outPath, json, 'utf-8');
126
176
  if (args.verbose) {
127
- console.log(`[gjsify flatpak init] runtime=${runtimeId} ${runtimeVersion} sdk=${sdk}`);
177
+ console.log(`[gjsify flatpak init] kind=${kind} runtime=${runtimeId} ${runtimeVersion} sdk=${sdk}`);
128
178
  console.log(`[gjsify flatpak init] command=${command} finish-args=${JSON.stringify(finishArgs)}`);
179
+ void runtime;
129
180
  }
130
- console.log(`[gjsify flatpak init] wrote ${outPath}`);
131
181
  },
132
182
  };
133
- /** Concatenate two optional arrays, dropping `undefined`. */
183
+ function writeIfFresh(path, content, force, label) {
184
+ if (existsSync(path) && !force) {
185
+ console.log(`[gjsify flatpak init] skipped ${label}: ${path} (exists; --force to overwrite)`);
186
+ return;
187
+ }
188
+ mkdirSync(dirname(path), { recursive: true });
189
+ writeFileSync(path, content, 'utf-8');
190
+ console.log(`[gjsify flatpak init] wrote ${label}: ${path}`);
191
+ }
192
+ function friendlyName(pkgName, appId) {
193
+ if (pkgName.startsWith('@')) {
194
+ const base = pkgName.slice(pkgName.indexOf('/') + 1);
195
+ return base;
196
+ }
197
+ if (pkgName === appId) {
198
+ const segs = appId.split('.');
199
+ return segs[segs.length - 1] ?? appId;
200
+ }
201
+ return pkgName;
202
+ }
134
203
  function mergeArrays(a, b) {
135
204
  if (!a?.length && !b?.length)
136
205
  return undefined;
137
206
  return [...(a ?? []), ...(b ?? [])];
138
207
  }
139
- /** Map known SDK extension ids to their /usr/lib/sdk/<name>/bin paths. */
140
208
  function deriveAppendPath(sdkExtensions) {
141
209
  const out = [];
142
210
  for (const ext of sdkExtensions) {
@@ -147,7 +215,6 @@ function deriveAppendPath(sdkExtensions) {
147
215
  out.push('/app/bin');
148
216
  return out;
149
217
  }
150
- /** Last segment of the reverse-DNS id, used as the meson-module name. */
151
218
  function deriveModuleName(appId) {
152
219
  const parts = appId.split('.');
153
220
  return parts[parts.length - 1] || appId;
@@ -0,0 +1,26 @@
1
+ import type { ConfigDataFlatpak } from '../../types/config-data.js';
2
+ export interface ScaffoldInputs {
3
+ appId: string;
4
+ name: string;
5
+ command: string;
6
+ kind: 'app' | 'cli';
7
+ flatpak: ConfigDataFlatpak;
8
+ }
9
+ export interface MissingFieldError {
10
+ field: string;
11
+ hint: string;
12
+ }
13
+ /**
14
+ * Validate that the config has the minimum set of fields required for
15
+ * MetaInfo XML rendering. Returns the list of missing fields with
16
+ * actionable hints; empty list means OK.
17
+ */
18
+ export declare function validateScaffoldInputs(inputs: ScaffoldInputs): MissingFieldError[];
19
+ /** Render the MetaInfo XML for a desktop application. */
20
+ export declare function renderMetainfoApp(inputs: ScaffoldInputs): string;
21
+ /** Render the MetaInfo XML for a console application. */
22
+ export declare function renderMetainfoCli(inputs: ScaffoldInputs): string;
23
+ /** Render the .desktop entry (app kind only). */
24
+ export declare function renderDesktop(inputs: ScaffoldInputs): string;
25
+ /** Render the flathub.json policy file. */
26
+ export declare function renderFlathubJson(kind: 'app' | 'cli'): string;