@geekity/cms 0.24.0 → 0.26.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/README.md +96 -160
- package/admin/pages/documents/editor.njk +48 -0
- package/admin/pages/federation/settings.njk +1 -59
- package/admin/pages/plugins/installed.njk +113 -0
- package/admin/pages/plugins/screen.njk +129 -0
- package/admin/static/admin.css +144 -4
- package/admin/static/editor-actions.js +202 -0
- package/dist/admin/accounts.d.ts +18 -39
- package/dist/admin/accounts.d.ts.map +1 -1
- package/dist/admin/accounts.js +19 -48
- package/dist/admin/accounts.js.map +1 -1
- package/dist/admin/documents.d.ts +5 -0
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +17 -8
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/editor-actions.d.ts +35 -0
- package/dist/admin/editor-actions.d.ts.map +1 -0
- package/dist/admin/editor-actions.js +187 -0
- package/dist/admin/editor-actions.js.map +1 -0
- package/dist/admin/federation.d.ts +4 -5
- package/dist/admin/federation.d.ts.map +1 -1
- package/dist/admin/federation.js +4 -5
- package/dist/admin/federation.js.map +1 -1
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/menu.d.ts +1 -1
- package/dist/admin/menu.d.ts.map +1 -1
- package/dist/admin/menu.js +10 -3
- package/dist/admin/menu.js.map +1 -1
- package/dist/admin/plugins.d.ts +24 -0
- package/dist/admin/plugins.d.ts.map +1 -0
- package/dist/admin/plugins.js +312 -0
- package/dist/admin/plugins.js.map +1 -0
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +5 -1
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/settings-federation.d.ts +0 -25
- package/dist/admin/settings-federation.d.ts.map +1 -1
- package/dist/admin/settings-federation.js +1 -54
- package/dist/admin/settings-federation.js.map +1 -1
- package/dist/admin/settings.d.ts +2 -14
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +1 -21
- package/dist/admin/settings.js.map +1 -1
- package/dist/admin/store.d.ts +8 -1
- package/dist/admin/store.d.ts.map +1 -1
- package/dist/admin/store.js +7 -3
- package/dist/admin/store.js.map +1 -1
- package/dist/admin/templates.d.ts +2 -0
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +2 -0
- package/dist/admin/templates.js.map +1 -1
- package/dist/cli.d.ts +2 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +278 -212
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/content/store.d.ts +6 -0
- package/dist/content/store.d.ts.map +1 -1
- package/dist/content/store.js +3 -0
- package/dist/content/store.js.map +1 -1
- package/dist/drain.d.ts +28 -0
- package/dist/drain.d.ts.map +1 -0
- package/dist/drain.js +106 -0
- package/dist/drain.js.map +1 -0
- package/dist/env.d.ts +11 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/federation.d.ts +4 -2
- package/dist/federation/federation.d.ts.map +1 -1
- package/dist/federation/federation.js +16 -21
- package/dist/federation/federation.js.map +1 -1
- package/dist/federation/inbox.d.ts +23 -3
- package/dist/federation/inbox.d.ts.map +1 -1
- package/dist/federation/inbox.js +63 -1
- package/dist/federation/inbox.js.map +1 -1
- package/dist/federation/index.d.ts +0 -4
- package/dist/federation/index.d.ts.map +1 -1
- package/dist/federation/index.js +0 -2
- package/dist/federation/index.js.map +1 -1
- package/dist/federation/mount.d.ts +6 -12
- package/dist/federation/mount.d.ts.map +1 -1
- package/dist/federation/mount.js +3 -50
- package/dist/federation/mount.js.map +1 -1
- package/dist/federation/queue.d.ts +22 -0
- package/dist/federation/queue.d.ts.map +1 -0
- package/dist/federation/queue.js +58 -0
- package/dist/federation/queue.js.map +1 -0
- package/dist/files/atomic.d.ts +13 -0
- package/dist/files/atomic.d.ts.map +1 -1
- package/dist/files/atomic.js +45 -0
- package/dist/files/atomic.js.map +1 -1
- package/dist/index.d.ts +30 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +156 -79
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +524 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +23 -0
- package/dist/plugin.js.map +1 -0
- package/dist/plugins/enabled.d.ts +23 -0
- package/dist/plugins/enabled.d.ts.map +1 -0
- package/dist/plugins/enabled.js +49 -0
- package/dist/plugins/enabled.js.map +1 -0
- package/dist/plugins/federation.d.ts +26 -0
- package/dist/plugins/federation.d.ts.map +1 -0
- package/dist/plugins/federation.js +114 -0
- package/dist/plugins/federation.js.map +1 -0
- package/dist/plugins/fetch.d.ts +15 -0
- package/dist/plugins/fetch.d.ts.map +1 -0
- package/dist/plugins/fetch.js +33 -0
- package/dist/plugins/fetch.js.map +1 -0
- package/dist/plugins/folder.d.ts +50 -0
- package/dist/plugins/folder.d.ts.map +1 -0
- package/dist/plugins/folder.js +171 -0
- package/dist/plugins/folder.js.map +1 -0
- package/dist/plugins/install.d.ts +45 -0
- package/dist/plugins/install.d.ts.map +1 -0
- package/dist/plugins/install.js +275 -0
- package/dist/plugins/install.js.map +1 -0
- package/dist/plugins/mount.d.ts +17 -0
- package/dist/plugins/mount.d.ts.map +1 -0
- package/dist/plugins/mount.js +44 -0
- package/dist/plugins/mount.js.map +1 -0
- package/dist/plugins/registry.d.ts +110 -0
- package/dist/plugins/registry.d.ts.map +1 -0
- package/dist/plugins/registry.js +447 -0
- package/dist/plugins/registry.js.map +1 -0
- package/dist/plugins/settings.d.ts +64 -0
- package/dist/plugins/settings.d.ts.map +1 -0
- package/dist/plugins/settings.js +219 -0
- package/dist/plugins/settings.js.map +1 -0
- package/dist/plugins/site.d.ts +17 -0
- package/dist/plugins/site.d.ts.map +1 -0
- package/dist/plugins/site.js +67 -0
- package/dist/plugins/site.js.map +1 -0
- package/dist/supervisor/primary.d.ts +47 -0
- package/dist/supervisor/primary.d.ts.map +1 -0
- package/dist/supervisor/primary.js +208 -0
- package/dist/supervisor/primary.js.map +1 -0
- package/dist/supervisor/protocol.d.ts +37 -0
- package/dist/supervisor/protocol.d.ts.map +1 -0
- package/dist/supervisor/protocol.js +29 -0
- package/dist/supervisor/protocol.js.map +1 -0
- package/dist/supervisor/supervision.d.ts +25 -0
- package/dist/supervisor/supervision.d.ts.map +1 -0
- package/dist/supervisor/supervision.js +7 -0
- package/dist/supervisor/supervision.js.map +1 -0
- package/dist/supervisor/worker.d.ts +29 -0
- package/dist/supervisor/worker.d.ts.map +1 -0
- package/dist/supervisor/worker.js +97 -0
- package/dist/supervisor/worker.js.map +1 -0
- package/dist/web/maintenance.d.ts +2 -2
- package/dist/web/maintenance.js +2 -2
- package/dist/web/negotiate.d.ts +4 -3
- package/dist/web/negotiate.d.ts.map +1 -1
- package/dist/web/negotiate.js +4 -3
- package/dist/web/negotiate.js.map +1 -1
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +33 -14
- package/dist/web/routes.js.map +1 -1
- package/package.json +10 -1
- package/dist/comments/conversation.d.ts +0 -29
- package/dist/comments/conversation.d.ts.map +0 -1
- package/dist/comments/conversation.js +0 -60
- package/dist/comments/conversation.js.map +0 -1
- package/dist/federation/import-wordpress.d.ts +0 -128
- package/dist/federation/import-wordpress.d.ts.map +0 -1
- package/dist/federation/import-wordpress.js +0 -426
- package/dist/federation/import-wordpress.js.map +0 -1
- package/dist/federation/wordpress.d.ts +0 -131
- package/dist/federation/wordpress.d.ts.map +0 -1
- package/dist/federation/wordpress.js +0 -342
- package/dist/federation/wordpress.js.map +0 -1
- package/dist/web/comments.d.ts +0 -60
- package/dist/web/comments.d.ts.map +0 -1
- package/dist/web/comments.js +0 -207
- package/dist/web/comments.js.map +0 -1
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import type { GeekityConfig } from './config.ts';
|
|
3
|
-
export type Command = 'serve' | 'init' | 'sync' | 'rebuild' | 'resend' | 'user' | '
|
|
3
|
+
export type Command = 'serve' | 'init' | 'sync' | 'rebuild' | 'resend' | 'user' | 'maintenance' | 'strip-metadata' | 'plugin' | 'help' | 'version';
|
|
4
4
|
export interface ParsedArgs {
|
|
5
5
|
command: Command;
|
|
6
6
|
/** Path to the config file, relative to the working directory, if the user named one. */
|
|
@@ -24,8 +24,7 @@ export interface ParsedArgs {
|
|
|
24
24
|
flags: Readonly<Record<string, string | true>>;
|
|
25
25
|
/**
|
|
26
26
|
* Positional arguments after the command: the directory for `init`, the
|
|
27
|
-
* subcommand and its arguments for `user
|
|
28
|
-
* here.
|
|
27
|
+
* subcommand and its arguments for `user`. Flags are never in here.
|
|
29
28
|
*/
|
|
30
29
|
args: readonly string[];
|
|
31
30
|
}
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAoBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAsBjD,MAAM,MAAM,OAAO,GACf,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,GACT,QAAQ,GACR,MAAM,GACN,aAAa,GACb,gBAAgB,GAChB,QAAQ,GACR,MAAM,GACN,SAAS,CAAC;AAwBd,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,yFAAyF;IACzF,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B;;;OAGG;IACH,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IAC/C;;;OAGG;IACH,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CACzB;AAwED,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CAsH7D;AASD;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAAC,aAAa,CAAC,CAyBxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAW3D;AAED;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe7F;AAwwBD,gEAAgE;AAChE,KAAK,aAAa,GAAG,MAAM,CAAC,cAAc,GAAG;IAC3C,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC;CACzC,CAAC;AAEF,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,4FAA4F;IAC5F,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CA4CrF"}
|
package/dist/cli.js
CHANGED
|
@@ -7,8 +7,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
7
7
|
}
|
|
8
8
|
return path;
|
|
9
9
|
};
|
|
10
|
+
import cluster from 'node:cluster';
|
|
10
11
|
import { existsSync, realpathSync } from 'node:fs';
|
|
11
|
-
import { readFile } from 'node:fs/promises';
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { createInterface } from 'node:readline';
|
|
@@ -22,9 +22,13 @@ import { databaseFile, discardDatabase } from './cache.js';
|
|
|
22
22
|
import { resolveConfig } from './config.js';
|
|
23
23
|
import { stripStoredUploads } from './content/metadata/sweep.js';
|
|
24
24
|
import { UPLOAD_DIRECTORY } from './web/assets.js';
|
|
25
|
-
import { importWordPressActor } from './federation/import-wordpress.js';
|
|
26
25
|
import { createCms } from './index.js';
|
|
27
26
|
import { initSite, ownManifest, seedStarterContent } from './init.js';
|
|
27
|
+
import { importPluginFolders, scanPluginFolders } from './plugins/folder.js';
|
|
28
|
+
import { addPlugin, DEFAULT_REGISTRY, installedFolders, parsePackageSpec, removePlugin, requirementNotes, } from './plugins/install.js';
|
|
29
|
+
import { pluginSite, sitePluginRegistry } from './plugins/site.js';
|
|
30
|
+
import { superviseCluster } from './supervisor/primary.js';
|
|
31
|
+
import { processChannel, superviseWorker } from './supervisor/worker.js';
|
|
28
32
|
import { enterMaintenance, leaveMaintenance, maintenanceFile, readMaintenance, } from './maintenance.js';
|
|
29
33
|
/** The commands a site can name on the command line, as opposed to the flags. */
|
|
30
34
|
const COMMANDS = [
|
|
@@ -34,29 +38,17 @@ const COMMANDS = [
|
|
|
34
38
|
'rebuild',
|
|
35
39
|
'resend',
|
|
36
40
|
'user',
|
|
37
|
-
'import',
|
|
38
41
|
'maintenance',
|
|
39
42
|
'strip-metadata',
|
|
43
|
+
'plugin',
|
|
40
44
|
];
|
|
41
45
|
/**
|
|
42
46
|
* The options one command reads out of {@link ParsedArgs.flags}, each of which
|
|
43
47
|
* carries a value.
|
|
44
|
-
*
|
|
45
|
-
* A table rather than a branch per flag, because `import wordpress-actor`
|
|
46
|
-
* takes six of them and a parser with six more near-identical cases in it
|
|
47
|
-
* would be six more places to get the same thing subtly wrong.
|
|
48
48
|
*/
|
|
49
|
-
const VALUE_FLAGS = [
|
|
50
|
-
'actor-id',
|
|
51
|
-
'wordpress-id',
|
|
52
|
-
'private-key',
|
|
53
|
-
'public-key',
|
|
54
|
-
'keypair',
|
|
55
|
-
'followers',
|
|
56
|
-
'until',
|
|
57
|
-
];
|
|
49
|
+
const VALUE_FLAGS = ['until'];
|
|
58
50
|
/** The options that are simply on or off. */
|
|
59
|
-
const SWITCH_FLAGS = ['
|
|
51
|
+
const SWITCH_FLAGS = ['all'];
|
|
60
52
|
const CONFIG_FILENAMES = ['geekity.config.ts', 'geekity.config.js', 'geekity.config.mjs'];
|
|
61
53
|
const USAGE = `geekity — the Geekity CMS command line
|
|
62
54
|
|
|
@@ -69,9 +61,7 @@ Usage:
|
|
|
69
61
|
geekity maintenance (on [--until <time>] | off | status) [--config <file>]
|
|
70
62
|
geekity strip-metadata [--config <file>]
|
|
71
63
|
geekity user add <username> [--password <pw>] [--email <address>] [--config <file>]
|
|
72
|
-
geekity
|
|
73
|
-
(--keypair <file> | --private-key <file> [--public-key <file>])
|
|
74
|
-
[--followers <url|file|none>] [--force] [--config <file>]
|
|
64
|
+
geekity plugin (add <package>[@version] | remove <package>) [--config <file>]
|
|
75
65
|
|
|
76
66
|
Commands:
|
|
77
67
|
serve Start the CMS (the default when no command is given).
|
|
@@ -97,11 +87,13 @@ Commands:
|
|
|
97
87
|
as it is.
|
|
98
88
|
user add Create an admin user, so a site can get its first login
|
|
99
89
|
without the setup screen.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
90
|
+
plugin add Install a plugin package's bundle from the npm registry
|
|
91
|
+
(npm_config_registry, or registry.npmjs.org) into the
|
|
92
|
+
plugins folder, GEEKITY_PLUGINS_DIR. It installs only the
|
|
93
|
+
package named, and names any plugin it requires that is
|
|
94
|
+
missing. Reload on the Plugins screen loads it.
|
|
95
|
+
plugin remove Delete a plugin's folder from the plugins folder. Reload on
|
|
96
|
+
the Plugins screen unloads it.
|
|
105
97
|
|
|
106
98
|
Options:
|
|
107
99
|
--config <file> Config file to load. Defaults to the first of
|
|
@@ -112,36 +104,17 @@ Options:
|
|
|
112
104
|
list and in shell history, so prefer being asked.
|
|
113
105
|
--email <addr> The new user's email address. Optional; it is what a
|
|
114
106
|
forgotten password is recovered through.
|
|
115
|
-
--actor-id <url> The actor id WordPress published, query string and all,
|
|
116
|
-
for example https://example.com/?author=2
|
|
117
|
-
--wordpress-id <n>
|
|
118
|
-
The WordPress user id, which is the number in the plugin's
|
|
119
|
-
paths (/wp-json/activitypub/1.0/actors/<n>/inbox).
|
|
120
|
-
--keypair <file> The JSON \`wp option get activitypub_keypair_for_<login>
|
|
121
|
-
--format=json\` prints: {"private_key": …, "public_key": …}.
|
|
122
|
-
--private-key <file>
|
|
123
|
-
The private key as PEM, instead of --keypair.
|
|
124
|
-
--public-key <file>
|
|
125
|
-
The public key as PEM. Optional, and only checked against
|
|
126
|
-
the private half — it is derived, never stored.
|
|
127
|
-
--followers <url|file|none>
|
|
128
|
-
Where the followers come from. Left off, the plugin's own
|
|
129
|
-
public followers collection on the actor id's origin.
|
|
130
107
|
--until <time> With maintenance on: when the site should be back, as an
|
|
131
108
|
ISO 8601 time such as 2026-09-28T14:00:00Z. It is the
|
|
132
109
|
Retry-After a client is sent, and the maintenance page
|
|
133
110
|
says it.
|
|
134
111
|
--all With resend: every announced post rather than named ones.
|
|
135
|
-
--force Import over a key pair the user already has. Do this only
|
|
136
|
-
when you are certain the pair being imported is the one the
|
|
137
|
-
followers hold: a new key means none of them can verify
|
|
138
|
-
this person again.
|
|
139
112
|
-h, --help Show this help.
|
|
140
113
|
-v, --version Show the installed version.
|
|
141
114
|
|
|
142
115
|
Environment overrides:
|
|
143
116
|
GEEKITY_PORT (or PORT), GEEKITY_CONTENT_DIR, GEEKITY_DATA_DIR,
|
|
144
|
-
GEEKITY_THEMES_DIR, GEEKITY_BASE_URL, GEEKITY_WATCH,
|
|
117
|
+
GEEKITY_THEMES_DIR, GEEKITY_PLUGINS_DIR, GEEKITY_BASE_URL, GEEKITY_WATCH,
|
|
145
118
|
GEEKITY_MAINTENANCE (on keeps the site in maintenance until a restart)
|
|
146
119
|
`;
|
|
147
120
|
/** Turn `process.argv.slice(2)` into a command, its options and its arguments. */
|
|
@@ -354,9 +327,13 @@ function isModuleNotFound(error, candidate) {
|
|
|
354
327
|
}
|
|
355
328
|
/** Run one command. Resolves with the exit code the process should use. */
|
|
356
329
|
async function main(argv) {
|
|
330
|
+
const first = leadingWords(argv)[0];
|
|
331
|
+
if (first !== undefined && !isCommand(first) && !argv.some(isHelpOrVersion)) {
|
|
332
|
+
return await pluginCommand(argv);
|
|
333
|
+
}
|
|
357
334
|
const { command, configPath, password, email, flags, args } = parseArgs(argv);
|
|
358
335
|
if (command === 'help') {
|
|
359
|
-
process.stdout.write(
|
|
336
|
+
process.stdout.write(await usage(configPath));
|
|
360
337
|
return 0;
|
|
361
338
|
}
|
|
362
339
|
if (command === 'version') {
|
|
@@ -367,8 +344,6 @@ async function main(argv) {
|
|
|
367
344
|
return init(args);
|
|
368
345
|
if (command === 'user')
|
|
369
346
|
return userCommand(args, configPath, password, email);
|
|
370
|
-
if (command === 'import')
|
|
371
|
-
return importCommand(args, configPath, flags);
|
|
372
347
|
if (command === 'sync')
|
|
373
348
|
return syncCommand(configPath);
|
|
374
349
|
if (command === 'rebuild')
|
|
@@ -379,6 +354,8 @@ async function main(argv) {
|
|
|
379
354
|
return maintenanceCommand(args, configPath, flags);
|
|
380
355
|
if (command === 'strip-metadata')
|
|
381
356
|
return stripMetadataCommand(configPath);
|
|
357
|
+
if (command === 'plugin')
|
|
358
|
+
return pluginFolderCommand(args, configPath);
|
|
382
359
|
return serveCommand(configPath);
|
|
383
360
|
}
|
|
384
361
|
/** `geekity init <directory>`: scaffold a new site. */
|
|
@@ -649,33 +626,105 @@ const SQLITE_LOCKED = 6;
|
|
|
649
626
|
/**
|
|
650
627
|
* `geekity serve`: the default. Runs until it is signalled.
|
|
651
628
|
*
|
|
629
|
+
* This process is a supervisor (TASK-288): it owns the port and runs the CMS
|
|
630
|
+
* in a `node:cluster` worker that runs this same command, so Reload on the
|
|
631
|
+
* Plugins screen can boot a new worker beside the old one and a worker that
|
|
632
|
+
* crashes is respawned, both without restarting the container.
|
|
633
|
+
*
|
|
652
634
|
* With `seedContent` on, a missing or empty content directory is given the
|
|
653
|
-
* starter site before
|
|
654
|
-
* show and the setup screen. It happens here rather than in `createCms` so
|
|
655
|
-
* site that builds its own server never has content written for it.
|
|
635
|
+
* starter site before any worker opens it, so a new box boots into something
|
|
636
|
+
* to show and the setup screen. It happens here rather than in `createCms` so
|
|
637
|
+
* a site that builds its own server never has content written for it.
|
|
656
638
|
*/
|
|
657
639
|
async function serveCommand(configPath) {
|
|
658
|
-
|
|
659
|
-
|
|
640
|
+
if (cluster.isWorker)
|
|
641
|
+
return serveWorker(configPath);
|
|
642
|
+
const resolved = resolveConfig(await loadConfig(process.cwd(), configPath));
|
|
660
643
|
if (resolved.seedContent &&
|
|
661
644
|
(await seedStarterContent({ contentDir: resolved.contentDir, baseUrl: resolved.baseUrl }))) {
|
|
662
645
|
process.stdout.write(`Seeded ${resolved.contentDir} with the starter site\n`);
|
|
663
646
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
647
|
+
superviseCluster();
|
|
648
|
+
return 0;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* One worker under the supervisor: the site's config and its plugins folder,
|
|
652
|
+
* read afresh, so a reload sees what changed. Signals are the supervisor's to
|
|
653
|
+
* act on; it tells this worker when to close.
|
|
654
|
+
*/
|
|
655
|
+
async function serveWorker(configPath) {
|
|
656
|
+
const channel = processChannel();
|
|
657
|
+
for (const signal of ['SIGINT', 'SIGTERM'])
|
|
658
|
+
process.on(signal, () => undefined);
|
|
659
|
+
try {
|
|
660
|
+
const config = await loadConfig(process.cwd(), configPath);
|
|
661
|
+
const loaded = scanPluginFolders(resolveConfig(config).pluginsDir);
|
|
662
|
+
const worker = superviseWorker({
|
|
663
|
+
channel,
|
|
664
|
+
loaded,
|
|
665
|
+
exit: (code) => process.exit(code),
|
|
666
|
+
log: (line) => process.stderr.write(`${line}\n`),
|
|
675
667
|
});
|
|
668
|
+
// The access log is on here and off in `createCms`: a server that answers
|
|
669
|
+
// the internet should be able to say what it answered, while a CMS embedded
|
|
670
|
+
// in somebody else's app has no business writing to their stdout uninvited.
|
|
671
|
+
// Named as a config value rather than forced, so `GEEKITY_ACCESS_LOG` still
|
|
672
|
+
// wins and a site that wrote `accessLog: false` still gets silence.
|
|
673
|
+
const cms = createCms({ ...config, accessLog: config.accessLog ?? true }, { folderPlugins: await importPluginFolders(loaded), supervision: worker.supervision });
|
|
674
|
+
const { port } = await cms.serve();
|
|
675
|
+
process.stdout.write(`Geekity is serving ${cms.config.baseUrl} on port ${String(port)}\n`);
|
|
676
|
+
worker.serving(cms);
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
680
|
+
channel.send({ type: 'boot-failed', error: message }, () => process.exit(1));
|
|
681
|
+
}
|
|
682
|
+
return 0;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* `geekity plugin add <package>` and `geekity plugin remove <package>`
|
|
686
|
+
* (decision-33): change the plugins folder, which Reload on the Plugins
|
|
687
|
+
* screen then loads. Nothing a plugin requires is installed for it.
|
|
688
|
+
*/
|
|
689
|
+
async function pluginFolderCommand(args, configPath) {
|
|
690
|
+
const [action, spec, ...rest] = args;
|
|
691
|
+
if ((action !== 'add' && action !== 'remove') || spec === undefined || rest.length > 0) {
|
|
692
|
+
throw new Error('geekity plugin needs add <package>[@version] or remove <package>.');
|
|
693
|
+
}
|
|
694
|
+
const config = resolveConfig(await loadConfig(process.cwd(), configPath));
|
|
695
|
+
const { pluginsDir } = config;
|
|
696
|
+
if (pluginsDir === undefined) {
|
|
697
|
+
throw new Error('geekity plugin needs a plugins folder: set GEEKITY_PLUGINS_DIR, or pluginsDir in the config.');
|
|
698
|
+
}
|
|
699
|
+
if (action === 'remove') {
|
|
700
|
+
const directory = await removePlugin(spec, pluginsDir);
|
|
701
|
+
process.stdout.write(`Removed ${spec} (${directory}). Reload on the Plugins screen to unload it.\n`);
|
|
702
|
+
return 0;
|
|
676
703
|
}
|
|
704
|
+
const registry = firstSet(process.env['npm_config_registry'], process.env['NPM_CONFIG_REGISTRY']) ??
|
|
705
|
+
DEFAULT_REGISTRY;
|
|
706
|
+
const { manifest, directory, replaced } = await addPlugin(parsePackageSpec(spec), {
|
|
707
|
+
pluginsDir,
|
|
708
|
+
registry,
|
|
709
|
+
});
|
|
710
|
+
const installed = new Map([
|
|
711
|
+
...config.plugins.map((plugin) => [plugin.name, plugin.version]),
|
|
712
|
+
...installedFolders(pluginsDir),
|
|
713
|
+
]);
|
|
714
|
+
const done = replaced === undefined || replaced === manifest.version
|
|
715
|
+
? `Added ${manifest.name} ${manifest.version} to ${directory}.`
|
|
716
|
+
: `Replaced ${manifest.name} ${replaced} with ${manifest.version} in ${directory}.`;
|
|
717
|
+
process.stdout.write([
|
|
718
|
+
done,
|
|
719
|
+
...requirementNotes(manifest, installed, ownManifest().version),
|
|
720
|
+
'Reload on the Plugins screen to load it, then enable it there.',
|
|
721
|
+
'',
|
|
722
|
+
].join('\n'));
|
|
677
723
|
return 0;
|
|
678
724
|
}
|
|
725
|
+
function firstSet(...values) {
|
|
726
|
+
return values.find((value) => value !== undefined && value !== '');
|
|
727
|
+
}
|
|
679
728
|
/**
|
|
680
729
|
* `geekity user add <username>`: create an admin without the setup screen.
|
|
681
730
|
*
|
|
@@ -731,174 +780,191 @@ async function userCommand(args, configPath, flagPassword, flagEmail) {
|
|
|
731
780
|
}
|
|
732
781
|
/** Where the success line points a new admin. Relative, because the host is the site's. */
|
|
733
782
|
const LOGIN_URL = '/admin/login';
|
|
783
|
+
/** One flag as the text it carries, or `undefined` when it was not given. */
|
|
784
|
+
function text(flags, name) {
|
|
785
|
+
const value = flags[name];
|
|
786
|
+
return typeof value === 'string' ? value : undefined;
|
|
787
|
+
}
|
|
788
|
+
function isHelpOrVersion(arg) {
|
|
789
|
+
return arg === '--help' || arg === '-h' || arg === '--version' || arg === '-v';
|
|
790
|
+
}
|
|
791
|
+
/** The bare words before the first option other than `--config`. */
|
|
792
|
+
function leadingWords(argv) {
|
|
793
|
+
const words = [];
|
|
794
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
795
|
+
const arg = argv[i];
|
|
796
|
+
if (arg === '--config') {
|
|
797
|
+
i += 1;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (arg.startsWith('--config='))
|
|
801
|
+
continue;
|
|
802
|
+
if (arg.startsWith('-'))
|
|
803
|
+
break;
|
|
804
|
+
words.push(arg);
|
|
805
|
+
}
|
|
806
|
+
return words;
|
|
807
|
+
}
|
|
808
|
+
/** `--config <file>` or `--config=<file>`, wherever it is. */
|
|
809
|
+
function configFlag(argv) {
|
|
810
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
811
|
+
const arg = argv[i];
|
|
812
|
+
if (arg === '--config')
|
|
813
|
+
return argv[i + 1];
|
|
814
|
+
if (arg.startsWith('--config='))
|
|
815
|
+
return arg.slice('--config='.length);
|
|
816
|
+
}
|
|
817
|
+
return undefined;
|
|
818
|
+
}
|
|
734
819
|
/**
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
820
|
+
* Every command the site's available plugins add, from its config and its
|
|
821
|
+
* plugins folder, longest words first.
|
|
822
|
+
*/
|
|
823
|
+
async function pluginCommands(config) {
|
|
824
|
+
const folderPlugins = await importPluginFolders(scanPluginFolders(config.pluginsDir));
|
|
825
|
+
return sitePluginRegistry(config, folderPlugins)
|
|
826
|
+
.plugins.filter((entry) => entry.problem === undefined)
|
|
827
|
+
.flatMap((entry) => entry.commands.map((command) => ({ plugin: entry.plugin.name, command })))
|
|
828
|
+
.sort((a, b) => b.command.words.length - a.command.words.length);
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Run a command a plugin added (decision-33). It needs no enabling: a
|
|
832
|
+
* migration command runs before the plugin it prepares for is turned on.
|
|
744
833
|
*
|
|
745
|
-
* The database is opened for the reason `user add` opens it:
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
* or the import would write into a file holding nobody.
|
|
834
|
+
* The database is opened for the reason `user add` opens it: a command can
|
|
835
|
+
* reach `data/users.json` before any server has, so a site upgrading from the
|
|
836
|
+
* version that kept its accounts in SQLite is migrated first.
|
|
749
837
|
*/
|
|
750
|
-
async function
|
|
751
|
-
const
|
|
752
|
-
if (subcommand === undefined) {
|
|
753
|
-
throw new Error('geekity import <what> needs a subcommand. The only one is: wordpress-actor.');
|
|
754
|
-
}
|
|
755
|
-
if (subcommand !== 'wordpress-actor') {
|
|
756
|
-
throw new Error(`Unknown import subcommand "${subcommand}". The only one is: wordpress-actor.`);
|
|
757
|
-
}
|
|
758
|
-
const username = args[1];
|
|
759
|
-
if (username === undefined) {
|
|
760
|
-
throw new Error('geekity import wordpress-actor <username> needs the account on this site the ' +
|
|
761
|
-
"plugin's actor becomes.");
|
|
762
|
-
}
|
|
763
|
-
const actorId = text(flags, 'actor-id');
|
|
764
|
-
if (actorId === undefined) {
|
|
765
|
-
throw new Error('--actor-id is the id WordPress published this person under, query string and all, ' +
|
|
766
|
-
'for example --actor-id "https://example.com/?author=2". Its followers key the ' +
|
|
767
|
-
'account by it, so it is the one thing the import cannot work out for itself.');
|
|
768
|
-
}
|
|
769
|
-
if (!isAbsoluteHttpUrl(actorId)) {
|
|
770
|
-
throw new Error(`--actor-id must be an http or https URL; "${actorId}" is not one.`);
|
|
771
|
-
}
|
|
772
|
-
const number = text(flags, 'wordpress-id');
|
|
773
|
-
if (number === undefined || !/^[1-9][0-9]*$/.test(number)) {
|
|
774
|
-
throw new Error('--wordpress-id is the WordPress user id, a whole positive number: it is what the ' +
|
|
775
|
-
"plugin's paths are built from, as in /wp-json/activitypub/1.0/actors/2/inbox.");
|
|
776
|
-
}
|
|
777
|
-
const { privateKeyPem, publicKeyPem } = await readKeyPair(flags);
|
|
838
|
+
async function pluginCommand(argv) {
|
|
839
|
+
const configPath = configFlag(argv);
|
|
778
840
|
const config = resolveConfig(await loadConfig(process.cwd(), configPath));
|
|
841
|
+
const words = leadingWords(argv);
|
|
842
|
+
const found = (await pluginCommands(config)).find(({ command }) => command.words.every((word, index) => words[index] === word));
|
|
843
|
+
if (found === undefined) {
|
|
844
|
+
throw new Error(`Unknown command "${words[0] ?? ''}". Run geekity --help to see what is available.`);
|
|
845
|
+
}
|
|
846
|
+
const { args, options } = parseCommandArgs(withoutConfig(argv).slice(found.command.words.length), found.command);
|
|
779
847
|
const admin = openAdminStore({ dataDir: config.dataDir });
|
|
780
848
|
migrateUsersToFile({ admin, dataDir: config.dataDir });
|
|
781
849
|
try {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
wordpressActorId: Number(number),
|
|
789
|
-
privateKeyPem,
|
|
790
|
-
publicKeyPem,
|
|
791
|
-
followers: followersSource(flags),
|
|
792
|
-
force: flags['force'] === true,
|
|
850
|
+
return await found.command.run({
|
|
851
|
+
args,
|
|
852
|
+
options,
|
|
853
|
+
cwd: process.cwd(),
|
|
854
|
+
site: pluginSite({ admin, config }),
|
|
855
|
+
write: (output) => process.stdout.write(output),
|
|
793
856
|
});
|
|
794
|
-
process.stdout.write(importSummary(report));
|
|
795
|
-
return 0;
|
|
796
857
|
}
|
|
797
858
|
finally {
|
|
798
859
|
admin.close();
|
|
799
860
|
}
|
|
800
861
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
*/
|
|
809
|
-
async function readKeyPair(flags) {
|
|
810
|
-
const keypair = text(flags, 'keypair');
|
|
811
|
-
const privateKey = text(flags, 'private-key');
|
|
812
|
-
const publicKey = text(flags, 'public-key');
|
|
813
|
-
if (keypair !== undefined) {
|
|
814
|
-
if (privateKey !== undefined) {
|
|
815
|
-
throw new Error('Pass either --keypair or --private-key, not both.');
|
|
816
|
-
}
|
|
817
|
-
let parsed;
|
|
818
|
-
try {
|
|
819
|
-
parsed = JSON.parse(await readFile(path.resolve(process.cwd(), keypair), 'utf8'));
|
|
862
|
+
function withoutConfig(argv) {
|
|
863
|
+
const kept = [];
|
|
864
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
865
|
+
const arg = argv[i];
|
|
866
|
+
if (arg === '--config') {
|
|
867
|
+
i += 1;
|
|
868
|
+
continue;
|
|
820
869
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
870
|
+
if (!arg.startsWith('--config='))
|
|
871
|
+
kept.push(arg);
|
|
872
|
+
}
|
|
873
|
+
return kept;
|
|
874
|
+
}
|
|
875
|
+
/** A plugin command's arguments and options, refusing any option it does not take. */
|
|
876
|
+
function parseCommandArgs(argv, command) {
|
|
877
|
+
const args = [];
|
|
878
|
+
const options = {};
|
|
879
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
880
|
+
const arg = argv[i];
|
|
881
|
+
if (!arg.startsWith('--')) {
|
|
882
|
+
args.push(arg);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
const equals = arg.indexOf('=');
|
|
886
|
+
const name = equals === -1 ? arg.slice(2) : arg.slice(2, equals);
|
|
887
|
+
const inline = equals === -1 ? undefined : arg.slice(equals + 1);
|
|
888
|
+
const option = command.options?.find((entry) => entry.name === name);
|
|
889
|
+
if (option === undefined) {
|
|
890
|
+
throw new Error(`Unknown option "--${name}". Run geekity --help to see what is available.`);
|
|
891
|
+
}
|
|
892
|
+
if (option.value === undefined) {
|
|
893
|
+
if (inline !== undefined)
|
|
894
|
+
throw new Error(`--${name} takes no value.`);
|
|
895
|
+
options[name] = true;
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
if (inline !== undefined) {
|
|
899
|
+
if (inline === '')
|
|
900
|
+
throw new Error(`--${name} needs a value.`);
|
|
901
|
+
options[name] = inline;
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const value = argv[i + 1];
|
|
905
|
+
if (value === undefined || value.startsWith('-'))
|
|
906
|
+
throw new Error(`--${name} needs a value.`);
|
|
907
|
+
options[name] = value;
|
|
908
|
+
i += 1;
|
|
909
|
+
}
|
|
910
|
+
return { args, options };
|
|
845
911
|
}
|
|
846
912
|
/**
|
|
847
|
-
*
|
|
848
|
-
*
|
|
913
|
+
* The help, with the commands the site's plugins add. A config that will not
|
|
914
|
+
* load costs the help its plugin section, not the help itself.
|
|
849
915
|
*/
|
|
850
|
-
function
|
|
851
|
-
|
|
852
|
-
if (given === undefined)
|
|
853
|
-
return undefined;
|
|
854
|
-
if (given === 'none')
|
|
855
|
-
return false;
|
|
856
|
-
return isAbsoluteHttpUrl(given) ? given : path.resolve(process.cwd(), given);
|
|
857
|
-
}
|
|
858
|
-
/** What the run did, as the operator reads it. */
|
|
859
|
-
function importSummary(report) {
|
|
860
|
-
const lines = [];
|
|
861
|
-
const { followers } = report;
|
|
862
|
-
lines.push(report.changed
|
|
863
|
-
? `Imported ${report.username} from ${report.actorId}`
|
|
864
|
-
: `Nothing to change: ${report.username} is already ${report.actorId}`);
|
|
865
|
-
lines.push(` ids ${report.identity === 'set' ? 'written to' : 'already in'} ` +
|
|
866
|
-
`data/users.json (WordPress actor ${String(report.wordpressActorId)})`);
|
|
867
|
-
for (const key of report.keys)
|
|
868
|
-
lines.push(` key ${key.state} ${key.file}`);
|
|
869
|
-
if (followers.source === undefined) {
|
|
870
|
-
lines.push(' followers skipped');
|
|
871
|
-
}
|
|
872
|
-
else {
|
|
873
|
-
lines.push(` followers ${count(followers.added.length, 'added', 'added')}, ` +
|
|
874
|
-
`${String(followers.refreshed.length)} refreshed, ` +
|
|
875
|
-
`${String(followers.unchanged.length)} unchanged, ` +
|
|
876
|
-
`${String(followers.failed.length)} could not be fetched ` +
|
|
877
|
-
`(${followers.source})`);
|
|
878
|
-
for (const failure of followers.failed) {
|
|
879
|
-
lines.push(` skipped ${failure.actor}: ${failure.reason}`);
|
|
880
|
-
}
|
|
881
|
-
if (followers.failed.length > 0) {
|
|
882
|
-
lines.push(' Those followers were left out. Run this again once their servers answer; ' +
|
|
883
|
-
'nothing already imported is touched twice.');
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
return `${lines.join('\n')}\n`;
|
|
887
|
-
}
|
|
888
|
-
/** One flag as the text it carries, or `undefined` when it was not given. */
|
|
889
|
-
function text(flags, name) {
|
|
890
|
-
const value = flags[name];
|
|
891
|
-
return typeof value === 'string' ? value : undefined;
|
|
892
|
-
}
|
|
893
|
-
/** Whether a value is a URL a peer could fetch. */
|
|
894
|
-
function isAbsoluteHttpUrl(value) {
|
|
916
|
+
async function usage(configPath) {
|
|
917
|
+
let found;
|
|
895
918
|
try {
|
|
896
|
-
|
|
897
|
-
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
919
|
+
found = await pluginCommands(resolveConfig(await loadConfig(process.cwd(), configPath)));
|
|
898
920
|
}
|
|
899
|
-
catch {
|
|
900
|
-
|
|
921
|
+
catch (error) {
|
|
922
|
+
process.stderr.write(`Plugin commands are not listed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
923
|
+
return USAGE;
|
|
924
|
+
}
|
|
925
|
+
if (found.length === 0)
|
|
926
|
+
return USAGE;
|
|
927
|
+
const lines = ['', 'Plugin commands:'];
|
|
928
|
+
for (const { plugin, command } of found) {
|
|
929
|
+
const synopsis = wrap(`geekity ${[...command.words, command.usage].join(' ').trim()} [--config <file>]`, USAGE_CONTINUATION);
|
|
930
|
+
lines.push(` ${(synopsis[0] ?? '').trimStart()}`, ...synopsis.slice(1));
|
|
931
|
+
lines.push(...wrap(command.summary, HELP_INDENT), `${HELP_INDENT}From ${plugin}.`);
|
|
932
|
+
for (const option of command.options ?? []) {
|
|
933
|
+
const flag = `--${option.name}${option.value === undefined ? '' : ` ${option.value}`}`;
|
|
934
|
+
const described = wrap(option.description, HELP_INDENT);
|
|
935
|
+
const head = ` ${flag}`;
|
|
936
|
+
if (head.length < HELP_INDENT.length - 1 && described[0] !== undefined) {
|
|
937
|
+
described[0] = `${head.padEnd(HELP_INDENT.length)}${described[0].trimStart()}`;
|
|
938
|
+
lines.push(...described);
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
lines.push(head, ...described);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return `${USAGE}${lines.join('\n')}\n`;
|
|
946
|
+
}
|
|
947
|
+
/** Where the help's descriptions start, as the core commands' do. */
|
|
948
|
+
const HELP_INDENT = ' '.repeat(19);
|
|
949
|
+
/** Where a usage line carries on, as the core commands' do. */
|
|
950
|
+
const USAGE_CONTINUATION = ' '.repeat(10);
|
|
951
|
+
const HELP_WIDTH = 79;
|
|
952
|
+
/** Text wrapped to the help's width, each line indented. */
|
|
953
|
+
function wrap(text, indent) {
|
|
954
|
+
const lines = [];
|
|
955
|
+
let line = '';
|
|
956
|
+
for (const word of text.split(/\s+/).filter((entry) => entry !== '')) {
|
|
957
|
+
if (line !== '' && indent.length + line.length + 1 + word.length > HELP_WIDTH) {
|
|
958
|
+
lines.push(`${indent}${line}`);
|
|
959
|
+
line = word;
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
line = line === '' ? word : `${line} ${word}`;
|
|
963
|
+
}
|
|
901
964
|
}
|
|
965
|
+
if (line !== '')
|
|
966
|
+
lines.push(`${indent}${line}`);
|
|
967
|
+
return lines;
|
|
902
968
|
}
|
|
903
969
|
/**
|
|
904
970
|
* Ask for a password once, without putting it on the screen.
|