@meith/cli 0.23.1 → 0.23.3
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/package.json +13 -13
- package/src/backup.ts +25 -2
- package/src/board-eject.ts +7 -19
- package/src/upgrade.ts +1 -1
- package/src/write-errors.ts +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/cli",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.3",
|
|
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": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -23,18 +23,18 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"tsx": "^4.23.12",
|
|
26
|
-
"@meith/accounts": "0.23.
|
|
27
|
-
"@meith/
|
|
28
|
-
"@meith/
|
|
29
|
-
"@meith/demo": "0.23.
|
|
30
|
-
"@meith/drivers": "0.23.
|
|
31
|
-
"@meith/forums": "0.23.
|
|
32
|
-
"@meith/plugin-kit": "0.23.
|
|
33
|
-
"@meith/profile-fields": "0.23.
|
|
34
|
-
"@meith/runtime": "0.23.
|
|
35
|
-
"@meith/settings": "0.23.
|
|
36
|
-
"@meith/tasks": "0.23.
|
|
37
|
-
"create-meith": "0.23.
|
|
26
|
+
"@meith/accounts": "0.23.3",
|
|
27
|
+
"@meith/db": "0.23.3",
|
|
28
|
+
"@meith/core": "0.23.3",
|
|
29
|
+
"@meith/demo": "0.23.3",
|
|
30
|
+
"@meith/drivers": "0.23.3",
|
|
31
|
+
"@meith/forums": "0.23.3",
|
|
32
|
+
"@meith/plugin-kit": "0.23.3",
|
|
33
|
+
"@meith/profile-fields": "0.23.3",
|
|
34
|
+
"@meith/runtime": "0.23.3",
|
|
35
|
+
"@meith/settings": "0.23.3",
|
|
36
|
+
"@meith/tasks": "0.23.3",
|
|
37
|
+
"create-meith": "0.23.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"esbuild": "^0.28.2"
|
package/src/backup.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { BlobFileStore, S3FileStore, unusableKeyReason } from '@meith/drivers'
|
|
|
21
21
|
import { optional, parseFlags } from './args'
|
|
22
22
|
import { requirePostgres } from './context'
|
|
23
23
|
import { CODE_VERSION } from './upgrade'
|
|
24
|
+
import { translateWriteError } from './write-errors'
|
|
24
25
|
|
|
25
26
|
export type UploadsMode = 'include' | 'skip'
|
|
26
27
|
|
|
@@ -382,6 +383,27 @@ export async function reserveBackupDestination(destination: string): Promise<voi
|
|
|
382
383
|
await file.close()
|
|
383
384
|
}
|
|
384
385
|
|
|
386
|
+
export async function claimBackupDestination(destination: string): Promise<void> {
|
|
387
|
+
try {
|
|
388
|
+
await reserveBackupDestination(destination)
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if ((error as NodeJS.ErrnoException | undefined)?.code === 'EEXIST') {
|
|
391
|
+
throw new ValidationError(
|
|
392
|
+
`backup will not write over ${destination}: something is already there. Move it aside ` +
|
|
393
|
+
'or pass a different --out. A previous run killed part-way through can leave an ' +
|
|
394
|
+
'empty or truncated bundle at the path it had claimed; that file is not a backup ' +
|
|
395
|
+
'and is safe to delete.',
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
translateWriteError(error, {
|
|
399
|
+
command: 'backup',
|
|
400
|
+
path: destination,
|
|
401
|
+
target: path.dirname(destination),
|
|
402
|
+
reference: 'docs/guides/operations/operating.md, "Backup"',
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
385
407
|
interface StagedUploads {
|
|
386
408
|
readonly uploads: 'included' | 'skipped'
|
|
387
409
|
readonly skippedKeys: readonly string[]
|
|
@@ -521,6 +543,9 @@ export async function backupCommand(args: readonly string[]): Promise<number> {
|
|
|
521
543
|
const stage = await mkdtemp(path.join(tmpdir(), 'meith-backup-'))
|
|
522
544
|
let destinationCreated = false
|
|
523
545
|
try {
|
|
546
|
+
await claimBackupDestination(out)
|
|
547
|
+
destinationCreated = true
|
|
548
|
+
|
|
524
549
|
console.log(
|
|
525
550
|
env.DIRECT_DATABASE_URL === undefined
|
|
526
551
|
? 'Dumping the database…'
|
|
@@ -550,8 +575,6 @@ export async function backupCommand(args: readonly string[]): Promise<number> {
|
|
|
550
575
|
|
|
551
576
|
const members = ['manifest.json', 'db.dump']
|
|
552
577
|
if (uploads === 'included') members.push('uploads.tar.gz')
|
|
553
|
-
await reserveBackupDestination(out)
|
|
554
|
-
destinationCreated = true
|
|
555
578
|
await run('tar', ['czf', out, '-C', stage, ...members])
|
|
556
579
|
await chmod(out, 0o600)
|
|
557
580
|
|
package/src/board-eject.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { DEFAULT_REPOSITORY_URL, scaffold, validateName } from 'create-meith'
|
|
|
7
7
|
import { ValidationError } from '@meith/core'
|
|
8
8
|
|
|
9
9
|
import { CODE_VERSION } from './upgrade'
|
|
10
|
+
import { translateWriteError } from './write-errors'
|
|
10
11
|
|
|
11
12
|
function defaultManifestPath(): string {
|
|
12
13
|
return join(
|
|
@@ -173,24 +174,6 @@ export function installedPluginDefinitions() {
|
|
|
173
174
|
`
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
function isFsPermissionError(error: unknown): error is NodeJS.ErrnoException {
|
|
177
|
-
const code = (error as NodeJS.ErrnoException | undefined)?.code
|
|
178
|
-
return code === 'EACCES' || code === 'EPERM'
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function translateWriteError(error: unknown, target: string, path: string): never {
|
|
182
|
-
if (isFsPermissionError(error)) {
|
|
183
|
-
throw new ValidationError(
|
|
184
|
-
`board:eject could not write to ${path}: permission denied. The account running this ` +
|
|
185
|
-
`command needs write access to ${target} — inside the official image that account is ` +
|
|
186
|
-
'a fixed, non-root user, so a bind-mounted target directory has to already be owned by ' +
|
|
187
|
-
'(or writable by) that same account before eject runs. See docs/customization/marketplace.md, ' +
|
|
188
|
-
'"Moving to a custom board", for the exact invocation.',
|
|
189
|
-
)
|
|
190
|
-
}
|
|
191
|
-
throw error
|
|
192
|
-
}
|
|
193
|
-
|
|
194
177
|
export async function boardEject(args: readonly string[]): Promise<number> {
|
|
195
178
|
const positional = args.filter((arg) => !arg.startsWith('-'))
|
|
196
179
|
const [dir] = positional
|
|
@@ -233,7 +216,12 @@ export async function boardEject(args: readonly string[]): Promise<number> {
|
|
|
233
216
|
await mkdir(dirname(path), { recursive: true })
|
|
234
217
|
await writeFile(path, contents, 'utf8')
|
|
235
218
|
} catch (error) {
|
|
236
|
-
translateWriteError(error,
|
|
219
|
+
translateWriteError(error, {
|
|
220
|
+
command: 'board:eject',
|
|
221
|
+
path,
|
|
222
|
+
target,
|
|
223
|
+
reference: 'docs/customization/marketplace.md, "Moving to a custom board"',
|
|
224
|
+
})
|
|
237
225
|
}
|
|
238
226
|
}
|
|
239
227
|
|
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.23.
|
|
14
|
+
export const CODE_VERSION = '0.23.3'
|
|
15
15
|
|
|
16
16
|
export function pluginUpgrades(plugins: readonly PluginDefinition[]): readonly PluginUpgrade[] {
|
|
17
17
|
return plugins.map((plugin) => ({
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ValidationError } from '@meith/core'
|
|
2
|
+
|
|
3
|
+
export interface WriteFailure {
|
|
4
|
+
readonly command: string
|
|
5
|
+
readonly path: string
|
|
6
|
+
readonly target: string
|
|
7
|
+
readonly reference: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isFsPermissionError(error: unknown): error is NodeJS.ErrnoException {
|
|
11
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code
|
|
12
|
+
return code === 'EACCES' || code === 'EPERM'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function translateWriteError(error: unknown, failure: WriteFailure): never {
|
|
16
|
+
if (isFsPermissionError(error)) {
|
|
17
|
+
throw new ValidationError(
|
|
18
|
+
`${failure.command} could not write to ${failure.path}: permission denied. The account ` +
|
|
19
|
+
`running this command needs write access to ${failure.target} — inside the official ` +
|
|
20
|
+
'image that account is a fixed, non-root user, so a bind-mounted target directory has ' +
|
|
21
|
+
`to already be owned by (or writable by) that same account before ${failure.command} ` +
|
|
22
|
+
`runs. See ${failure.reference} for the exact invocation.`,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
throw error
|
|
26
|
+
}
|