@gjsify/cli 0.39.0 → 0.41.0
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 +202 -181
- package/lib/cli-app.js +12 -25
- package/lib/commands/create.d.ts +3 -0
- package/lib/commands/create.js +49 -6
- package/lib/commands/flatpak/check.js +1 -1
- package/lib/commands/flatpak/ci.js +1 -1
- package/lib/commands/flatpak/diff.js +1 -1
- package/lib/commands/flatpak/init.js +1 -1
- package/lib/commands/flatpak/scaffold.d.ts +2 -4
- package/lib/commands/flatpak/scaffold.js +25 -298
- package/lib/commands/flatpak/sync-flathub.js +1 -1
- package/lib/commands/foreach.js +34 -20
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/install.d.ts +1 -0
- package/lib/commands/install.js +32 -1
- package/lib/commands/prune.d.ts +11 -0
- package/lib/commands/prune.js +84 -0
- package/lib/commands/self-update.d.ts +1 -0
- package/lib/commands/self-update.js +55 -0
- package/lib/commands/ship.d.ts +11 -0
- package/lib/commands/ship.js +217 -0
- package/lib/config.d.ts +12 -0
- package/lib/config.js +15 -0
- package/lib/templates/install.mjs.tmpl +8 -3
- package/lib/types/config-data.d.ts +136 -46
- package/lib/utils/app-metadata.d.ts +51 -0
- package/lib/utils/app-metadata.js +338 -0
- package/lib/utils/check-system-deps.d.ts +2 -2
- package/lib/utils/check-system-deps.js +43 -6
- package/lib/utils/cli-runtime-closure.d.ts +23 -0
- package/lib/utils/cli-runtime-closure.js +274 -0
- package/lib/utils/install-provenance.d.ts +44 -0
- package/lib/utils/install-provenance.js +106 -0
- package/lib/utils/prune-prefix.d.ts +83 -0
- package/lib/utils/prune-prefix.js +333 -0
- package/lib/utils/publish-headers.d.ts +13 -0
- package/lib/utils/publish-headers.js +30 -4
- package/lib/utils/ship/ar.d.ts +21 -0
- package/lib/utils/ship/ar.js +57 -0
- package/lib/utils/ship/bytes.d.ts +4 -0
- package/lib/utils/ship/bytes.js +23 -0
- package/lib/utils/ship/cpio.d.ts +22 -0
- package/lib/utils/ship/cpio.js +93 -0
- package/lib/utils/ship/deb.d.ts +24 -0
- package/lib/utils/ship/deb.js +118 -0
- package/lib/utils/ship/depends.d.ts +62 -0
- package/lib/utils/ship/depends.js +221 -0
- package/lib/utils/ship/discover.d.ts +21 -0
- package/lib/utils/ship/discover.js +129 -0
- package/lib/utils/ship/formats.d.ts +5 -0
- package/lib/utils/ship/formats.js +83 -0
- package/lib/utils/ship/gi-namespaces.d.ts +7 -0
- package/lib/utils/ship/gi-namespaces.js +49 -0
- package/lib/utils/ship/gzip.d.ts +2 -0
- package/lib/utils/ship/gzip.js +21 -0
- package/lib/utils/ship/launcher.d.ts +7 -0
- package/lib/utils/ship/launcher.js +37 -0
- package/lib/utils/ship/payload.d.ts +30 -0
- package/lib/utils/ship/payload.js +59 -0
- package/lib/utils/ship/plan.d.ts +34 -0
- package/lib/utils/ship/plan.js +167 -0
- package/lib/utils/ship/rpm-header.d.ts +29 -0
- package/lib/utils/ship/rpm-header.js +170 -0
- package/lib/utils/ship/rpm.d.ts +12 -0
- package/lib/utils/ship/rpm.js +377 -0
- package/lib/utils/ship/scripts.d.ts +31 -0
- package/lib/utils/ship/scripts.js +76 -0
- package/lib/utils/ship/settings.d.ts +60 -0
- package/lib/utils/ship/settings.js +184 -0
- package/lib/utils/ship/stage-writer.d.ts +17 -0
- package/lib/utils/ship/stage-writer.js +49 -0
- package/lib/utils/ship/types.d.ts +114 -0
- package/lib/utils/ship/types.js +10 -0
- package/lib/utils/ship/version.d.ts +17 -0
- package/lib/utils/ship/version.js +55 -0
- package/package.json +27 -18
- /package/lib/templates/{flatpak → app}/desktop.tmpl +0 -0
package/lib/cli-app.js
CHANGED
|
@@ -15,12 +15,10 @@
|
|
|
15
15
|
// This module MUST NOT execute anything at load time (no top-level
|
|
16
16
|
// `parseAsync`) — `run.ts` imports it, and a top-level run would re-dispatch
|
|
17
17
|
// the original argv on import.
|
|
18
|
-
import { readFileSync } from 'node:fs';
|
|
19
|
-
import { dirname, join } from 'node:path';
|
|
20
|
-
import { fileURLToPath } from 'node:url';
|
|
21
18
|
import yargs from 'yargs';
|
|
22
19
|
import { classifyCliFailure } from './cli-fail.js';
|
|
23
|
-
import {
|
|
20
|
+
import { cliVersion } from './utils/publish-headers.js';
|
|
21
|
+
import { buildCommand as build, clearCommand as clear, copyCommand as copy, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
|
|
24
22
|
import { APP_NAME } from './constants.js';
|
|
25
23
|
import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
26
24
|
import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
|
|
@@ -49,22 +47,6 @@ function runtimeLabel() {
|
|
|
49
47
|
}
|
|
50
48
|
return 'unknown runtime';
|
|
51
49
|
}
|
|
52
|
-
// Read the version from package.json adjacent to the bundle. yargs's
|
|
53
|
-
// auto-version-discovery (its `pkg-up`-driven default) doesn't reach
|
|
54
|
-
// through the bundled `dist/cli.gjs.mjs` path on GJS — falls back to
|
|
55
|
-
// "unknown". Both layouts are covered:
|
|
56
|
-
// - dev (tsx, `yarn workspace`): src/index.ts → ../package.json
|
|
57
|
-
// - bundled (install -g): dist/cli.gjs.mjs → ../package.json
|
|
58
|
-
function readBundleVersion() {
|
|
59
|
-
try {
|
|
60
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
61
|
-
const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
|
|
62
|
-
return typeof pkg.version === 'string' ? pkg.version : 'unknown';
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return 'unknown';
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
50
|
/**
|
|
69
51
|
* Build + run the gjsify yargs command surface for the given argv (already
|
|
70
52
|
* stripped of the `node`/`gjs` + script prefix — i.e. what `hideBin` returns).
|
|
@@ -75,15 +57,18 @@ function readBundleVersion() {
|
|
|
75
57
|
* exit before any async work runs.
|
|
76
58
|
*/
|
|
77
59
|
export async function runCli(argv) {
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
60
|
+
// Make a runnable `gjsify` available on PATH for child processes
|
|
61
|
+
// (workspace/foreach orchestration + compound `gjsify run` scripts). TWO
|
|
62
|
+
// cases, not one — the "No-op on Node" this comment used to claim stopped
|
|
63
|
+
// being true when the bootstrap case landed: under GJS, so node-free builds
|
|
64
|
+
// don't fall back to the Node bin; and a BOOTSTRAP CLI under Node, where the
|
|
65
|
+
// tree's own `node_modules/.bin/gjsify` dangles until `build:infra` — which
|
|
66
|
+
// is what a cold macOS/Windows leg runs — has produced what it dispatches to.
|
|
82
67
|
ensureGjsifyShimOnPath();
|
|
83
68
|
const cli = yargs(argv);
|
|
84
69
|
await cli
|
|
85
70
|
.scriptName(APP_NAME)
|
|
86
|
-
.version(
|
|
71
|
+
.version(cliVersion())
|
|
87
72
|
.strict()
|
|
88
73
|
// Sits beside `.strict()` because that is what produces most of what it
|
|
89
74
|
// classifies. See `cli-fail.ts` for the four yargs measurements this
|
|
@@ -122,6 +107,7 @@ export async function runCli(argv) {
|
|
|
122
107
|
.command(gettext.command, gettext.description, gettext.builder, gettext.handler)
|
|
123
108
|
.command(gsettings.command, gsettings.description, gsettings.builder, gsettings.handler)
|
|
124
109
|
.command(flatpak.command, flatpak.description, flatpak.builder, flatpak.handler)
|
|
110
|
+
.command(ship.command, ship.description, ship.builder, ship.handler)
|
|
125
111
|
.command(foreach.command, foreach.description, foreach.builder, foreach.handler)
|
|
126
112
|
.command(workspace.command, workspace.description, workspace.builder, workspace.handler)
|
|
127
113
|
.command(pack.command, pack.description, pack.builder, pack.handler)
|
|
@@ -134,6 +120,7 @@ export async function runCli(argv) {
|
|
|
134
120
|
.command(selfUpdate.command, selfUpdate.description, selfUpdate.builder, selfUpdate.handler)
|
|
135
121
|
.command(generateInstaller.command, generateInstaller.description, generateInstaller.builder, generateInstaller.handler)
|
|
136
122
|
.command(uninstall.command, uninstall.description, uninstall.builder, uninstall.handler)
|
|
123
|
+
.command(prune.command, prune.description, prune.builder, prune.handler)
|
|
137
124
|
.command(upgrade.command, upgrade.description, upgrade.builder, upgrade.handler)
|
|
138
125
|
.command(format.command, format.description, format.builder, format.handler)
|
|
139
126
|
.command(lint.command, lint.description, lint.builder, lint.handler)
|
package/lib/commands/create.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import type { PackageManager } from '@gjsify/create-app';
|
|
1
2
|
import type { Command } from '../types/index.js';
|
|
2
3
|
interface CreateOptions {
|
|
3
4
|
'project-name': string;
|
|
4
5
|
template?: string;
|
|
6
|
+
runtime?: string;
|
|
7
|
+
'package-manager'?: PackageManager;
|
|
5
8
|
force: boolean;
|
|
6
9
|
install: boolean;
|
|
7
10
|
}
|
package/lib/commands/create.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { createProject, discoverTemplates } from '@gjsify/create-app';
|
|
2
|
-
import { promptTemplate } from '@gjsify/create-app/prompt';
|
|
1
|
+
import { createProject, discoverTemplates, PACKAGE_MANAGERS, supportedRuntimes } from '@gjsify/create-app';
|
|
2
|
+
import { isCancelled, promptSetup, promptTemplate } from '@gjsify/create-app/prompt';
|
|
3
3
|
export const createCommand = {
|
|
4
4
|
command: 'create [project-name]',
|
|
5
5
|
description: 'Scaffold a new Gjsify project in a new directory.',
|
|
6
6
|
builder: (yargs) => {
|
|
7
7
|
const templates = discoverTemplates();
|
|
8
8
|
const templateChoices = templates.map((t) => t.name);
|
|
9
|
+
// DERIVED from the shipped templates, never a hand-written list:
|
|
10
|
+
// accepting `--runtime deno` is a claim that some template runs there,
|
|
11
|
+
// and only the templates can make it.
|
|
12
|
+
const runtimeChoices = [...new Set(templates.flatMap((t) => supportedRuntimes(t)))];
|
|
9
13
|
return yargs
|
|
10
14
|
.positional('project-name', {
|
|
11
15
|
describe: 'Name of the project directory to create',
|
|
@@ -17,6 +21,12 @@ export const createCommand = {
|
|
|
17
21
|
describe: 'Template to scaffold from',
|
|
18
22
|
type: 'string',
|
|
19
23
|
choices: templateChoices.length > 0 ? templateChoices : undefined,
|
|
24
|
+
})
|
|
25
|
+
.option('runtime', {
|
|
26
|
+
alias: 'r',
|
|
27
|
+
describe: 'Runtime to set the project up for. Decides which package managers are offered and which start script the next steps name.',
|
|
28
|
+
type: 'string',
|
|
29
|
+
choices: runtimeChoices.length > 0 ? runtimeChoices : undefined,
|
|
20
30
|
})
|
|
21
31
|
.option('force', {
|
|
22
32
|
alias: 'f',
|
|
@@ -25,30 +35,63 @@ export const createCommand = {
|
|
|
25
35
|
default: false,
|
|
26
36
|
})
|
|
27
37
|
.option('install', {
|
|
28
|
-
describe: '
|
|
38
|
+
describe: 'Install dependencies after scaffolding',
|
|
29
39
|
type: 'boolean',
|
|
30
40
|
default: false,
|
|
41
|
+
})
|
|
42
|
+
.option('package-manager', {
|
|
43
|
+
alias: 'p',
|
|
44
|
+
describe: 'Package manager to install with, and to name in the printed next steps. Must be one the chosen runtime can install for. `gjsify` is the only one that works on a host with no Node.js.',
|
|
45
|
+
type: 'string',
|
|
46
|
+
choices: PACKAGE_MANAGERS,
|
|
31
47
|
});
|
|
32
48
|
},
|
|
33
49
|
handler: async (args) => {
|
|
50
|
+
const interactive = Boolean(process.stdin.isTTY);
|
|
51
|
+
const templates = discoverTemplates();
|
|
34
52
|
let template = args.template;
|
|
35
53
|
if (!template) {
|
|
36
|
-
|
|
37
|
-
if (!process.stdin.isTTY) {
|
|
54
|
+
if (!interactive) {
|
|
38
55
|
const list = templates.map((t) => t.name).join(', ');
|
|
39
56
|
console.error(`Error: --template is required in non-interactive mode. Available templates: ${list || '(none)'}`);
|
|
40
57
|
// `return` — the deferred GJS exit otherwise fell through into
|
|
41
58
|
// the interactive template prompt on a non-TTY.
|
|
42
59
|
return process.exit(1);
|
|
43
60
|
}
|
|
44
|
-
|
|
61
|
+
// Ctrl+C is an abort, not a crash.
|
|
62
|
+
const picked = await promptTemplate(templates).catch((error) => {
|
|
63
|
+
if (!isCancelled(error))
|
|
64
|
+
throw error;
|
|
65
|
+
console.error('Cancelled.');
|
|
66
|
+
return undefined;
|
|
67
|
+
});
|
|
68
|
+
if (picked === undefined)
|
|
69
|
+
return process.exit(1);
|
|
45
70
|
template = picked.name;
|
|
46
71
|
}
|
|
72
|
+
// The SAME decision layer `npm create @gjsify/app` runs, not a second
|
|
73
|
+
// one. Wired late: this command used to pass neither runtime nor package
|
|
74
|
+
// manager, so `createProject` fell back to the host runtime and the first
|
|
75
|
+
// manager for it — meaning `gjsify create --install` on a Node host ran
|
|
76
|
+
// `npm install` without ever saying so, the one default that reaches the
|
|
77
|
+
// user's disk and the one the standalone bin refuses to guess.
|
|
78
|
+
const info = templates.find((t) => t.name === template);
|
|
79
|
+
const setup = await promptSetup({
|
|
80
|
+
offered: info ? supportedRuntimes(info) : [],
|
|
81
|
+
runtime: args.runtime,
|
|
82
|
+
packageManager: args['package-manager'],
|
|
83
|
+
install: args.install,
|
|
84
|
+
interactive,
|
|
85
|
+
});
|
|
86
|
+
if (setup === undefined)
|
|
87
|
+
return process.exit(1);
|
|
47
88
|
await createProject({
|
|
48
89
|
projectName: args['project-name'],
|
|
49
90
|
template,
|
|
91
|
+
runtime: setup.runtime,
|
|
50
92
|
force: args.force,
|
|
51
93
|
install: args.install,
|
|
94
|
+
packageManager: setup.packageManager,
|
|
52
95
|
});
|
|
53
96
|
},
|
|
54
97
|
};
|
|
@@ -52,7 +52,7 @@ export const flatpakCheckCommand = {
|
|
|
52
52
|
},
|
|
53
53
|
handler: async (args) => {
|
|
54
54
|
const cfg = new Config();
|
|
55
|
-
const configData = await cfg.
|
|
55
|
+
const configData = await cfg.forCommand().catch(() => ({}));
|
|
56
56
|
const flatpak = configData.flatpak ?? {};
|
|
57
57
|
const cwd = process.cwd();
|
|
58
58
|
const appId = resolveAppId(args.manifest, flatpak, cwd);
|
|
@@ -58,7 +58,7 @@ export const flatpakCiCommand = {
|
|
|
58
58
|
handler: async (args) => {
|
|
59
59
|
const cwd = process.cwd();
|
|
60
60
|
const cfg = new Config();
|
|
61
|
-
const configData = await cfg.
|
|
61
|
+
const configData = await cfg.forCommand().catch(() => ({}));
|
|
62
62
|
const flatpak = configData.flatpak ?? {};
|
|
63
63
|
const pkg = readPackageJson(cwd);
|
|
64
64
|
const appId = flatpak.appId ?? (looksLikeAppId(pkg.name) ? pkg.name : undefined);
|
|
@@ -60,7 +60,7 @@ export const flatpakDiffCommand = {
|
|
|
60
60
|
handler: async (args) => {
|
|
61
61
|
const cwd = process.cwd();
|
|
62
62
|
const cfg = new Config();
|
|
63
|
-
const configData = await cfg.
|
|
63
|
+
const configData = await cfg.forCommand().catch(() => ({}));
|
|
64
64
|
const flatpak = configData.flatpak ?? {};
|
|
65
65
|
const appId = args.appId ?? flatpak.appId ?? readPackageJson(cwd).name;
|
|
66
66
|
if (!appId) {
|
|
@@ -97,7 +97,7 @@ export const flatpakInitCommand = {
|
|
|
97
97
|
},
|
|
98
98
|
handler: async (args) => {
|
|
99
99
|
const cfg = new Config();
|
|
100
|
-
const configData = await cfg.
|
|
100
|
+
const configData = await cfg.forCommand().catch(() => ({}));
|
|
101
101
|
const flatpak = configData.flatpak ?? {};
|
|
102
102
|
const cwd = process.cwd();
|
|
103
103
|
const pkg = readPackageJson(cwd);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ConfigDataFlatpak } from '../../types/config-data.js';
|
|
2
|
+
import { type MissingFieldError } from '../../utils/app-metadata.js';
|
|
3
|
+
export type { MissingFieldError };
|
|
2
4
|
export interface ScaffoldInputs {
|
|
3
5
|
appId: string;
|
|
4
6
|
name: string;
|
|
@@ -6,10 +8,6 @@ export interface ScaffoldInputs {
|
|
|
6
8
|
kind: 'app' | 'cli';
|
|
7
9
|
flatpak: ConfigDataFlatpak;
|
|
8
10
|
}
|
|
9
|
-
export interface MissingFieldError {
|
|
10
|
-
field: string;
|
|
11
|
-
hint: string;
|
|
12
|
-
}
|
|
13
11
|
/**
|
|
14
12
|
* Validate that the config has the minimum set of fields required for
|
|
15
13
|
* MetaInfo XML rendering. Returns the list of missing fields with
|
|
@@ -1,328 +1,55 @@
|
|
|
1
|
-
//
|
|
2
|
-
// `gjsify
|
|
3
|
-
// directly in TypeScript instead of substituting into a static template —
|
|
4
|
-
// the AppStream surface (description blocks, per-release rich notes,
|
|
5
|
-
// translator hints, kudos, supports/requires/recommends, content_rating
|
|
6
|
-
// attributes, provides) has too many optional nested sections for a
|
|
7
|
-
// template+placeholder approach to stay legible.
|
|
1
|
+
// Flathub-specific scaffolding for `gjsify flatpak init`, plus the thin adapter
|
|
2
|
+
// that hands `gjsify.flatpak`'s metadata half to the shared renderers.
|
|
8
3
|
//
|
|
9
|
-
// `.desktop`
|
|
10
|
-
//
|
|
4
|
+
// The MetaInfo XML and the `.desktop` entry moved to `utils/app-metadata.ts`
|
|
5
|
+
// (ADR 0024 § 8): they describe the APP, not the Flatpak, and `gjsify ship`
|
|
6
|
+
// stages the same two files into `share/metainfo/` and `share/applications/`.
|
|
7
|
+
// What stays here is genuinely Flathub's — `flathub.json`, a policy file no
|
|
8
|
+
// other format has.
|
|
11
9
|
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { renderDesktopEntry, renderMetainfoApp as renderMetainfoAppShared, renderMetainfoCli as renderMetainfoCliShared, validateAppMetadata, } from '../../utils/app-metadata.js';
|
|
12
11
|
/**
|
|
13
|
-
* Lazy template loaders
|
|
14
|
-
*
|
|
15
|
-
* into the GJS bundle at build time.
|
|
12
|
+
* Lazy template loaders. `static-read-inliner` matches this shape and inlines
|
|
13
|
+
* the templates into the GJS bundle at build time.
|
|
16
14
|
*/
|
|
17
|
-
function loadDesktopTemplate() {
|
|
18
|
-
return readFileSync(new URL('../../templates/flatpak/desktop.tmpl', import.meta.url), 'utf-8');
|
|
19
|
-
}
|
|
20
15
|
function loadFlathubAppTemplate() {
|
|
21
16
|
return readFileSync(new URL('../../templates/flatpak/flathub-app.json.tmpl', import.meta.url), 'utf-8');
|
|
22
17
|
}
|
|
23
18
|
function loadFlathubCliTemplate() {
|
|
24
19
|
return readFileSync(new URL('../../templates/flatpak/flathub-cli.json.tmpl', import.meta.url), 'utf-8');
|
|
25
20
|
}
|
|
21
|
+
/** `gjsify.flatpak` IS an `AppMetadata` plus Flatpak's own keys. */
|
|
22
|
+
function toMetadataInputs(inputs) {
|
|
23
|
+
return {
|
|
24
|
+
appId: inputs.appId,
|
|
25
|
+
name: inputs.name,
|
|
26
|
+
command: inputs.command,
|
|
27
|
+
kind: inputs.kind,
|
|
28
|
+
metadata: inputs.flatpak,
|
|
29
|
+
configKey: 'gjsify.flatpak',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
26
32
|
/**
|
|
27
33
|
* Validate that the config has the minimum set of fields required for
|
|
28
34
|
* MetaInfo XML rendering. Returns the list of missing fields with
|
|
29
35
|
* actionable hints; empty list means OK.
|
|
30
36
|
*/
|
|
31
37
|
export function validateScaffoldInputs(inputs) {
|
|
32
|
-
|
|
33
|
-
const missing = [];
|
|
34
|
-
if (!f.developer?.id || !f.developer?.name) {
|
|
35
|
-
missing.push({
|
|
36
|
-
field: 'gjsify.flatpak.developer',
|
|
37
|
-
hint: 'Set `gjsify.flatpak.developer = { "id": "io.github.you", "name": "Your Name" }` in package.json. The id is reverse-DNS.',
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
if (!f.summary) {
|
|
41
|
-
missing.push({
|
|
42
|
-
field: 'gjsify.flatpak.summary',
|
|
43
|
-
hint: 'One-line app summary, ≤80 chars, no trailing period. Example: "Learn 6502 assembly language".',
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
if (!f.description) {
|
|
47
|
-
missing.push({
|
|
48
|
-
field: 'gjsify.flatpak.description',
|
|
49
|
-
hint: 'Plain text (split on blank lines) or DescriptionBlock[] for rich content with bullet lists + translator hints.',
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
if (!f.license?.project) {
|
|
53
|
-
missing.push({
|
|
54
|
-
field: 'gjsify.flatpak.license.project',
|
|
55
|
-
hint: 'SPDX identifier of the project license, e.g. "MIT", "GPL-3.0-or-later".',
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
if (!f.homepageUrl) {
|
|
59
|
-
missing.push({
|
|
60
|
-
field: 'gjsify.flatpak.homepageUrl',
|
|
61
|
-
hint: 'Required by Flathub. Example: "https://github.com/you/your-repo".',
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
return missing;
|
|
38
|
+
return validateAppMetadata(toMetadataInputs(inputs));
|
|
65
39
|
}
|
|
66
40
|
/** Render the MetaInfo XML for a desktop application. */
|
|
67
41
|
export function renderMetainfoApp(inputs) {
|
|
68
|
-
return
|
|
42
|
+
return renderMetainfoAppShared(toMetadataInputs(inputs));
|
|
69
43
|
}
|
|
70
44
|
/** Render the MetaInfo XML for a console application. */
|
|
71
45
|
export function renderMetainfoCli(inputs) {
|
|
72
|
-
return
|
|
46
|
+
return renderMetainfoCliShared(toMetadataInputs(inputs));
|
|
73
47
|
}
|
|
74
48
|
/** Render the .desktop entry (app kind only). */
|
|
75
49
|
export function renderDesktop(inputs) {
|
|
76
|
-
|
|
77
|
-
const categoriesLine = (f.categories ?? ['Utility']).join(';') + ';';
|
|
78
|
-
const keywordsLine = f.keywords?.length ? `Keywords=${f.keywords.join(';')};\n` : '';
|
|
79
|
-
// `MimeType=` is sourced from `gjsify.flatpak.provides.mimetypes` —
|
|
80
|
-
// the same field already populates `<mediatype>` entries in the
|
|
81
|
-
// MetaInfo XML, so callers configure both with one knob. Typical
|
|
82
|
-
// entries: `x-scheme-handler/<scheme>` for URL-scheme handlers
|
|
83
|
-
// (Flatpak portal: chats / browsers can launch the app via a
|
|
84
|
-
// custom URL), or `application/<mime>` for file-type handlers.
|
|
85
|
-
const mimetypes = f.provides?.mimetypes ?? [];
|
|
86
|
-
const mimetypesLine = mimetypes.length ? `MimeType=${mimetypes.join(';')};\n` : '';
|
|
87
|
-
return substitute(loadDesktopTemplate(), {
|
|
88
|
-
NAME: inputs.name,
|
|
89
|
-
SUMMARY: f.summary ?? inputs.name,
|
|
90
|
-
COMMAND: inputs.command,
|
|
91
|
-
APP_ID: inputs.appId,
|
|
92
|
-
CATEGORIES_LINE: categoriesLine,
|
|
93
|
-
KEYWORDS_LINE: keywordsLine,
|
|
94
|
-
MIMETYPES_LINE: mimetypesLine,
|
|
95
|
-
});
|
|
50
|
+
return renderDesktopEntry(toMetadataInputs(inputs));
|
|
96
51
|
}
|
|
97
52
|
/** Render the flathub.json policy file. */
|
|
98
53
|
export function renderFlathubJson(kind) {
|
|
99
54
|
return kind === 'cli' ? loadFlathubCliTemplate() : loadFlathubAppTemplate();
|
|
100
55
|
}
|
|
101
|
-
// ─── MetaInfo XML builder ────────────────────────────────────────────────
|
|
102
|
-
function renderMetainfo(inputs, kind) {
|
|
103
|
-
const f = inputs.flatpak;
|
|
104
|
-
const year = new Date().getFullYear();
|
|
105
|
-
const developerName = f.developer?.name ?? '';
|
|
106
|
-
const lines = [];
|
|
107
|
-
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
108
|
-
lines.push(`<!-- Copyright ${year} ${escapeXml(developerName)} -->`);
|
|
109
|
-
lines.push(`<component type="${kind}">`);
|
|
110
|
-
lines.push(` <id>${escapeXml(inputs.appId)}</id>`);
|
|
111
|
-
lines.push(` <metadata_license>${escapeXml(f.license?.metadata ?? 'CC0-1.0')}</metadata_license>`);
|
|
112
|
-
lines.push(` <project_license>${escapeXml(f.license?.project ?? '')}</project_license>`);
|
|
113
|
-
lines.push(` <name>${escapeXml(inputs.name)}</name>`);
|
|
114
|
-
pushTranslatorHint(lines, f.summaryTranslatorHint, ' ');
|
|
115
|
-
lines.push(` <summary>${escapeXml(f.summary ?? inputs.name)}</summary>`);
|
|
116
|
-
if (f.iconRemote) {
|
|
117
|
-
lines.push(` <icon type="remote">${escapeXml(f.iconRemote)}</icon>`);
|
|
118
|
-
}
|
|
119
|
-
// <description>
|
|
120
|
-
lines.push(' <description>');
|
|
121
|
-
for (const blockLine of renderDescriptionBlocks(f.description ?? '', ' ')) {
|
|
122
|
-
lines.push(blockLine);
|
|
123
|
-
}
|
|
124
|
-
lines.push(' </description>');
|
|
125
|
-
// <developer>
|
|
126
|
-
if (f.developer?.id && f.developer?.name) {
|
|
127
|
-
lines.push(` <developer id="${escapeXml(f.developer.id)}">`);
|
|
128
|
-
const translateAttr = f.developer.nameTranslatable === true ? '' : ' translate="no"';
|
|
129
|
-
lines.push(` <name${translateAttr}>${escapeXml(f.developer.name)}</name>`);
|
|
130
|
-
if (f.developer.email) {
|
|
131
|
-
lines.push(` <email>${escapeXml(f.developer.email)}</email>`);
|
|
132
|
-
}
|
|
133
|
-
lines.push(' </developer>');
|
|
134
|
-
}
|
|
135
|
-
if (kind === 'desktop-application') {
|
|
136
|
-
lines.push(` <launchable type="desktop-id">${escapeXml(inputs.appId)}.desktop</launchable>`);
|
|
137
|
-
}
|
|
138
|
-
// <screenshots>
|
|
139
|
-
if (f.screenshots?.length) {
|
|
140
|
-
lines.push(' <screenshots>');
|
|
141
|
-
f.screenshots.forEach((s, i) => {
|
|
142
|
-
const type = s.type ?? (i === 0 ? 'default' : undefined);
|
|
143
|
-
const typeAttr = type ? ` type="${escapeXml(type)}"` : '';
|
|
144
|
-
const envAttr = s.environment ? ` environment="${escapeXml(s.environment)}"` : '';
|
|
145
|
-
lines.push(` <screenshot${typeAttr}${envAttr}>`);
|
|
146
|
-
lines.push(` <image>${escapeXml(s.url)}</image>`);
|
|
147
|
-
if (s.caption) {
|
|
148
|
-
pushTranslatorHint(lines, s.captionTranslatorHint, ' ');
|
|
149
|
-
lines.push(` <caption>${escapeXml(s.caption)}</caption>`);
|
|
150
|
-
}
|
|
151
|
-
lines.push(' </screenshot>');
|
|
152
|
-
});
|
|
153
|
-
lines.push(' </screenshots>');
|
|
154
|
-
}
|
|
155
|
-
// <url> entries
|
|
156
|
-
if (f.homepageUrl)
|
|
157
|
-
lines.push(` <url type="homepage">${escapeXml(f.homepageUrl)}</url>`);
|
|
158
|
-
if (f.bugtrackerUrl)
|
|
159
|
-
lines.push(` <url type="bugtracker">${escapeXml(f.bugtrackerUrl)}</url>`);
|
|
160
|
-
if (f.vcsBrowserUrl)
|
|
161
|
-
lines.push(` <url type="vcs-browser">${escapeXml(f.vcsBrowserUrl)}</url>`);
|
|
162
|
-
if (f.donationUrl)
|
|
163
|
-
lines.push(` <url type="donation">${escapeXml(f.donationUrl)}</url>`);
|
|
164
|
-
if (f.translateUrl)
|
|
165
|
-
lines.push(` <url type="translate">${escapeXml(f.translateUrl)}</url>`);
|
|
166
|
-
// <content_rating>
|
|
167
|
-
const cr = normaliseContentRating(f.contentRating);
|
|
168
|
-
if (cr.attributes && Object.keys(cr.attributes).length > 0) {
|
|
169
|
-
lines.push(` <content_rating type="${escapeXml(cr.type)}">`);
|
|
170
|
-
for (const [key, value] of Object.entries(cr.attributes)) {
|
|
171
|
-
lines.push(` <content_attribute id="${escapeXml(key)}">${escapeXml(value)}</content_attribute>`);
|
|
172
|
-
}
|
|
173
|
-
lines.push(' </content_rating>');
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
lines.push(` <content_rating type="${escapeXml(cr.type)}" />`);
|
|
177
|
-
}
|
|
178
|
-
// <releases>
|
|
179
|
-
if (f.releases?.length) {
|
|
180
|
-
lines.push(' <releases>');
|
|
181
|
-
for (const r of f.releases) {
|
|
182
|
-
if (r.description === undefined) {
|
|
183
|
-
lines.push(` <release version="${escapeXml(r.version)}" date="${escapeXml(r.date)}" />`);
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
lines.push(` <release version="${escapeXml(r.version)}" date="${escapeXml(r.date)}">`);
|
|
187
|
-
lines.push(' <description>');
|
|
188
|
-
for (const blockLine of renderDescriptionBlocks(r.description, ' ')) {
|
|
189
|
-
lines.push(blockLine);
|
|
190
|
-
}
|
|
191
|
-
lines.push(' </description>');
|
|
192
|
-
lines.push(' </release>');
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
lines.push(' </releases>');
|
|
196
|
-
}
|
|
197
|
-
// <categories>
|
|
198
|
-
if (f.categories?.length) {
|
|
199
|
-
lines.push(' <categories>');
|
|
200
|
-
for (const c of f.categories)
|
|
201
|
-
lines.push(` <category>${escapeXml(c)}</category>`);
|
|
202
|
-
lines.push(' </categories>');
|
|
203
|
-
}
|
|
204
|
-
// <keywords>
|
|
205
|
-
if (f.keywords?.length) {
|
|
206
|
-
lines.push(' <keywords>');
|
|
207
|
-
for (const k of f.keywords)
|
|
208
|
-
lines.push(` <keyword>${escapeXml(k)}</keyword>`);
|
|
209
|
-
lines.push(' </keywords>');
|
|
210
|
-
}
|
|
211
|
-
// <branding> (apps only — Flathub ignores it on CLI)
|
|
212
|
-
if (kind === 'desktop-application' && f.branding) {
|
|
213
|
-
lines.push(' <branding>');
|
|
214
|
-
lines.push(` <color type="primary" scheme_preference="light">${escapeXml(f.branding.accentLight)}</color>`);
|
|
215
|
-
lines.push(` <color type="primary" scheme_preference="dark">${escapeXml(f.branding.accentDark)}</color>`);
|
|
216
|
-
lines.push(' </branding>');
|
|
217
|
-
}
|
|
218
|
-
// <kudos>
|
|
219
|
-
if (f.kudos?.length) {
|
|
220
|
-
lines.push(' <kudos>');
|
|
221
|
-
for (const k of f.kudos)
|
|
222
|
-
lines.push(` <kudo>${escapeXml(k)}</kudo>`);
|
|
223
|
-
lines.push(' </kudos>');
|
|
224
|
-
}
|
|
225
|
-
// <provides> — always emit <binary> for both kinds; <mediatype>/<dbus> only when configured
|
|
226
|
-
const binaries = f.provides?.binaries ?? [inputs.command];
|
|
227
|
-
const mimetypes = f.provides?.mimetypes ?? [];
|
|
228
|
-
const dbus = f.provides?.dbus ?? [];
|
|
229
|
-
if (binaries.length || mimetypes.length || dbus.length) {
|
|
230
|
-
lines.push(' <provides>');
|
|
231
|
-
for (const b of binaries)
|
|
232
|
-
lines.push(` <binary>${escapeXml(b)}</binary>`);
|
|
233
|
-
for (const m of mimetypes)
|
|
234
|
-
lines.push(` <mediatype>${escapeXml(m)}</mediatype>`);
|
|
235
|
-
for (const d of dbus)
|
|
236
|
-
lines.push(` <dbus type="${escapeXml(d.type)}">${escapeXml(d.id)}</dbus>`);
|
|
237
|
-
lines.push(' </provides>');
|
|
238
|
-
}
|
|
239
|
-
// <supports>
|
|
240
|
-
if (f.supports?.controls?.length || f.supports?.internet) {
|
|
241
|
-
lines.push(' <supports>');
|
|
242
|
-
for (const c of f.supports.controls ?? [])
|
|
243
|
-
lines.push(` <control>${escapeXml(c)}</control>`);
|
|
244
|
-
if (f.supports.internet)
|
|
245
|
-
lines.push(` <internet>${escapeXml(f.supports.internet)}</internet>`);
|
|
246
|
-
lines.push(' </supports>');
|
|
247
|
-
}
|
|
248
|
-
// <requires>
|
|
249
|
-
if (f.requires?.displayLengthMin || f.requires?.controls?.length || f.requires?.internet) {
|
|
250
|
-
lines.push(' <requires>');
|
|
251
|
-
if (f.requires.displayLengthMin) {
|
|
252
|
-
lines.push(` <display_length compare="ge">${f.requires.displayLengthMin}</display_length>`);
|
|
253
|
-
}
|
|
254
|
-
for (const c of f.requires.controls ?? [])
|
|
255
|
-
lines.push(` <control>${escapeXml(c)}</control>`);
|
|
256
|
-
if (f.requires.internet)
|
|
257
|
-
lines.push(` <internet>${escapeXml(f.requires.internet)}</internet>`);
|
|
258
|
-
lines.push(' </requires>');
|
|
259
|
-
}
|
|
260
|
-
// <recommends>
|
|
261
|
-
if (f.recommends?.displayLengthMin || f.recommends?.controls?.length) {
|
|
262
|
-
lines.push(' <recommends>');
|
|
263
|
-
if (f.recommends.displayLengthMin) {
|
|
264
|
-
lines.push(` <display_length compare="ge">${f.recommends.displayLengthMin}</display_length>`);
|
|
265
|
-
}
|
|
266
|
-
for (const c of f.recommends.controls ?? [])
|
|
267
|
-
lines.push(` <control>${escapeXml(c)}</control>`);
|
|
268
|
-
lines.push(' </recommends>');
|
|
269
|
-
}
|
|
270
|
-
lines.push('</component>');
|
|
271
|
-
return lines.join('\n') + '\n';
|
|
272
|
-
}
|
|
273
|
-
// ─── Description block renderer ──────────────────────────────────────────
|
|
274
|
-
function renderDescriptionBlocks(description, indent) {
|
|
275
|
-
const blocks = typeof description === 'string' ? stringToBlocks(description) : description;
|
|
276
|
-
const out = [];
|
|
277
|
-
for (const block of blocks) {
|
|
278
|
-
if ('p' in block) {
|
|
279
|
-
pushTranslatorHint(out, block.translatorHint, indent);
|
|
280
|
-
out.push(`${indent}<p>${escapeXml(block.p.trim().replace(/\s+/g, ' '))}</p>`);
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
pushTranslatorHint(out, block.translatorHint, indent);
|
|
284
|
-
out.push(`${indent}<ul>`);
|
|
285
|
-
for (const item of block.ul) {
|
|
286
|
-
if (typeof item === 'string') {
|
|
287
|
-
out.push(`${indent} <li>${escapeXml(item)}</li>`);
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
pushTranslatorHint(out, item.translatorHint, `${indent} `);
|
|
291
|
-
out.push(`${indent} <li>${escapeXml(item.item)}</li>`);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
out.push(`${indent}</ul>`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return out;
|
|
298
|
-
}
|
|
299
|
-
/** Auto-convert blank-line-split string into paragraph blocks. */
|
|
300
|
-
function stringToBlocks(s) {
|
|
301
|
-
return s
|
|
302
|
-
.trim()
|
|
303
|
-
.split(/\n\n+/)
|
|
304
|
-
.map((para) => ({ p: para.trim().replace(/\s+/g, ' ') }));
|
|
305
|
-
}
|
|
306
|
-
function pushTranslatorHint(out, hint, indent) {
|
|
307
|
-
if (!hint)
|
|
308
|
-
return;
|
|
309
|
-
out.push(`${indent}<!-- TRANSLATORS: ${hint} -->`);
|
|
310
|
-
}
|
|
311
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
312
|
-
function normaliseContentRating(cr) {
|
|
313
|
-
if (cr === undefined)
|
|
314
|
-
return { type: 'oars-1.1' };
|
|
315
|
-
if (typeof cr === 'string')
|
|
316
|
-
return { type: cr };
|
|
317
|
-
return { type: cr.type ?? 'oars-1.1', attributes: cr.attributes };
|
|
318
|
-
}
|
|
319
|
-
function substitute(template, tokens) {
|
|
320
|
-
let out = template;
|
|
321
|
-
for (const [key, value] of Object.entries(tokens)) {
|
|
322
|
-
out = out.split(`{{${key}}}`).join(value);
|
|
323
|
-
}
|
|
324
|
-
return out;
|
|
325
|
-
}
|
|
326
|
-
function escapeXml(value) {
|
|
327
|
-
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
328
|
-
}
|
|
@@ -75,7 +75,7 @@ export const flatpakSyncFlathubCommand = {
|
|
|
75
75
|
handler: async (args) => {
|
|
76
76
|
const cwd = process.cwd();
|
|
77
77
|
const cfg = new Config();
|
|
78
|
-
const configData = await cfg.
|
|
78
|
+
const configData = await cfg.forCommand().catch(() => ({}));
|
|
79
79
|
const flatpak = configData.flatpak ?? {};
|
|
80
80
|
const appId = args.appId ?? flatpak.appId ?? readPackageJson(cwd).name;
|
|
81
81
|
if (!appId) {
|