@meith/cli 0.23.0 → 0.23.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/cli",
3
- "version": "0.23.0",
3
+ "version": "0.23.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": "MIT",
6
6
  "repository": {
@@ -23,18 +23,18 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "tsx": "^4.23.12",
26
- "@meith/accounts": "0.23.0",
27
- "@meith/core": "0.23.0",
28
- "@meith/db": "0.23.0",
29
- "@meith/demo": "0.23.0",
30
- "@meith/drivers": "0.23.0",
31
- "@meith/forums": "0.23.0",
32
- "@meith/plugin-kit": "0.23.0",
33
- "@meith/profile-fields": "0.23.0",
34
- "@meith/runtime": "0.23.0",
35
- "@meith/settings": "0.23.0",
36
- "@meith/tasks": "0.23.0",
37
- "create-meith": "0.23.0"
26
+ "@meith/accounts": "0.23.2",
27
+ "@meith/core": "0.23.2",
28
+ "@meith/db": "0.23.2",
29
+ "@meith/demo": "0.23.2",
30
+ "@meith/drivers": "0.23.2",
31
+ "@meith/forums": "0.23.2",
32
+ "@meith/plugin-kit": "0.23.2",
33
+ "@meith/profile-fields": "0.23.2",
34
+ "@meith/runtime": "0.23.2",
35
+ "@meith/settings": "0.23.2",
36
+ "@meith/tasks": "0.23.2",
37
+ "create-meith": "0.23.2"
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
 
@@ -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, target, path)
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.0'
14
+ export const CODE_VERSION = '0.23.2'
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
+ }