francois 0.18.13 → 0.19.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/bin/francois.js +116 -0
- package/bin/francois.test.mjs +0 -0
- package/manifest.json +5 -5
- package/package.json +1 -1
package/bin/francois.js
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { spawn, spawnSync } = require('node:child_process');
|
|
13
|
+
const fs = require('node:fs');
|
|
13
14
|
const path = require('node:path');
|
|
14
15
|
|
|
15
16
|
const desktop = require('../lib/desktop.js');
|
|
17
|
+
const extensions = require('../lib/extensions.js');
|
|
16
18
|
const {
|
|
17
19
|
assetKey,
|
|
18
20
|
readInstallRecord,
|
|
@@ -30,11 +32,118 @@ Usage
|
|
|
30
32
|
francois shortcut re-register the Start Menu / Launchpad / menu entry
|
|
31
33
|
francois shortcut --remove unregister it
|
|
32
34
|
francois uninstall unregister the desktop entry, then npm-uninstall this package
|
|
35
|
+
francois ext install <name> install a plugin by name, local path, or git URL
|
|
36
|
+
francois ext list list installed plugins (works with the app closed)
|
|
37
|
+
francois ext remove <id> delete an installed plugin
|
|
33
38
|
francois --help this message
|
|
34
39
|
|
|
35
40
|
Anything else is forwarded to the app.
|
|
36
41
|
`;
|
|
37
42
|
|
|
43
|
+
const EXT_USAGE = `francois ext — manage plugins in ~/.francois/extensions (filesystem only,
|
|
44
|
+
never the app's socket — this works with the app closed).
|
|
45
|
+
|
|
46
|
+
Usage
|
|
47
|
+
francois ext install <name|path|git-url> [--force]
|
|
48
|
+
francois ext list
|
|
49
|
+
francois ext remove <id> [--yes]
|
|
50
|
+
|
|
51
|
+
A bare <name> resolves by CONVENTION to the repository
|
|
52
|
+
\`github.com/antoine-gmnz/francois-plugin-<name>\`; \`<owner>/<name>\` uses that
|
|
53
|
+
owner instead. There is no index and nothing to search — the convention is a
|
|
54
|
+
URL shorthand, so any plugin is still installable by its full URL. An existing
|
|
55
|
+
local directory of that name always wins, so nothing reaches the network when
|
|
56
|
+
a local answer exists.
|
|
57
|
+
|
|
58
|
+
Installing never enables a plugin — review and enable it in Extensions (⌘K).
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
/** A single blocking line read off stdin — no dependency, works in a real
|
|
62
|
+
* terminal and under a piped `echo y |` in CI alike. */
|
|
63
|
+
function confirmSync(question) {
|
|
64
|
+
process.stdout.write(question);
|
|
65
|
+
const buf = Buffer.alloc(4096);
|
|
66
|
+
let bytesRead = 0;
|
|
67
|
+
try {
|
|
68
|
+
bytesRead = fs.readSync(0, buf, 0, buf.length, null);
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const answer = buf.toString('utf8', 0, bytesRead).trim().toLowerCase();
|
|
73
|
+
return answer === 'y' || answer === 'yes';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function runExt(args) {
|
|
77
|
+
const sub = args[0];
|
|
78
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
79
|
+
process.stdout.write(EXT_USAGE);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (sub === 'install') {
|
|
84
|
+
const force = args.includes('--force');
|
|
85
|
+
const source = args.slice(1).find((a) => !a.startsWith('--'));
|
|
86
|
+
if (!source) die('francois ext install <name|path|git-url>');
|
|
87
|
+
let result;
|
|
88
|
+
try {
|
|
89
|
+
result = extensions.installExtension(source, { force });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
die(error.message);
|
|
92
|
+
}
|
|
93
|
+
// The resolved source is printed because a bare name is ambiguous by
|
|
94
|
+
// design: the user must be able to see whether it copied a directory or
|
|
95
|
+
// cloned a repository, without re-deriving the convention in their head.
|
|
96
|
+
// `result.source`/`result.id`/`result.path` are all untrusted (attacker-
|
|
97
|
+
// controlled path/URL text) — sanitize before they ever reach stdout, the
|
|
98
|
+
// same as the `list` path.
|
|
99
|
+
process.stdout.write(
|
|
100
|
+
`francois: installed "${extensions.sanitizeForDisplay(result.id)}" from ${extensions.sanitizeForDisplay(result.source)}\n`
|
|
101
|
+
);
|
|
102
|
+
process.stdout.write(`francois: at ${extensions.sanitizeForDisplay(result.path)}\n`);
|
|
103
|
+
// FR-15/FR-28: discovery is not authorization — it always arrives inert.
|
|
104
|
+
process.stdout.write('francois: disabled — enable it in Extensions (⌘K)\n');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (sub === 'list') {
|
|
109
|
+
const found = extensions.listExtensions();
|
|
110
|
+
if (found.length === 0) {
|
|
111
|
+
process.stdout.write(`francois: nothing installed yet — see ${extensions.extensionsDir()}\n`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const ext of found) {
|
|
115
|
+
const status = !ext.valid ? 'invalid manifest' : ext.enabled ? 'enabled' : 'disabled';
|
|
116
|
+
process.stdout.write(`${ext.id}\t${ext.label}\t${ext.path}\t${status}\n`);
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (sub === 'remove') {
|
|
122
|
+
const id = args[1];
|
|
123
|
+
if (!id || id.startsWith('--')) die('francois ext remove <id>');
|
|
124
|
+
let target;
|
|
125
|
+
try {
|
|
126
|
+
target = extensions.resolveExtensionDir(id);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
die(error.message);
|
|
129
|
+
}
|
|
130
|
+
process.stdout.write(`francois: about to delete ${target}\n`);
|
|
131
|
+
if (!args.includes('--yes') && !confirmSync('remove this extension? [y/N] ')) {
|
|
132
|
+
process.stdout.write('francois: cancelled.\n');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
extensions.removeExtension(id);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
die(error.message);
|
|
139
|
+
}
|
|
140
|
+
process.stdout.write(`francois: removed "${id}".\n`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
die(`unknown "francois ext ${sub}" — see \`francois ext --help\`.`);
|
|
145
|
+
}
|
|
146
|
+
|
|
38
147
|
function die(message) {
|
|
39
148
|
process.stderr.write(`\nfrancois: ${message}\n\n`);
|
|
40
149
|
process.exit(1);
|
|
@@ -79,6 +188,13 @@ function main() {
|
|
|
79
188
|
return;
|
|
80
189
|
}
|
|
81
190
|
|
|
191
|
+
// extension-install FR-27: filesystem verbs, off the socket — work with the
|
|
192
|
+
// app closed, and never reach the app's dispatch below.
|
|
193
|
+
if (argv[0] === 'ext') {
|
|
194
|
+
runExt(argv.slice(1));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
82
198
|
// Re-runnable on demand: a shortcut can be deleted, or the postinstall can have
|
|
83
199
|
// run somewhere the desktop wasn't reachable (SSH, a container, a CI image).
|
|
84
200
|
if (argv[0] === 'shortcut') {
|
|
Binary file
|
package/manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repo": "antoine-gmnz/francois",
|
|
3
|
-
"tag": "v0.
|
|
3
|
+
"tag": "v0.19.0",
|
|
4
4
|
"channel": "stable",
|
|
5
|
-
"appVersion": "0.
|
|
5
|
+
"appVersion": "0.19.0",
|
|
6
6
|
"productName": "Francois",
|
|
7
7
|
"assets": {
|
|
8
|
-
"darwin-universal": { "name": "francois-darwin-universal.tar.gz", "sha256": "
|
|
9
|
-
"linux-x64": { "name": "francois-linux-x64.tar.gz", "sha256": "
|
|
10
|
-
"win32-x64": { "name": "francois-win32-x64.zip", "sha256": "
|
|
8
|
+
"darwin-universal": { "name": "francois-darwin-universal.tar.gz", "sha256": "ab291ffde31a9b86790d6d478be32081ef40cdafd1e74d8234f5431b36726cf3" },
|
|
9
|
+
"linux-x64": { "name": "francois-linux-x64.tar.gz", "sha256": "f991296c452aae7a3217be955845b15f6350ee3734b8d53cb8e2d2d2045b3c4c" },
|
|
10
|
+
"win32-x64": { "name": "francois-win32-x64.zip", "sha256": "45b6b69bcd720a284c167c4341546b31edc0cd22563e3110a16057a4e2ed3af0" }
|
|
11
11
|
}
|
|
12
12
|
}
|
package/package.json
CHANGED