@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.
- package/dist/cli.gjs.mjs +16 -12
- package/lib/commands/flatpak/check.d.ts +11 -0
- package/lib/commands/flatpak/check.js +163 -0
- package/lib/commands/flatpak/index.d.ts +2 -1
- package/lib/commands/flatpak/index.js +5 -3
- package/lib/commands/flatpak/init.d.ts +4 -0
- package/lib/commands/flatpak/init.js +94 -27
- package/lib/commands/flatpak/scaffold.d.ts +26 -0
- package/lib/commands/flatpak/scaffold.js +327 -0
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/uninstall.d.ts +9 -0
- package/lib/commands/uninstall.js +145 -0
- package/lib/index.js +2 -1
- package/lib/templates/flatpak/desktop.tmpl +10 -0
- package/lib/templates/flatpak/flathub-app.json.tmpl +1 -0
- package/lib/templates/flatpak/flathub-cli.json.tmpl +3 -0
- package/lib/types/config-data.d.ts +193 -0
- package/package.json +16 -16
|
@@ -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, '&')
|
|
324
|
+
.replace(/</g, '<')
|
|
325
|
+
.replace(/>/g, '>')
|
|
326
|
+
.replace(/"/g, '"');
|
|
327
|
+
}
|
package/lib/commands/index.d.ts
CHANGED
package/lib/commands/index.js
CHANGED
|
@@ -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 @@
|
|
|
1
|
+
{}
|