@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,327 @@
1
+ // MetaInfo XML / .desktop / flathub.json scaffolding for
2
+ // `gjsify flatpak init`. Phase F.9.6 onwards builds the MetaInfo XML
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.
8
+ //
9
+ // `.desktop` and `flathub.json` keep their static templates (they're
10
+ // flat key=value or empty-object files where substitution is fine).
11
+ import { readFileSync } from 'node:fs';
12
+ /**
13
+ * Lazy template loaders for the two artefacts that stay template-based.
14
+ * `static-read-inliner` matches this shape and inlines the templates
15
+ * into the GJS bundle at build time.
16
+ */
17
+ function loadDesktopTemplate() {
18
+ return readFileSync(new URL('../../templates/flatpak/desktop.tmpl', import.meta.url), 'utf-8');
19
+ }
20
+ function loadFlathubAppTemplate() {
21
+ return readFileSync(new URL('../../templates/flatpak/flathub-app.json.tmpl', import.meta.url), 'utf-8');
22
+ }
23
+ function loadFlathubCliTemplate() {
24
+ return readFileSync(new URL('../../templates/flatpak/flathub-cli.json.tmpl', import.meta.url), 'utf-8');
25
+ }
26
+ /**
27
+ * Validate that the config has the minimum set of fields required for
28
+ * MetaInfo XML rendering. Returns the list of missing fields with
29
+ * actionable hints; empty list means OK.
30
+ */
31
+ export function validateScaffoldInputs(inputs) {
32
+ const f = inputs.flatpak;
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;
65
+ }
66
+ /** Render the MetaInfo XML for a desktop application. */
67
+ export function renderMetainfoApp(inputs) {
68
+ return renderMetainfo(inputs, 'desktop-application');
69
+ }
70
+ /** Render the MetaInfo XML for a console application. */
71
+ export function renderMetainfoCli(inputs) {
72
+ return renderMetainfo(inputs, 'console-application');
73
+ }
74
+ /** Render the .desktop entry (app kind only). */
75
+ export function renderDesktop(inputs) {
76
+ const f = inputs.flatpak;
77
+ const categoriesLine = (f.categories ?? ['Utility']).join(';') + ';';
78
+ const keywordsLine = f.keywords?.length
79
+ ? `Keywords=${f.keywords.join(';')};\n`
80
+ : '';
81
+ return substitute(loadDesktopTemplate(), {
82
+ NAME: inputs.name,
83
+ SUMMARY: f.summary ?? inputs.name,
84
+ COMMAND: inputs.command,
85
+ APP_ID: inputs.appId,
86
+ CATEGORIES_LINE: categoriesLine,
87
+ KEYWORDS_LINE: keywordsLine,
88
+ });
89
+ }
90
+ /** Render the flathub.json policy file. */
91
+ export function renderFlathubJson(kind) {
92
+ return kind === 'cli' ? loadFlathubCliTemplate() : loadFlathubAppTemplate();
93
+ }
94
+ // ─── MetaInfo XML builder ────────────────────────────────────────────────
95
+ function renderMetainfo(inputs, kind) {
96
+ const f = inputs.flatpak;
97
+ const year = new Date().getFullYear();
98
+ const developerName = f.developer?.name ?? '';
99
+ const lines = [];
100
+ lines.push('<?xml version="1.0" encoding="UTF-8"?>');
101
+ lines.push(`<!-- Copyright ${year} ${escapeXml(developerName)} -->`);
102
+ lines.push(`<component type="${kind}">`);
103
+ lines.push(` <id>${escapeXml(inputs.appId)}</id>`);
104
+ lines.push(` <metadata_license>${escapeXml(f.license?.metadata ?? 'CC0-1.0')}</metadata_license>`);
105
+ lines.push(` <project_license>${escapeXml(f.license?.project ?? '')}</project_license>`);
106
+ lines.push(` <name>${escapeXml(inputs.name)}</name>`);
107
+ pushTranslatorHint(lines, f.summaryTranslatorHint, ' ');
108
+ lines.push(` <summary>${escapeXml(f.summary ?? inputs.name)}</summary>`);
109
+ if (f.iconRemote) {
110
+ lines.push(` <icon type="remote">${escapeXml(f.iconRemote)}</icon>`);
111
+ }
112
+ // <description>
113
+ lines.push(' <description>');
114
+ for (const blockLine of renderDescriptionBlocks(f.description ?? '', ' ')) {
115
+ lines.push(blockLine);
116
+ }
117
+ lines.push(' </description>');
118
+ // <developer>
119
+ if (f.developer?.id && f.developer?.name) {
120
+ lines.push(` <developer id="${escapeXml(f.developer.id)}">`);
121
+ const translateAttr = f.developer.nameTranslatable === true ? '' : ' translate="no"';
122
+ lines.push(` <name${translateAttr}>${escapeXml(f.developer.name)}</name>`);
123
+ if (f.developer.email) {
124
+ lines.push(` <email>${escapeXml(f.developer.email)}</email>`);
125
+ }
126
+ lines.push(' </developer>');
127
+ }
128
+ if (kind === 'desktop-application') {
129
+ lines.push(` <launchable type="desktop-id">${escapeXml(inputs.appId)}.desktop</launchable>`);
130
+ }
131
+ // <screenshots>
132
+ if (f.screenshots?.length) {
133
+ lines.push(' <screenshots>');
134
+ f.screenshots.forEach((s, i) => {
135
+ const type = s.type ?? (i === 0 ? 'default' : undefined);
136
+ const typeAttr = type ? ` type="${escapeXml(type)}"` : '';
137
+ const envAttr = s.environment ? ` environment="${escapeXml(s.environment)}"` : '';
138
+ lines.push(` <screenshot${typeAttr}${envAttr}>`);
139
+ lines.push(` <image>${escapeXml(s.url)}</image>`);
140
+ if (s.caption) {
141
+ pushTranslatorHint(lines, s.captionTranslatorHint, ' ');
142
+ lines.push(` <caption>${escapeXml(s.caption)}</caption>`);
143
+ }
144
+ lines.push(' </screenshot>');
145
+ });
146
+ lines.push(' </screenshots>');
147
+ }
148
+ // <url> entries
149
+ if (f.homepageUrl)
150
+ lines.push(` <url type="homepage">${escapeXml(f.homepageUrl)}</url>`);
151
+ if (f.bugtrackerUrl)
152
+ lines.push(` <url type="bugtracker">${escapeXml(f.bugtrackerUrl)}</url>`);
153
+ if (f.vcsBrowserUrl)
154
+ lines.push(` <url type="vcs-browser">${escapeXml(f.vcsBrowserUrl)}</url>`);
155
+ if (f.donationUrl)
156
+ lines.push(` <url type="donation">${escapeXml(f.donationUrl)}</url>`);
157
+ if (f.translateUrl)
158
+ lines.push(` <url type="translate">${escapeXml(f.translateUrl)}</url>`);
159
+ // <content_rating>
160
+ const cr = normaliseContentRating(f.contentRating);
161
+ if (cr.attributes && Object.keys(cr.attributes).length > 0) {
162
+ lines.push(` <content_rating type="${escapeXml(cr.type)}">`);
163
+ for (const [key, value] of Object.entries(cr.attributes)) {
164
+ lines.push(` <content_attribute id="${escapeXml(key)}">${escapeXml(value)}</content_attribute>`);
165
+ }
166
+ lines.push(' </content_rating>');
167
+ }
168
+ else {
169
+ lines.push(` <content_rating type="${escapeXml(cr.type)}" />`);
170
+ }
171
+ // <releases>
172
+ if (f.releases?.length) {
173
+ lines.push(' <releases>');
174
+ for (const r of f.releases) {
175
+ if (r.description === undefined) {
176
+ lines.push(` <release version="${escapeXml(r.version)}" date="${escapeXml(r.date)}" />`);
177
+ }
178
+ else {
179
+ lines.push(` <release version="${escapeXml(r.version)}" date="${escapeXml(r.date)}">`);
180
+ lines.push(' <description>');
181
+ for (const blockLine of renderDescriptionBlocks(r.description, ' ')) {
182
+ lines.push(blockLine);
183
+ }
184
+ lines.push(' </description>');
185
+ lines.push(' </release>');
186
+ }
187
+ }
188
+ lines.push(' </releases>');
189
+ }
190
+ // <categories>
191
+ if (f.categories?.length) {
192
+ lines.push(' <categories>');
193
+ for (const c of f.categories)
194
+ lines.push(` <category>${escapeXml(c)}</category>`);
195
+ lines.push(' </categories>');
196
+ }
197
+ // <keywords>
198
+ if (f.keywords?.length) {
199
+ lines.push(' <keywords>');
200
+ for (const k of f.keywords)
201
+ lines.push(` <keyword>${escapeXml(k)}</keyword>`);
202
+ lines.push(' </keywords>');
203
+ }
204
+ // <branding> (apps only — Flathub ignores it on CLI)
205
+ if (kind === 'desktop-application' && f.branding) {
206
+ lines.push(' <branding>');
207
+ lines.push(` <color type="primary" scheme_preference="light">${escapeXml(f.branding.accentLight)}</color>`);
208
+ lines.push(` <color type="primary" scheme_preference="dark">${escapeXml(f.branding.accentDark)}</color>`);
209
+ lines.push(' </branding>');
210
+ }
211
+ // <kudos>
212
+ if (f.kudos?.length) {
213
+ lines.push(' <kudos>');
214
+ for (const k of f.kudos)
215
+ lines.push(` <kudo>${escapeXml(k)}</kudo>`);
216
+ lines.push(' </kudos>');
217
+ }
218
+ // <provides> — always emit <binary> for both kinds; <mediatype>/<dbus> only when configured
219
+ const binaries = f.provides?.binaries ?? [inputs.command];
220
+ const mimetypes = f.provides?.mimetypes ?? [];
221
+ const dbus = f.provides?.dbus ?? [];
222
+ if (binaries.length || mimetypes.length || dbus.length) {
223
+ lines.push(' <provides>');
224
+ for (const b of binaries)
225
+ lines.push(` <binary>${escapeXml(b)}</binary>`);
226
+ for (const m of mimetypes)
227
+ lines.push(` <mediatype>${escapeXml(m)}</mediatype>`);
228
+ for (const d of dbus)
229
+ lines.push(` <dbus type="${escapeXml(d.type)}">${escapeXml(d.id)}</dbus>`);
230
+ lines.push(' </provides>');
231
+ }
232
+ // <supports>
233
+ if (f.supports?.controls?.length || f.supports?.internet) {
234
+ lines.push(' <supports>');
235
+ for (const c of f.supports.controls ?? [])
236
+ lines.push(` <control>${escapeXml(c)}</control>`);
237
+ if (f.supports.internet)
238
+ lines.push(` <internet>${escapeXml(f.supports.internet)}</internet>`);
239
+ lines.push(' </supports>');
240
+ }
241
+ // <requires>
242
+ if (f.requires?.displayLengthMin || f.requires?.controls?.length || f.requires?.internet) {
243
+ lines.push(' <requires>');
244
+ if (f.requires.displayLengthMin) {
245
+ lines.push(` <display_length compare="ge">${f.requires.displayLengthMin}</display_length>`);
246
+ }
247
+ for (const c of f.requires.controls ?? [])
248
+ lines.push(` <control>${escapeXml(c)}</control>`);
249
+ if (f.requires.internet)
250
+ lines.push(` <internet>${escapeXml(f.requires.internet)}</internet>`);
251
+ lines.push(' </requires>');
252
+ }
253
+ // <recommends>
254
+ if (f.recommends?.displayLengthMin || f.recommends?.controls?.length) {
255
+ lines.push(' <recommends>');
256
+ if (f.recommends.displayLengthMin) {
257
+ lines.push(` <display_length compare="ge">${f.recommends.displayLengthMin}</display_length>`);
258
+ }
259
+ for (const c of f.recommends.controls ?? [])
260
+ lines.push(` <control>${escapeXml(c)}</control>`);
261
+ lines.push(' </recommends>');
262
+ }
263
+ lines.push('</component>');
264
+ return lines.join('\n') + '\n';
265
+ }
266
+ // ─── Description block renderer ──────────────────────────────────────────
267
+ function renderDescriptionBlocks(description, indent) {
268
+ const blocks = typeof description === 'string'
269
+ ? stringToBlocks(description)
270
+ : description;
271
+ const out = [];
272
+ for (const block of blocks) {
273
+ if ('p' in block) {
274
+ pushTranslatorHint(out, block.translatorHint, indent);
275
+ out.push(`${indent}<p>${escapeXml(block.p.trim().replace(/\s+/g, ' '))}</p>`);
276
+ }
277
+ else {
278
+ pushTranslatorHint(out, block.translatorHint, indent);
279
+ out.push(`${indent}<ul>`);
280
+ for (const item of block.ul) {
281
+ if (typeof item === 'string') {
282
+ out.push(`${indent} <li>${escapeXml(item)}</li>`);
283
+ }
284
+ else {
285
+ pushTranslatorHint(out, item.translatorHint, `${indent} `);
286
+ out.push(`${indent} <li>${escapeXml(item.item)}</li>`);
287
+ }
288
+ }
289
+ out.push(`${indent}</ul>`);
290
+ }
291
+ }
292
+ return out;
293
+ }
294
+ /** Auto-convert blank-line-split string into paragraph blocks. */
295
+ function stringToBlocks(s) {
296
+ return s
297
+ .trim()
298
+ .split(/\n\n+/)
299
+ .map((para) => ({ p: para.trim().replace(/\s+/g, ' ') }));
300
+ }
301
+ function pushTranslatorHint(out, hint, indent) {
302
+ if (!hint)
303
+ return;
304
+ out.push(`${indent}<!-- TRANSLATORS: ${hint} -->`);
305
+ }
306
+ // ─── Helpers ─────────────────────────────────────────────────────────────
307
+ function normaliseContentRating(cr) {
308
+ if (cr === undefined)
309
+ return { type: 'oars-1.1' };
310
+ if (typeof cr === 'string')
311
+ return { type: cr };
312
+ return { type: cr.type ?? 'oars-1.1', attributes: cr.attributes };
313
+ }
314
+ function substitute(template, tokens) {
315
+ let out = template;
316
+ for (const [key, value] of Object.entries(tokens)) {
317
+ out = out.split(`{{${key}}}`).join(value);
318
+ }
319
+ return out;
320
+ }
321
+ function escapeXml(value) {
322
+ return value
323
+ .replace(/&/g, '&amp;')
324
+ .replace(/</g, '&lt;')
325
+ .replace(/>/g, '&gt;')
326
+ .replace(/"/g, '&quot;');
327
+ }
@@ -16,3 +16,4 @@ export * from './pack.js';
16
16
  export * from './publish.js';
17
17
  export * from './self-update.js';
18
18
  export * from './generate-installer.js';
19
+ export * from './uninstall.js';
@@ -16,3 +16,4 @@ export * from './pack.js';
16
16
  export * from './publish.js';
17
17
  export * from './self-update.js';
18
18
  export * from './generate-installer.js';
19
+ export * from './uninstall.js';
@@ -0,0 +1,9 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface UninstallOptions {
3
+ packages: string[];
4
+ global?: boolean;
5
+ 'dry-run'?: boolean;
6
+ verbose?: boolean;
7
+ }
8
+ export declare const uninstallCommand: Command<any, UninstallOptions>;
9
+ export {};
@@ -0,0 +1,145 @@
1
+ // `gjsify uninstall -g <pkg>` — symmetric inverse of `install -g`.
2
+ //
3
+ // Removes the installed package tree from the user-global XDG location
4
+ // and any bin shims under `~/.local/bin/` that point into it. Mirrors
5
+ // the layout decisions in install-global.ts:
6
+ //
7
+ // ~/.local/share/gjsify/global/node_modules/<pkg>/ ← deleted
8
+ // ~/.local/bin/<bin> ← deleted iff it
9
+ // execs a path
10
+ // inside the
11
+ // removed tree
12
+ //
13
+ // Scope: --global only. Project-local uninstall (mirror of `npm uninstall
14
+ // <pkg>` without -g) is a separate workstream — it needs to rewrite
15
+ // package.json + refresh the lockfile, which install -g doesn't touch.
16
+ import { existsSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { defaultGlobalLayout, specToPackageName } from '../utils/install-global.js';
19
+ export const uninstallCommand = {
20
+ command: 'uninstall <packages..>',
21
+ description: 'Uninstall a previously installed package. Currently only `--global` mode is supported.',
22
+ builder: (yargs) => yargs
23
+ .positional('packages', {
24
+ description: 'Package(s) to uninstall (npm names, optionally with version).',
25
+ type: 'string',
26
+ array: true,
27
+ demandOption: true,
28
+ })
29
+ .option('global', {
30
+ description: 'Uninstall from the user-global XDG location (the install -g target).',
31
+ type: 'boolean',
32
+ alias: 'g',
33
+ default: false,
34
+ })
35
+ .option('dry-run', {
36
+ description: 'Show what would be removed without touching the filesystem.',
37
+ type: 'boolean',
38
+ default: false,
39
+ })
40
+ .option('verbose', {
41
+ description: 'Verbose logging.',
42
+ type: 'boolean',
43
+ default: false,
44
+ }),
45
+ handler: (args) => {
46
+ if (!args.global) {
47
+ console.error('gjsify uninstall currently only supports --global. ' +
48
+ 'For project-local removal, edit package.json + re-run `gjsify install`.');
49
+ process.exit(1);
50
+ return;
51
+ }
52
+ const layout = defaultGlobalLayout();
53
+ const dryRun = args['dry-run'] ?? false;
54
+ const verbose = args.verbose ?? false;
55
+ const prefix = `gjsify uninstall${dryRun ? ' (dry-run)' : ''} --global`;
56
+ console.log(`${prefix} ← ${layout.prefix}`);
57
+ console.log(`${' '.repeat(prefix.length)} bins ← ${layout.binDir}`);
58
+ let removedAny = false;
59
+ for (const spec of args.packages) {
60
+ const pkgName = specToPackageName(spec);
61
+ const pkgDir = join(layout.prefix, 'node_modules', pkgName);
62
+ if (!existsSync(pkgDir)) {
63
+ console.warn(` ✗ ${pkgName} — not installed at ${pkgDir}`);
64
+ continue;
65
+ }
66
+ // Find bin shims that exec into this package's tree. The shims
67
+ // are POSIX sh launchers written by linkGlobalBins; we identify
68
+ // candidates by reading the launcher script and matching the
69
+ // absolute path.
70
+ const binsToRemove = findBinShimsForPackage(layout.binDir, pkgDir, verbose);
71
+ if (dryRun) {
72
+ console.log(` • would remove ${pkgDir}`);
73
+ for (const bin of binsToRemove) {
74
+ console.log(` • would remove ${bin}`);
75
+ }
76
+ }
77
+ else {
78
+ rmSync(pkgDir, { recursive: true, force: true });
79
+ console.log(` • removed ${pkgDir}`);
80
+ for (const bin of binsToRemove) {
81
+ unlinkSync(bin);
82
+ console.log(` • removed ${bin}`);
83
+ }
84
+ }
85
+ removedAny = true;
86
+ }
87
+ if (!removedAny) {
88
+ console.error('\nNo packages removed.');
89
+ process.exit(1);
90
+ }
91
+ },
92
+ };
93
+ /**
94
+ * Scan `binDir` for POSIX `sh` launchers whose `exec` target points into
95
+ * `pkgDir`. The launcher shape is fixed by `linkGlobalBins` — either:
96
+ *
97
+ * #!/bin/sh
98
+ * exec '<absolute-path>' "$@"
99
+ *
100
+ * or (for `.gjs.mjs` / `.mjs` targets):
101
+ *
102
+ * #!/bin/sh
103
+ * exec gjs -m '<absolute-path>' "$@"
104
+ *
105
+ * We parse the absolute path out of the single-quoted segment and check
106
+ * whether it's under `pkgDir`. Non-shim files (e.g. unrelated binaries
107
+ * the user installed via `npm install -g`) are skipped silently.
108
+ */
109
+ function findBinShimsForPackage(binDir, pkgDir, verbose) {
110
+ if (!existsSync(binDir))
111
+ return [];
112
+ const matches = [];
113
+ let entries;
114
+ try {
115
+ entries = readdirSync(binDir);
116
+ }
117
+ catch {
118
+ return [];
119
+ }
120
+ for (const name of entries) {
121
+ const fullPath = join(binDir, name);
122
+ try {
123
+ const st = statSync(fullPath);
124
+ if (!st.isFile())
125
+ continue;
126
+ const content = readFileSync(fullPath, 'utf-8');
127
+ if (!content.startsWith('#!/bin/sh'))
128
+ continue;
129
+ // Match the first single-quoted absolute path.
130
+ const m = content.match(/'([^']+)'/);
131
+ if (!m)
132
+ continue;
133
+ const target = m[1];
134
+ if (target.startsWith(pkgDir + '/') || target === pkgDir) {
135
+ matches.push(fullPath);
136
+ }
137
+ }
138
+ catch (err) {
139
+ if (verbose) {
140
+ console.warn(` ? could not inspect ${fullPath}: ${err.message}`);
141
+ }
142
+ }
143
+ }
144
+ return matches;
145
+ }
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import yargs from 'yargs';
3
3
  import { hideBin } from 'yargs/helpers';
4
- import { buildCommand as build, runCommand as run, infoCommand as info, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, } from './commands/index.js';
4
+ import { buildCommand as build, runCommand as run, infoCommand as info, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, } from './commands/index.js';
5
5
  import { APP_NAME } from './constants.js';
6
6
  // `parseAsync()` instead of `.argv` so the top-level await keeps the
7
7
  // process alive until command handlers complete. Under Node this is
@@ -29,6 +29,7 @@ await yargs(hideBin(process.argv))
29
29
  .command(publish.command, publish.description, publish.builder, publish.handler)
30
30
  .command(selfUpdate.command, selfUpdate.description, selfUpdate.builder, selfUpdate.handler)
31
31
  .command(generateInstaller.command, generateInstaller.description, generateInstaller.builder, generateInstaller.handler)
32
+ .command(uninstall.command, uninstall.description, uninstall.builder, uninstall.handler)
32
33
  .demandCommand(1)
33
34
  .help()
34
35
  .parseAsync();
@@ -0,0 +1,10 @@
1
+ [Desktop Entry]
2
+ Name={{NAME}}
3
+ Comment={{SUMMARY}}
4
+ Exec={{COMMAND}}
5
+ Icon={{APP_ID}}
6
+ Terminal=false
7
+ Type=Application
8
+ Categories={{CATEGORIES_LINE}}
9
+ {{KEYWORDS_LINE}}StartupNotify=true
10
+ StartupWMClass={{APP_ID}}
@@ -0,0 +1,3 @@
1
+ {
2
+ "skip-icons-check": true
3
+ }