@meith/cli 0.17.0 → 0.17.2
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/community.mjs +6 -4
- package/package.json +13 -13
- package/src/board-eject.ts +142 -7
- package/src/plugin-manifest.ts +104 -39
- package/src/upgrade.ts +1 -1
package/bin/community.mjs
CHANGED
|
@@ -92,11 +92,13 @@ function materialize() {
|
|
|
92
92
|
return join(srcTarget, 'index.ts')
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Resolved from this package's own directory — `tsx` is `@meith/cli`'s
|
|
97
|
+
* dependency, found either hoisted to the workspace root or nested under
|
|
98
|
+
* this package's own `node_modules`, the same reasoning as forum-web's
|
|
99
|
+
* `resolveNextBin()`.
|
|
100
|
+
*/
|
|
95
101
|
function resolveTsx() {
|
|
96
|
-
// Resolved from this package's own directory — `tsx` is @meith/cli's
|
|
97
|
-
// dependency, found either hoisted to the workspace root or nested under
|
|
98
|
-
// this package's own node_modules, the same reasoning as forum-web's
|
|
99
|
-
// resolveNextBin().
|
|
100
102
|
const require = createRequire(join(packageRoot, 'package.json'))
|
|
101
103
|
try {
|
|
102
104
|
return require.resolve('tsx/cli')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/cli",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"description": "The operator CLI: migrations, backup and restore, imports, users and settings — and the community bin that runs it against an external board workspace.",
|
|
5
5
|
"license": "LGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -23,18 +23,18 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"tsx": "^4.23.12",
|
|
26
|
-
"@meith/
|
|
27
|
-
"@meith/
|
|
28
|
-
"@meith/
|
|
29
|
-
"@meith/
|
|
30
|
-
"@meith/
|
|
31
|
-
"@meith/
|
|
32
|
-
"@meith/plugin-kit": "0.17.
|
|
33
|
-
"@meith/profile-fields": "0.17.
|
|
34
|
-
"@meith/
|
|
35
|
-
"@meith/
|
|
36
|
-
"
|
|
37
|
-
"
|
|
26
|
+
"@meith/accounts": "0.17.2",
|
|
27
|
+
"@meith/db": "0.17.2",
|
|
28
|
+
"@meith/drivers": "0.17.2",
|
|
29
|
+
"@meith/demo": "0.17.2",
|
|
30
|
+
"@meith/forums": "0.17.2",
|
|
31
|
+
"@meith/core": "0.17.2",
|
|
32
|
+
"@meith/plugin-kit": "0.17.2",
|
|
33
|
+
"@meith/profile-fields": "0.17.2",
|
|
34
|
+
"@meith/tasks": "0.17.2",
|
|
35
|
+
"@meith/runtime": "0.17.2",
|
|
36
|
+
"@meith/settings": "0.17.2",
|
|
37
|
+
"create-meith": "0.17.2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"esbuild": "^0.28.2"
|
package/src/board-eject.ts
CHANGED
|
@@ -17,8 +17,12 @@ import { CODE_VERSION } from './upgrade'
|
|
|
17
17
|
* bundled `dist/cli.cjs` does not sit at that same distance — Docker's own
|
|
18
18
|
* `COPY apps/cli/dist/ ./apps/cli/` drops the `dist` segment.
|
|
19
19
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
function defaultManifestPath(): string {
|
|
21
|
+
return join(
|
|
22
|
+
fileURLToPath(new URL('../../../', import.meta.url)),
|
|
23
|
+
'boards/stock/board.plugins.json',
|
|
24
|
+
)
|
|
25
|
+
}
|
|
22
26
|
|
|
23
27
|
interface ManifestEntry {
|
|
24
28
|
readonly key: string
|
|
@@ -31,7 +35,81 @@ interface Manifest {
|
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
function manifestPath(): string {
|
|
34
|
-
return process.env.BOARD_PLUGINS_MANIFEST ??
|
|
38
|
+
return process.env.BOARD_PLUGINS_MANIFEST ?? defaultManifestPath()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Duplicated from scripts/board-plugins.mjs's own PLUGIN_KEY_PATTERN, IDENTIFIER_PATTERN
|
|
43
|
+
* and NPM_PACKAGE_NAME_PATTERN rather than imported — see docs/plugin-api.md, "The board
|
|
44
|
+
* plugin manifests" — and pinned to agree with it by board-eject.test.ts.
|
|
45
|
+
*/
|
|
46
|
+
const PLUGIN_KEY_PATTERN = /^[a-z][a-z0-9-]{1,39}$/
|
|
47
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
48
|
+
const NPM_PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The same shape of refusal scripts/board-plugins.mjs's validateManifest makes for
|
|
52
|
+
* apps/community and boards/stock, minus the dependency check — a deployed image
|
|
53
|
+
* carries no package.json listing what it was actually built with. A manifest that
|
|
54
|
+
* gets this far already passed that check once, when plugin:add wrote it; this is
|
|
55
|
+
* defense against a manifest hand-edited after the fact, not the primary gate.
|
|
56
|
+
*/
|
|
57
|
+
export function validateEjectManifest(plugins: readonly ManifestEntry[], path: string): void {
|
|
58
|
+
const seen = new Set<string>()
|
|
59
|
+
const identifiers = new Map<string, string>()
|
|
60
|
+
|
|
61
|
+
for (const entry of plugins) {
|
|
62
|
+
if (typeof entry.key !== 'string' || typeof entry.package !== 'string') {
|
|
63
|
+
throw new ValidationError(
|
|
64
|
+
`${path}: every entry needs a string "key" and "package". Got ${JSON.stringify(entry)}.`,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (seen.has(entry.key)) {
|
|
69
|
+
throw new ValidationError(`${path}: "${entry.key}" is listed twice.`)
|
|
70
|
+
}
|
|
71
|
+
seen.add(entry.key)
|
|
72
|
+
|
|
73
|
+
if (!PLUGIN_KEY_PATTERN.test(entry.key)) {
|
|
74
|
+
throw new ValidationError(
|
|
75
|
+
`${path}: "${entry.key}" is not a valid plugin key. definePlugin requires lower-case ` +
|
|
76
|
+
'letters, digits and hyphens, starting with a letter, 2-40 characters long.',
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (entry.enabled !== undefined && typeof entry.enabled !== 'boolean') {
|
|
81
|
+
throw new ValidationError(
|
|
82
|
+
`${path}: "${entry.key}" has a non-boolean "enabled" (${JSON.stringify(entry.enabled)}). ` +
|
|
83
|
+
'Omit the field to enable the plugin, or set it to true or false.',
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!NPM_PACKAGE_NAME_PATTERN.test(entry.package) || entry.package.length > 214) {
|
|
88
|
+
throw new ValidationError(
|
|
89
|
+
`${path}: "${entry.package}" (key "${entry.key}") is not a valid npm package name.`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const identifier = toIdentifier(entry.key)
|
|
94
|
+
if (!IDENTIFIER_PATTERN.test(identifier)) {
|
|
95
|
+
throw new ValidationError(
|
|
96
|
+
`${path}: "${entry.key}" is a valid plugin key, but the identifier ` +
|
|
97
|
+
`community.plugins.ts would bind for it, "${identifier}", is not a valid TypeScript ` +
|
|
98
|
+
'identifier. Each hyphen must be followed by exactly one lower-case letter or digit, ' +
|
|
99
|
+
'and a key cannot end in a hyphen.',
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const collidingKey = identifiers.get(identifier)
|
|
104
|
+
if (collidingKey !== undefined) {
|
|
105
|
+
throw new ValidationError(
|
|
106
|
+
`${path}: "${entry.key}" and "${collidingKey}" both generate the identifier ` +
|
|
107
|
+
`"${identifier}" for community.plugins.ts. Rename one of the keys so the generated ` +
|
|
108
|
+
'imports do not collide.',
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
identifiers.set(identifier, entry.key)
|
|
112
|
+
}
|
|
35
113
|
}
|
|
36
114
|
|
|
37
115
|
/**
|
|
@@ -63,6 +141,7 @@ async function readManifest(): Promise<Manifest> {
|
|
|
63
141
|
if (!Array.isArray(parsed.plugins)) {
|
|
64
142
|
throw new ValidationError(`${path} must have a "plugins" array.`)
|
|
65
143
|
}
|
|
144
|
+
validateEjectManifest(parsed.plugins, path)
|
|
66
145
|
return { plugins: parsed.plugins }
|
|
67
146
|
}
|
|
68
147
|
|
|
@@ -75,10 +154,33 @@ async function isEmptyOrMissing(target: string): Promise<boolean> {
|
|
|
75
154
|
}
|
|
76
155
|
}
|
|
77
156
|
|
|
78
|
-
function toIdentifier(key: string): string {
|
|
157
|
+
export function toIdentifier(key: string): string {
|
|
79
158
|
return key.replace(/-([a-z0-9])/g, (_match, char) => char.toUpperCase())
|
|
80
159
|
}
|
|
81
160
|
|
|
161
|
+
interface EjectedPackageJson {
|
|
162
|
+
dependencies: Record<string, string>
|
|
163
|
+
[key: string]: unknown
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Every manifest entry's package pinned into the scaffolded package.json's
|
|
168
|
+
* dependencies, at this exact running version — see docs/marketplace.md,
|
|
169
|
+
* "1. Eject", for why never `latest`, and for the collision policy this
|
|
170
|
+
* implements: a package scaffold() already pins is left exactly where it
|
|
171
|
+
* is, never duplicated or reordered, because both pins are always this
|
|
172
|
+
* same CODE_VERSION.
|
|
173
|
+
*/
|
|
174
|
+
function mergePluginDependencies(packageJson: string, plugins: readonly ManifestEntry[]): string {
|
|
175
|
+
const parsed = JSON.parse(packageJson) as EjectedPackageJson
|
|
176
|
+
for (const entry of plugins) {
|
|
177
|
+
if (!(entry.package in parsed.dependencies)) {
|
|
178
|
+
parsed.dependencies[entry.package] = CODE_VERSION
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return `${JSON.stringify(parsed, null, 2)}\n`
|
|
182
|
+
}
|
|
183
|
+
|
|
82
184
|
/**
|
|
83
185
|
* The ejected workspace's own community.plugins.ts — the same shape
|
|
84
186
|
* create-meith's scaffold() writes for a plugin-free board (see
|
|
@@ -122,6 +224,30 @@ export function installedPluginDefinitions() {
|
|
|
122
224
|
`
|
|
123
225
|
}
|
|
124
226
|
|
|
227
|
+
function isFsPermissionError(error: unknown): error is NodeJS.ErrnoException {
|
|
228
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code
|
|
229
|
+
return code === 'EACCES' || code === 'EPERM'
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Turns an EACCES/EPERM from the write loop below into this instead of a
|
|
234
|
+
* bare Node stack trace — see docs/marketplace.md, "1. Eject", for the
|
|
235
|
+
* bind-mount ownership rule this message is naming. Anything else passes
|
|
236
|
+
* through unchanged.
|
|
237
|
+
*/
|
|
238
|
+
function translateWriteError(error: unknown, target: string, path: string): never {
|
|
239
|
+
if (isFsPermissionError(error)) {
|
|
240
|
+
throw new ValidationError(
|
|
241
|
+
`board:eject could not write to ${path}: permission denied. The account running this ` +
|
|
242
|
+
`command needs write access to ${target} — inside the official image that account is ` +
|
|
243
|
+
'a fixed, non-root user, so a bind-mounted target directory has to already be owned by ' +
|
|
244
|
+
'(or writable by) that same account before eject runs. See docs/marketplace.md, ' +
|
|
245
|
+
'"Moving to a custom board", for the exact invocation.',
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
throw error
|
|
249
|
+
}
|
|
250
|
+
|
|
125
251
|
export async function boardEject(args: readonly string[]): Promise<number> {
|
|
126
252
|
const positional = args.filter((arg) => !arg.startsWith('-'))
|
|
127
253
|
const [dir] = positional
|
|
@@ -150,13 +276,22 @@ export async function boardEject(args: readonly string[]): Promise<number> {
|
|
|
150
276
|
const files = new Map(
|
|
151
277
|
scaffold({ name, version: CODE_VERSION, repositoryUrl: DEFAULT_REPOSITORY_URL }),
|
|
152
278
|
)
|
|
279
|
+
const packageJson = files.get('package.json')
|
|
280
|
+
if (packageJson === undefined) {
|
|
281
|
+
throw new Error('scaffold() did not emit package.json')
|
|
282
|
+
}
|
|
283
|
+
files.set('package.json', mergePluginDependencies(packageJson, manifest.plugins))
|
|
153
284
|
files.set('board.plugins.json', `${JSON.stringify({ plugins: manifest.plugins }, null, 2)}\n`)
|
|
154
285
|
files.set('community.plugins.ts', renderInstalledPluginsModule(manifest.plugins))
|
|
155
286
|
|
|
156
287
|
for (const [relative, contents] of files) {
|
|
157
288
|
const path = join(target, relative)
|
|
158
|
-
|
|
159
|
-
|
|
289
|
+
try {
|
|
290
|
+
await mkdir(dirname(path), { recursive: true })
|
|
291
|
+
await writeFile(path, contents, 'utf8')
|
|
292
|
+
} catch (error) {
|
|
293
|
+
translateWriteError(error, target, path)
|
|
294
|
+
}
|
|
160
295
|
}
|
|
161
296
|
|
|
162
297
|
console.log(`Ejected ${files.size} files to ${target}, pinned to meith ${CODE_VERSION}.`)
|
|
@@ -171,7 +306,7 @@ export async function boardEject(args: readonly string[]): Promise<number> {
|
|
|
171
306
|
console.log(' git init && git add -A && git commit -m "Graduate from the stock image"')
|
|
172
307
|
console.log(' # push it to a new GitHub repository')
|
|
173
308
|
console.log(' # .github/workflows/build.yml builds and pushes the image on every push to main —')
|
|
174
|
-
console.log(
|
|
309
|
+
console.log(' # point Coolify at this repository and set MEITH_IMAGE to the pushed image')
|
|
175
310
|
console.log(' # redeploy — same database, same uploads, same secrets, new image source')
|
|
176
311
|
console.log('')
|
|
177
312
|
console.log('See docs/marketplace.md, "Moving to a custom board", for the full walkthrough.')
|
package/src/plugin-manifest.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
5
5
|
|
|
6
6
|
import { ValidationError } from '@meith/core'
|
|
7
7
|
|
|
8
|
+
import BOARDS_JSON from '../../../scripts/boards.json'
|
|
8
9
|
import { optional, parseFlags } from './args'
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -12,9 +13,33 @@ import { optional, parseFlags } from './args'
|
|
|
12
13
|
* distance from the repository root (apps/cli/{src,dist}/<file> either way), so this
|
|
13
14
|
* offset holds whether these commands run from source (tsx) or the built dist/cli.cjs.
|
|
14
15
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
function repoRoot(): string {
|
|
17
|
+
return fileURLToPath(new URL('../../../', import.meta.url))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function generatorScript(): string {
|
|
21
|
+
return join(repoRoot(), 'scripts/board-plugins-gen.mjs')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Every board's board.plugins.json is edited together, from the one list in
|
|
26
|
+
* scripts/boards.json that scripts/board-plugins-gen.mjs reads too. That file
|
|
27
|
+
* and the MEITH_BOARD_PLUGINS_ROOT override are described in
|
|
28
|
+
* docs/development.md, "The board plugin manifests".
|
|
29
|
+
*/
|
|
30
|
+
interface Board {
|
|
31
|
+
readonly manifestFile: string
|
|
32
|
+
readonly packageFile: string
|
|
33
|
+
readonly outputFile: string
|
|
34
|
+
readonly packageLabel: string
|
|
35
|
+
readonly filterName: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const BOARDS = BOARDS_JSON as readonly Board[]
|
|
39
|
+
|
|
40
|
+
function boardsRoot(): string {
|
|
41
|
+
return process.env.MEITH_BOARD_PLUGINS_ROOT ?? repoRoot()
|
|
42
|
+
}
|
|
18
43
|
|
|
19
44
|
interface ManifestEntry {
|
|
20
45
|
readonly key: string
|
|
@@ -32,19 +57,21 @@ interface Manifest {
|
|
|
32
57
|
* one meant to run as `docker compose run --rm web community plugin:purge`. The deployed
|
|
33
58
|
* image is built `FROM node:26-alpine` with only `.next/standalone`, the worker and this
|
|
34
59
|
* CLI's own bundle copied in (see docker/Dockerfile) — no `scripts/`, no `board.plugins.json`,
|
|
35
|
-
* no Biome. Reading
|
|
60
|
+
* no Biome. Reading a manifest is where that shows up first, so this is where it is named.
|
|
36
61
|
*/
|
|
37
|
-
async function
|
|
62
|
+
async function readManifestFor(board: Board): Promise<Manifest> {
|
|
63
|
+
const path = join(boardsRoot(), board.manifestFile)
|
|
38
64
|
let raw: string
|
|
39
65
|
try {
|
|
40
|
-
raw = await readFile(
|
|
66
|
+
raw = await readFile(path, 'utf8')
|
|
41
67
|
} catch (error) {
|
|
42
68
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
43
69
|
throw new ValidationError(
|
|
44
|
-
`${
|
|
45
|
-
'
|
|
46
|
-
'it
|
|
47
|
-
'
|
|
70
|
+
`${path} does not exist. This command edits source files for every board this ` +
|
|
71
|
+
'repository carries — apps/community and boards/stock — and reruns the generator, ' +
|
|
72
|
+
'so it needs a checkout of the repository, not the deployed image — run it where you ' +
|
|
73
|
+
'would run `pnpm add`, commit both board.plugins.json files and both ' +
|
|
74
|
+
'community.plugins.ts files, then rebuild and redeploy.',
|
|
48
75
|
)
|
|
49
76
|
}
|
|
50
77
|
throw error
|
|
@@ -52,13 +79,25 @@ async function readManifest(): Promise<Manifest> {
|
|
|
52
79
|
|
|
53
80
|
const parsed = JSON.parse(raw) as Partial<Manifest>
|
|
54
81
|
if (!Array.isArray(parsed.plugins)) {
|
|
55
|
-
throw new ValidationError(
|
|
82
|
+
throw new ValidationError(`${path} must have a "plugins" array.`)
|
|
56
83
|
}
|
|
84
|
+
|
|
85
|
+
const extraFields = Object.keys(parsed).filter((field) => field !== 'plugins')
|
|
86
|
+
if (extraFields.length > 0) {
|
|
87
|
+
throw new ValidationError(
|
|
88
|
+
`${path} has ${extraFields.length === 1 ? 'a field' : 'fields'} plugin:add/plugin:remove ` +
|
|
89
|
+
`do not know how to carry forward: ${extraFields.join(', ')}. "plugins" is the ` +
|
|
90
|
+
"manifest's only field — remove the rest by hand, since rewriting the file here would " +
|
|
91
|
+
'otherwise drop them silently.',
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
57
95
|
return { plugins: parsed.plugins }
|
|
58
96
|
}
|
|
59
97
|
|
|
60
|
-
async function
|
|
61
|
-
|
|
98
|
+
async function writeManifestFor(board: Board, manifest: Manifest): Promise<void> {
|
|
99
|
+
const path = join(boardsRoot(), board.manifestFile)
|
|
100
|
+
await writeFile(path, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
|
62
101
|
}
|
|
63
102
|
|
|
64
103
|
interface GeneratorResult {
|
|
@@ -68,14 +107,15 @@ interface GeneratorResult {
|
|
|
68
107
|
|
|
69
108
|
/**
|
|
70
109
|
* The one thing this file trusts to know whether a manifest edit is valid: the same
|
|
71
|
-
* generator `pnpm board:gen` runs. Shelling out — rather
|
|
72
|
-
* scripts/board-plugins.mjs — keeps a plain script and a workspace
|
|
73
|
-
* from needing to share a module; it also means a failed add or
|
|
74
|
-
* exactly the words a person typing `pnpm board:gen` themselves
|
|
110
|
+
* generator `pnpm board:gen` runs, for every board it carries. Shelling out — rather
|
|
111
|
+
* than importing scripts/board-plugins.mjs — keeps a plain script and a workspace
|
|
112
|
+
* TypeScript package from needing to share a module; it also means a failed add or
|
|
113
|
+
* remove is reported in exactly the words a person typing `pnpm board:gen` themselves
|
|
114
|
+
* would see, board by board.
|
|
75
115
|
*/
|
|
76
116
|
function runGenerator(): GeneratorResult {
|
|
77
117
|
try {
|
|
78
|
-
const output = execFileSync(process.execPath, [
|
|
118
|
+
const output = execFileSync(process.execPath, [generatorScript()], {
|
|
79
119
|
encoding: 'utf8',
|
|
80
120
|
stdio: 'pipe',
|
|
81
121
|
})
|
|
@@ -109,7 +149,7 @@ export async function pluginAdd(args: readonly string[]): Promise<number> {
|
|
|
109
149
|
throw new ValidationError(
|
|
110
150
|
`community plugin:add does not take plugin configuration (--${configFlags[0]}). The ` +
|
|
111
151
|
"manifest has no field for it — a plugin's own settings are the only place its " +
|
|
112
|
-
|
|
152
|
+
"configuration lives now, the way MEI-74 moved plugins/dues's plans there. Export a " +
|
|
113
153
|
'zero-argument plugin and add it with just its package name.',
|
|
114
154
|
)
|
|
115
155
|
}
|
|
@@ -122,23 +162,35 @@ export async function pluginAdd(args: readonly string[]): Promise<number> {
|
|
|
122
162
|
)
|
|
123
163
|
}
|
|
124
164
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
165
|
+
const originals = await Promise.all(BOARDS.map((board) => readManifestFor(board)))
|
|
166
|
+
|
|
167
|
+
originals.forEach(({ plugins }, index) => {
|
|
168
|
+
if (plugins.some((entry) => entry.key === key)) {
|
|
169
|
+
throw new ValidationError(`"${key}" is already in ${BOARDS[index]?.manifestFile}.`)
|
|
170
|
+
}
|
|
171
|
+
})
|
|
129
172
|
|
|
130
173
|
const entry: ManifestEntry = { key, package: packageName, enabled: !flags.has('disabled') }
|
|
131
|
-
|
|
174
|
+
|
|
175
|
+
await Promise.all(
|
|
176
|
+
BOARDS.map((board, index) =>
|
|
177
|
+
writeManifestFor(board, { plugins: [...(originals[index]?.plugins ?? []), entry] }),
|
|
178
|
+
),
|
|
179
|
+
)
|
|
132
180
|
|
|
133
181
|
const result = runGenerator()
|
|
134
182
|
if (!result.ok) {
|
|
135
|
-
await
|
|
183
|
+
await Promise.all(
|
|
184
|
+
BOARDS.map((board, index) => writeManifestFor(board, originals[index] ?? { plugins: [] })),
|
|
185
|
+
)
|
|
136
186
|
throw new ValidationError(`Could not add "${key}":\n\n${result.output}`)
|
|
137
187
|
}
|
|
138
188
|
|
|
189
|
+
const manifestFiles = BOARDS.map((board) => board.manifestFile).join(' and ')
|
|
190
|
+
const outputFiles = BOARDS.map((board) => board.outputFile).join(' and ')
|
|
139
191
|
console.log(
|
|
140
|
-
`Added "${key}" (${packageName}${entry.enabled ? '' : ', disabled'}) to
|
|
141
|
-
|
|
192
|
+
`Added "${key}" (${packageName}${entry.enabled ? '' : ', disabled'}) to ${manifestFiles} ` +
|
|
193
|
+
`and regenerated ${outputFiles}.`,
|
|
142
194
|
)
|
|
143
195
|
console.log('Rebuild and redeploy for it to take effect.')
|
|
144
196
|
return 0
|
|
@@ -152,25 +204,38 @@ export async function pluginRemove(args: readonly string[]): Promise<number> {
|
|
|
152
204
|
throw new ValidationError('Usage: community plugin:remove <key>')
|
|
153
205
|
}
|
|
154
206
|
|
|
155
|
-
const
|
|
156
|
-
if (!manifest.plugins.some((entry) => entry.key === key)) {
|
|
157
|
-
const present = manifest.plugins.map((entry) => entry.key)
|
|
158
|
-
throw new ValidationError(
|
|
159
|
-
present.length === 0
|
|
160
|
-
? `"${key}" is not in board.plugins.json — it lists no plugins.`
|
|
161
|
-
: `"${key}" is not in board.plugins.json. Present: ${present.join(', ')}.`,
|
|
162
|
-
)
|
|
163
|
-
}
|
|
207
|
+
const originals = await Promise.all(BOARDS.map((board) => readManifestFor(board)))
|
|
164
208
|
|
|
165
|
-
|
|
209
|
+
originals.forEach(({ plugins }, index) => {
|
|
210
|
+
if (!plugins.some((entry) => entry.key === key)) {
|
|
211
|
+
const present = plugins.map((entry) => entry.key)
|
|
212
|
+
throw new ValidationError(
|
|
213
|
+
present.length === 0
|
|
214
|
+
? `"${key}" is not in ${BOARDS[index]?.manifestFile} — it lists no plugins.`
|
|
215
|
+
: `"${key}" is not in ${BOARDS[index]?.manifestFile}. Present: ${present.join(', ')}.`,
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
await Promise.all(
|
|
221
|
+
BOARDS.map((board, index) =>
|
|
222
|
+
writeManifestFor(board, {
|
|
223
|
+
plugins: (originals[index]?.plugins ?? []).filter((entry) => entry.key !== key),
|
|
224
|
+
}),
|
|
225
|
+
),
|
|
226
|
+
)
|
|
166
227
|
|
|
167
228
|
const result = runGenerator()
|
|
168
229
|
if (!result.ok) {
|
|
169
|
-
await
|
|
230
|
+
await Promise.all(
|
|
231
|
+
BOARDS.map((board, index) => writeManifestFor(board, originals[index] ?? { plugins: [] })),
|
|
232
|
+
)
|
|
170
233
|
throw new ValidationError(`Could not remove "${key}":\n\n${result.output}`)
|
|
171
234
|
}
|
|
172
235
|
|
|
173
|
-
|
|
236
|
+
const manifestFiles = BOARDS.map((board) => board.manifestFile).join(' and ')
|
|
237
|
+
const outputFiles = BOARDS.map((board) => board.outputFile).join(' and ')
|
|
238
|
+
console.log(`Removed "${key}" from ${manifestFiles} and regenerated ${outputFiles}.`)
|
|
174
239
|
console.log('Rebuild and redeploy for it to take effect.')
|
|
175
240
|
return 0
|
|
176
241
|
}
|
package/src/upgrade.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { type PluginDefinition, pluginNavigationPlacements } from '@meith/plugin
|
|
|
11
11
|
import { runPluginLifecycle } from '@meith/runtime'
|
|
12
12
|
import { type PluginUpgrade, planUpgrade, upgradeNotice } from '@meith/upgrade'
|
|
13
13
|
|
|
14
|
-
export const CODE_VERSION = '0.17.
|
|
14
|
+
export const CODE_VERSION = '0.17.2'
|
|
15
15
|
|
|
16
16
|
export function pluginUpgrades(plugins: readonly PluginDefinition[]): readonly PluginUpgrade[] {
|
|
17
17
|
return plugins.map((plugin) => ({
|