@crombolo/cli 0.14.1 → 0.15.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.
Files changed (85) hide show
  1. package/bin/crombolo.ts +4 -2767
  2. package/oclif.manifest.json +6710 -0
  3. package/package.json +58 -7
  4. package/src/base.ts +129 -0
  5. package/src/classify.ts +119 -0
  6. package/src/clispec.ts +163 -0
  7. package/src/commands/admin/reset-password.ts +53 -0
  8. package/src/commands/assets/index.ts +6 -0
  9. package/src/commands/assets/list.ts +27 -0
  10. package/src/commands/assets/new.ts +42 -0
  11. package/src/commands/bundle/purge.ts +71 -0
  12. package/src/commands/components/check.ts +58 -0
  13. package/src/commands/components/index.ts +6 -0
  14. package/src/commands/components/list.ts +40 -0
  15. package/src/commands/components/new.ts +49 -0
  16. package/src/commands/content/check.ts +102 -0
  17. package/src/commands/content/export.ts +116 -0
  18. package/src/commands/content/import.ts +109 -0
  19. package/src/commands/content/publish.ts +21 -0
  20. package/src/commands/content/revert.ts +137 -0
  21. package/src/commands/content/runs.ts +62 -0
  22. package/src/commands/content/unpublish.ts +21 -0
  23. package/src/commands/dictionary/export.ts +49 -0
  24. package/src/commands/dictionary/import.ts +46 -0
  25. package/src/commands/domains/apply.ts +23 -0
  26. package/src/commands/domains/clear.ts +27 -0
  27. package/src/commands/domains/index.ts +6 -0
  28. package/src/commands/domains/list.ts +86 -0
  29. package/src/commands/domains/set.ts +55 -0
  30. package/src/commands/domains/undo.ts +29 -0
  31. package/src/commands/export.ts +100 -0
  32. package/src/commands/generate.ts +33 -0
  33. package/src/commands/import/index.ts +175 -0
  34. package/src/commands/import/umbraco/apply.ts +128 -0
  35. package/src/commands/import/umbraco/report.ts +56 -0
  36. package/src/commands/init.ts +76 -0
  37. package/src/commands/login.ts +38 -0
  38. package/src/commands/logout.ts +36 -0
  39. package/src/commands/maintenance/pause.ts +29 -0
  40. package/src/commands/maintenance/resume.ts +20 -0
  41. package/src/commands/maintenance/status.ts +34 -0
  42. package/src/commands/media/fetch.ts +62 -0
  43. package/src/commands/pull.ts +210 -0
  44. package/src/commands/push.ts +108 -0
  45. package/src/commands/remote/add.ts +78 -0
  46. package/src/commands/remote/job.ts +53 -0
  47. package/src/commands/remote/list.ts +41 -0
  48. package/src/commands/remote/remove.ts +26 -0
  49. package/src/commands/schema.ts +32 -0
  50. package/src/commands/start.ts +64 -0
  51. package/src/commands/status.ts +103 -0
  52. package/src/commands/types/add-property.ts +116 -0
  53. package/src/commands/types/check.ts +88 -0
  54. package/src/commands/types/export.ts +32 -0
  55. package/src/commands/types/new.ts +116 -0
  56. package/src/commands/types/purge.ts +65 -0
  57. package/src/commands/types/rewrite.ts +30 -0
  58. package/src/commands/types/sync.ts +66 -0
  59. package/src/commands/upgrade/check.ts +69 -0
  60. package/src/commands/upgrade/index.ts +120 -0
  61. package/src/commands/upgrade/ledger.ts +55 -0
  62. package/src/commands/whoami.ts +41 -0
  63. package/src/errors.ts +90 -0
  64. package/src/main.ts +23 -0
  65. package/src/manifest.ts +20 -0
  66. package/src/reference.ts +118 -0
  67. package/src/remote/client.ts +297 -0
  68. package/src/remote/remotes-file.ts +81 -0
  69. package/src/remote/transfer.ts +353 -0
  70. package/src/support/components.ts +7 -0
  71. package/src/support/describe.ts +179 -0
  72. package/src/support/domains.ts +89 -0
  73. package/src/support/flags.ts +157 -0
  74. package/src/support/prompt.ts +126 -0
  75. package/src/support/publish.ts +142 -0
  76. package/src/support/remote.ts +58 -0
  77. package/src/support/serve.ts +460 -0
  78. package/src/support/site.ts +26 -0
  79. package/src/support/types.ts +112 -0
  80. package/src/support/umbraco.ts +86 -0
  81. package/templates/basic/bundle/content/bundle.json +1 -1
  82. package/templates/basic/bundle/content/nodes/36955f6e-d110-469e-b280-72096eccd148.json +1 -1
  83. package/templates/basic/bundle/package.json +1 -1
  84. package/templates/demo/harbourstone/bundle/package.json +1 -1
  85. package/templates/starter/bundle/package.json +1 -1
@@ -0,0 +1,71 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import { BaseCommand } from '../../base.ts'
3
+ import * as ops from '../../operations/commands.ts'
4
+ import { backupTaken, dryRun } from '../../support/flags.ts'
5
+ import { confirm } from '../../support/prompt.ts'
6
+ import { siteConfig } from '../../support/site.ts'
7
+
8
+ /**
9
+ * Drops a bundle's tables and forgets its state. Taking a bundle out of
10
+ * `bundles` leaves them in place, so re-adding it resumes; this is the
11
+ * deliberate act that does not.
12
+ */
13
+ export default class BundlePurge extends BaseCommand {
14
+ static override summary = "back up, then drop a bundle's tables and clear its state"
15
+ static override description =
16
+ 'Refused while crombolo.config.ts still wires the bundle. Asks for the id to be typed back, unless --yes.'
17
+ static override args = { id: Args.string({ required: true, description: "the bundle's id" }) }
18
+ static override flags = {
19
+ yes: Flags.boolean({ summary: "don't ask: drop them" }),
20
+ 'dry-run': dryRun,
21
+ 'backup-taken': backupTaken,
22
+ }
23
+ static override audit = { category: 'system', action: 'Purge', entityType: 'Bundle' } as const
24
+ static override spec = {
25
+ effects: 'idempotent',
26
+ cardinality: 'single',
27
+ confirmationBypassArg: '--yes',
28
+ outputFields: [
29
+ { name: 'id', type: 'string' },
30
+ { name: 'tables', type: 'array', items: { type: 'string' } },
31
+ { name: 'dropped', type: 'array', items: { type: 'string' } },
32
+ { name: 'backup', type: 'string', nullable: true },
33
+ { name: 'changed', type: 'boolean' },
34
+ ],
35
+ errors: ['confirmation_required', 'refused'],
36
+ } as const
37
+
38
+ async run() {
39
+ const { args, flags } = await this.parse(BundlePurge)
40
+ const id = args.id
41
+ this.auditKey = id
42
+ const config = await siteConfig()
43
+ const plan = await ops.bundlePurgePlan(config, id)
44
+ if (plan.tables.length === 0) this.say(` The bundle "${id}" has no tables in this database.`)
45
+ else {
46
+ this.say(` This drops ${plan.tables.length} table(s), and every row in them:`)
47
+ for (const table of plan.tables) this.say(` ${table}`)
48
+ }
49
+ if (flags['dry-run']) {
50
+ this.changedNothing = true
51
+ return { id, tables: plan.tables, dropped: [], backup: null, changed: false }
52
+ }
53
+ if (!flags.yes)
54
+ await confirm(` Purge the bundle "${id}"? Type its id to confirm:`, {
55
+ bypass: '--yes',
56
+ answer: id,
57
+ declined: 'Nothing was dropped.',
58
+ })
59
+ const result = await ops.bundlePurge(config, id, { backupTaken: flags['backup-taken'] })
60
+ if (result.backup.path) this.progress(` backup ${result.backup.path}`)
61
+ for (const table of result.dropped) this.say(` dropped ${table}`)
62
+ this.say(` The bundle "${id}"'s state is cleared; adding it again starts its tables afresh.`)
63
+ return {
64
+ id,
65
+ tables: plan.tables,
66
+ dropped: result.dropped,
67
+ backup: result.backup.path ?? null,
68
+ changed: result.dropped.length > 0,
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,58 @@
1
+ import { checkComponents } from '@crombolo/server'
2
+ import { BaseCommand } from '../../base.ts'
3
+ import { declaredTypes } from '../../support/components.ts'
4
+ import { here } from '../../support/describe.ts'
5
+ import { siteConfig } from '../../support/site.ts'
6
+
7
+ /**
8
+ * A view is loaded when a request renders it, so one that does not compile is a
9
+ * 500 waiting for a visitor. This is that failure brought forward.
10
+ */
11
+ export default class ComponentsCheck extends BaseCommand {
12
+ static override summary = 'compile every component, and report one a type declares with no file'
13
+ static override description = 'Exits 20 when a component does not compile or type-check.'
14
+ static override spec = {
15
+ effects: 'read_only',
16
+ cardinality: 'single',
17
+ outputFields: [
18
+ { name: 'checked', type: 'integer' },
19
+ { name: 'templates', type: 'integer' },
20
+ { name: 'problems', type: 'array', items: { type: 'object' } },
21
+ { name: 'typeProblems', type: 'array', items: { type: 'string' } },
22
+ { name: 'typesNote', type: 'string', nullable: true },
23
+ ],
24
+ outcomes: ['problems_found'],
25
+ } as const
26
+
27
+ async run() {
28
+ await this.parse(this.ctor)
29
+ const config = await siteConfig()
30
+ const types = declaredTypes(config)
31
+ const report = await checkComponents({
32
+ componentsDir: config.componentsDir,
33
+ siteDir: config.siteDir,
34
+ declaredComponents: types.flatMap((type) => type.components),
35
+ })
36
+ const templates = report.declared.length
37
+ this.say(
38
+ ` checked ${report.components.length} file(s): ${templates} template(s), ` +
39
+ `${report.components.length - templates} component(s)`,
40
+ )
41
+ for (const problem of report.problems)
42
+ this.say(` [error] ${problem.file}: ${problem.message}`)
43
+ if (report.types.note) this.say(` types ${report.types.note}`)
44
+ else {
45
+ this.say(` types tsc --noEmit over ${here(config.siteDir)}`)
46
+ for (const problem of report.types.problems) this.say(` [types] ${problem}`)
47
+ }
48
+ if (report.problems.length > 0 || report.types.problems.length > 0)
49
+ this.outcome = 'problems_found'
50
+ return {
51
+ checked: report.components.length,
52
+ templates,
53
+ problems: report.problems.map((p) => ({ file: p.file, message: p.message })),
54
+ typeProblems: report.types.problems,
55
+ typesNote: report.types.note ?? null,
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,6 @@
1
+ import ComponentsCheck from './check.ts'
2
+
3
+ /** `crombolo components` on its own is the check. */
4
+ export default class Components extends ComponentsCheck {
5
+ static override hidden = true
6
+ }
@@ -0,0 +1,40 @@
1
+ import { listComponents } from '@crombolo/server'
2
+ import { BaseCommand } from '../../base.ts'
3
+ import { declaredTypes } from '../../support/components.ts'
4
+ import { here } from '../../support/describe.ts'
5
+ import { siteConfig } from '../../support/site.ts'
6
+
7
+ export default class ComponentsList extends BaseCommand {
8
+ static override summary = 'what is in components/, and which type declares each'
9
+ static override spec = {
10
+ effects: 'read_only',
11
+ cardinality: 'bounded',
12
+ // One item of `items`, as clispec reads a collection.
13
+ outputFields: [
14
+ { name: 'alias', type: 'string' },
15
+ { name: 'path', type: 'string' },
16
+ { name: 'kind', type: 'string', enum: ['template', 'component'] },
17
+ { name: 'declaredBy', type: 'array', items: { type: 'string' } },
18
+ ],
19
+ } as const
20
+
21
+ async run() {
22
+ await this.parse(ComponentsList)
23
+ const config = await siteConfig()
24
+ const types = declaredTypes(config)
25
+ const found = listComponents(config.componentsDir)
26
+ if (found.length === 0) this.say(`Nothing in ${here(config.componentsDir)}.`)
27
+ const items = found.map((component) => {
28
+ // A component is a template because a content type names it, not
29
+ // because of where it sits — so the label is read off the schema.
30
+ const declaredBy = types
31
+ .filter((type) => type.components.includes(component.alias))
32
+ .map((type) => type.alias)
33
+ const kind = declaredBy.length > 0 ? ('template' as const) : ('component' as const)
34
+ const note = declaredBy.length > 0 ? ` ← ${declaredBy.join(', ')}` : ''
35
+ this.say(` ${kind.padEnd(9)} ${here(component.path)}${note}`)
36
+ return { alias: component.alias, path: here(component.path), kind, declaredBy }
37
+ })
38
+ return { items }
39
+ }
40
+ }
@@ -0,0 +1,49 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { mkdir, writeFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import { viewScaffold } from '@crombolo/schema'
5
+ import { componentFileFor, componentScaffold, isSafeAlias } from '@crombolo/server'
6
+ import { Args } from '@oclif/core'
7
+ import { BaseCommand } from '../../base.ts'
8
+ import { declaredTypes } from '../../support/components.ts'
9
+ import { here } from '../../support/describe.ts'
10
+ import { siteConfig } from '../../support/site.ts'
11
+
12
+ export default class ComponentsNew extends BaseCommand {
13
+ static override summary = 'components/<alias>.tsx; the alias may carry folders'
14
+ static override examples = ['<%= config.bin %> components new pages/article']
15
+ static override args = {
16
+ alias: Args.string({ required: true, description: "the component's alias" }),
17
+ }
18
+ static override spec = {
19
+ effects: 'non_idempotent',
20
+ cardinality: 'single',
21
+ outputFields: [
22
+ { name: 'created', type: 'string' },
23
+ { name: 'template', type: 'boolean' },
24
+ ],
25
+ errors: ['usage', 'conflict'],
26
+ } as const
27
+
28
+ async run() {
29
+ const { args } = await this.parse(ComponentsNew)
30
+ const config = await siteConfig()
31
+ if (!isSafeAlias(args.alias))
32
+ this.fail('usage', `"${args.alias}" is not a path inside ${here(config.componentsDir)}.`)
33
+ const file = componentFileFor(config.componentsDir, args.alias)
34
+ if (existsSync(file)) this.fail('conflict', `${here(file)} already exists.`)
35
+ // A content type that names this alias gets the page scaffold, which takes
36
+ // PageProps; anything else gets the plain one. Nothing turns on the folder.
37
+ const type = declaredTypes(config).find((candidate) =>
38
+ candidate.components.includes(args.alias),
39
+ )
40
+ if (!type)
41
+ this.say(
42
+ ` note no content type declares the template "${args.alias}" — add it to one's templates, or \`crombolo types new\``,
43
+ )
44
+ await mkdir(join(file, '..'), { recursive: true })
45
+ await writeFile(file, type ? viewScaffold(type) : componentScaffold(args.alias))
46
+ this.say(` created ${here(file)}`)
47
+ return { created: here(file), template: Boolean(type) }
48
+ }
49
+ }
@@ -0,0 +1,102 @@
1
+ import { writeFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { RESOLUTIONS_FILE, writeResolutions } from '@crombolo/transfer'
4
+ import { Args, Flags } from '@oclif/core'
5
+ import { BaseCommand } from '../../base.ts'
6
+ import * as ops from '../../operations/commands.ts'
7
+ import {
8
+ describeBundle,
9
+ findingsOf,
10
+ here,
11
+ printSections,
12
+ printTransferFindings,
13
+ } from '../../support/describe.ts'
14
+ import { transferFlags, transferInput } from '../../support/flags.ts'
15
+ import { passphraseToOpen } from '../../support/prompt.ts'
16
+ import { siteConfig } from '../../support/site.ts'
17
+
18
+ export default class ContentCheck extends BaseCommand {
19
+ static override summary = "read-only: what installing a bundle's content here would do"
20
+ static override description = [
21
+ 'Says what it needs from a person first, and writes it to the Changes dashboard. --save writes the answers given into the bundle.',
22
+ 'An encrypted bundle asks for its passphrase, or reads CROMBOLO_BUNDLE_PASSPHRASE. Exits 21 when a finding would stop an import.',
23
+ ].join('\n\n')
24
+ static override aliases = ['bundle:check']
25
+ static override args = {
26
+ bundle: Args.string({
27
+ required: true,
28
+ description: 'a package directory, or the name of a bundle this site depends on',
29
+ }),
30
+ }
31
+ static override flags = {
32
+ ...transferFlags,
33
+ save: Flags.boolean({ summary: 'write the answers into the bundle' }),
34
+ }
35
+ static override spec = {
36
+ effects: 'read_only',
37
+ cardinality: 'single',
38
+ outputFields: [
39
+ { name: 'bundle', type: 'object' },
40
+ { name: 'under', type: 'string', nullable: true },
41
+ { name: 'counts', type: 'object' },
42
+ { name: 'findings', type: 'array', items: { type: 'object' } },
43
+ { name: 'outstanding', type: 'integer' },
44
+ { name: 'clean', type: 'boolean' },
45
+ { name: 'wouldNotPublish', type: 'array', items: { type: 'object' } },
46
+ { name: 'saved', type: 'string', nullable: true },
47
+ ],
48
+ errors: ['refused', 'auth'],
49
+ outcomes: ['blocked'],
50
+ } as const
51
+
52
+ async run() {
53
+ const { args, flags } = await this.parse(ContentCheck)
54
+ const config = await siteConfig()
55
+ const result = await ops.contentCheck(config, {
56
+ ...transferInput(args.bundle, flags),
57
+ passphrase: await passphraseToOpen(args.bundle, config),
58
+ })
59
+ for (const problem of result.problems) this.warning(` ${problem}`)
60
+ describeBundle(this, result.bundle)
61
+ if (result.under) this.say(` under ${result.under}`)
62
+ printSections(this, result.sections)
63
+ printTransferFindings(this, result.check)
64
+ // The reasons are publish's own: the check rehearsed the import to find them.
65
+ for (const refusal of result.publishRefusals)
66
+ this.say(` would not publish ${refusal.key}: ${refusal.reason}`)
67
+
68
+ let saved: string | null = null
69
+ if (flags.save) {
70
+ const target = join(result.payloadDir, RESOLUTIONS_FILE)
71
+ await writeFile(
72
+ target,
73
+ writeResolutions({
74
+ bundleId: result.bundle.id,
75
+ under: flags.under ?? null,
76
+ all: result.decisions.all,
77
+ byNode: result.decisions.byNode,
78
+ }),
79
+ )
80
+ saved = here(target)
81
+ this.say(` wrote ${saved}`)
82
+ }
83
+
84
+ if (result.clean) this.say('\nNothing outstanding: this bundle would import cleanly.')
85
+ else {
86
+ this.say(
87
+ `\n${result.check.outstanding.length} finding(s) would stop an import. Resolve them, or pass --resolve.`,
88
+ )
89
+ this.outcome = 'blocked'
90
+ }
91
+ return {
92
+ bundle: result.bundle,
93
+ under: result.under ?? null,
94
+ counts: result.check.counts,
95
+ findings: findingsOf(result.check.findings),
96
+ outstanding: result.check.outstanding.length,
97
+ clean: result.clean,
98
+ wouldNotPublish: result.publishRefusals,
99
+ saved,
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,116 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import { basename, join, resolve } from 'node:path'
3
+ import { packageNameFor } from '@crombolo/transfer'
4
+ import { Flags } from '@oclif/core'
5
+ import { BaseCommand } from '../../base.ts'
6
+ import * as ops from '../../operations/commands.ts'
7
+ import { counted, describeBytes, describeContentMode, here } from '../../support/describe.ts'
8
+ import { contentFlags, contentMode, many } from '../../support/flags.ts'
9
+ import { passphraseToSeal } from '../../support/prompt.ts'
10
+ import { siteConfig } from '../../support/site.ts'
11
+
12
+ export default class ContentExport extends BaseCommand {
13
+ static override summary =
14
+ 'write a subtree as a private bundle package, to move to another environment'
15
+ static override description = [
16
+ 'The subtree, its dependencies, and what the destination must already have.',
17
+ 'Names and the tree only, unless --include-content: values, media files (--with-blobs) and protected-page usernames, encrypted under a passphrase (asked for, or CROMBOLO_BUNDLE_PASSPHRASE). --allow-content-unencrypted writes them readable. --with-members and --with-users add people, always encrypted.',
18
+ ].join('\n\n')
19
+ static override examples = ['<%= config.bin %> content export --root /blog --out ./bundles/blog']
20
+ static override flags = {
21
+ root: many('the subtree to export; repeatable', '<path|uuid>'),
22
+ out: Flags.string({
23
+ summary: 'the directory to write the package to',
24
+ helpValue: '<dir>',
25
+ required: true,
26
+ }),
27
+ only: Flags.boolean({ summary: 'the roots alone, not what is below them' }),
28
+ drafts: Flags.boolean({ summary: 'drafts, not only what is published' }),
29
+ 'with-blueprints': Flags.boolean({ summary: 'the blueprints of its types' }),
30
+ 'with-blobs': Flags.boolean({ summary: 'with --include-content: the media files' }),
31
+ 'with-members': Flags.boolean({ summary: 'members, encrypted' }),
32
+ 'with-users': Flags.boolean({ summary: 'back-office users, encrypted' }),
33
+ name: Flags.string({ summary: "the package's npm name", helpValue: '<npm name>' }),
34
+ 'package-version': Flags.string({ summary: "the package's version", helpValue: '<version>' }),
35
+ ...contentFlags,
36
+ }
37
+ static override spec = {
38
+ effects: 'non_idempotent',
39
+ cardinality: 'single',
40
+ outputFields: [
41
+ { name: 'out', type: 'string' },
42
+ { name: 'package', type: 'object' },
43
+ { name: 'bundleId', type: 'string' },
44
+ { name: 'counts', type: 'object' },
45
+ { name: 'expected', type: 'array', items: { type: 'object' } },
46
+ { name: 'content', type: 'string', enum: ['omitted', 'sealed', 'open'] },
47
+ { name: 'blobs', type: 'object' },
48
+ ],
49
+ errors: ['usage', 'not_found', 'refused', 'auth'],
50
+ } as const
51
+
52
+ async run() {
53
+ const { flags } = await this.parse(ContentExport)
54
+ if (flags.root.length === 0) this.fail('usage', 'content export needs --root <path|uuid>.')
55
+ const content = contentMode(this, flags)
56
+ const result = await ops.contentExport(await siteConfig(), {
57
+ content,
58
+ roots: flags.root,
59
+ only: flags.only,
60
+ drafts: flags.drafts,
61
+ withBlobs: flags['with-blobs'],
62
+ withMembers: flags['with-members'],
63
+ withUsers: flags['with-users'],
64
+ passphrase:
65
+ content === 'sealed' || flags['with-members'] || flags['with-users']
66
+ ? await passphraseToSeal()
67
+ : undefined,
68
+ withBlueprints: flags['with-blueprints'],
69
+ // An export is a private package; without --name it is named for its directory.
70
+ package: {
71
+ name: flags.name ?? packageNameFor(basename(resolve(flags.out))),
72
+ version: flags['package-version'],
73
+ },
74
+ })
75
+ for (const key of result.blobs.missing)
76
+ this.warning(` missing ${key} is not in this environment's media store`)
77
+ for (const file of result.files) {
78
+ const target = join(flags.out, file.path)
79
+ await mkdir(join(target, '..'), { recursive: true })
80
+ await writeFile(target, file.bytes ?? (file.text as string))
81
+ }
82
+ if (Object.keys(result.counts).length === 0)
83
+ this.fail(
84
+ 'not_found',
85
+ 'Nothing was selected, so the package is empty.',
86
+ 'Check the --root values.',
87
+ )
88
+ this.say(` wrote ${here(flags.out)}`)
89
+ this.say(` package ${result.package.name}@${result.package.version} (private)`)
90
+ this.say(
91
+ ` bundle ${result.bundleId} (${result.snapshot}): ${counted(result.counts, 'nothing selected')}`,
92
+ )
93
+ if (result.expected.length > 0) {
94
+ this.say(` expects ${result.expected.length} node(s) to exist at the destination:`)
95
+ for (const e of result.expected) this.say(` ${e.key} ${e.name} (${e.why})`)
96
+ }
97
+ describeContentMode(this, result.content, result.people)
98
+ if (result.blobs.carried > 0)
99
+ this.say(
100
+ ` blobs ${result.blobs.carried} media file(s), ${describeBytes(result.blobs.bytes)}`,
101
+ )
102
+ else if (result.blobs.notCarried > 0)
103
+ this.say(
104
+ ` blobs ${result.blobs.notCarried} media file(s) are not carried — pass --with-blobs to include them`,
105
+ )
106
+ return {
107
+ out: here(flags.out),
108
+ package: result.package,
109
+ bundleId: result.bundleId,
110
+ counts: result.counts,
111
+ expected: result.expected,
112
+ content: result.content,
113
+ blobs: result.blobs,
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,109 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import { BaseCommand } from '../../base.ts'
3
+ import * as ops from '../../operations/commands.ts'
4
+ import {
5
+ describeBundle,
6
+ describePlaced,
7
+ findingsOf,
8
+ here,
9
+ printTransferFindings,
10
+ } from '../../support/describe.ts'
11
+ import { backupTaken, transferFlags, transferInput } from '../../support/flags.ts'
12
+ import { passphraseToOpen } from '../../support/prompt.ts'
13
+ import { siteConfig } from '../../support/site.ts'
14
+
15
+ export default class ContentImport extends BaseCommand {
16
+ static override summary = "re-check, back up, then apply a bundle's content"
17
+ static override description = [
18
+ 'Documents and elements arrive as drafts, media is live. One transaction, so a failure leaves nothing behind.',
19
+ 'A bundle carrying its own schema and views installs into a site that has neither. `content revert <run>` puts it back.',
20
+ ].join('\n\n')
21
+ static override aliases = ['bundle:install']
22
+ static override args = {
23
+ bundle: Args.string({
24
+ required: true,
25
+ description: 'a package directory, or the name of a bundle this site depends on',
26
+ }),
27
+ }
28
+ static override flags = {
29
+ ...transferFlags,
30
+ label: Flags.string({ summary: 'a label for the run', helpValue: '<text>' }),
31
+ 'backup-taken': backupTaken,
32
+ }
33
+ static override audit = { category: 'content', action: 'Import', entityType: 'Bundle' } as const
34
+ static override spec = {
35
+ effects: 'non_idempotent',
36
+ cardinality: 'single',
37
+ outputFields: [
38
+ { name: 'runId', type: 'string' },
39
+ { name: 'bundle', type: 'object' },
40
+ { name: 'counts', type: 'object' },
41
+ { name: 'backup', type: 'string', nullable: true },
42
+ { name: 'published', type: 'integer' },
43
+ { name: 'notPublished', type: 'array', items: { type: 'object' } },
44
+ ],
45
+ errors: ['refused', 'auth'],
46
+ } as const
47
+
48
+ async run() {
49
+ const { args, flags } = await this.parse(ContentImport)
50
+ const config = await siteConfig()
51
+ const result = await ops.contentImport(config, {
52
+ ...transferInput(args.bundle, flags),
53
+ passphrase: await passphraseToOpen(args.bundle, config),
54
+ publish: flags.publish,
55
+ label: flags.label,
56
+ backupTaken: flags['backup-taken'],
57
+ })
58
+ for (const problem of result.problems) this.warning(` ${problem}`)
59
+ if (result.backup.path) this.progress(` backup ${result.backup.path}`)
60
+ describeBundle(this, result.bundle)
61
+
62
+ if (!result.runId) {
63
+ printTransferFindings(this, result.check)
64
+ // Staged, then applied in one transaction: a refusal leaves nothing behind.
65
+ this.fail(
66
+ 'refused',
67
+ `import refused: ${result.check.outstanding.length || result.problems.length} finding(s) outstanding. Nothing was written.`,
68
+ 'Resolve them, or pass --resolve.',
69
+ findingsOf(result.check.outstanding),
70
+ )
71
+ }
72
+
73
+ const c = result.check.counts
74
+ this.say(` run ${result.runId}`)
75
+ this.say(
76
+ ` applied ${c.create} created, ${c.update} updated, ${c.unchanged} unchanged, ${c.skip} skipped`,
77
+ )
78
+ const placed = describePlaced(result.blobs)
79
+ if (placed) this.say(` blobs ${placed}`)
80
+ if (result.sections) {
81
+ this.say(
82
+ ` files ${result.sections.written.length} written` +
83
+ (result.sections.replaced.length > 0
84
+ ? `, ${result.sections.replaced.length} replaced`
85
+ : ''),
86
+ )
87
+ if (result.sections.schema) this.say(` schema ${result.sections.schema.action}`)
88
+ if (result.sections.dictionary)
89
+ this.say(` dict ${result.sections.dictionary.imported} imported`)
90
+ // Named, because this is what makes reverting the install possible.
91
+ if (result.replacedFilesKept)
92
+ this.say(` kept ${here(result.replacedFilesKept)} (the files it replaced)`)
93
+ }
94
+ if (result.published > 0) this.say(` published ${result.published} node(s)`)
95
+ for (const failure of result.publishFailures)
96
+ this.warning(` not published ${failure.key}: ${failure.reason}`)
97
+ for (const finding of result.check.findings.filter((f) => f.kind === 'auto'))
98
+ this.say(` [auto ] ${finding.message}`)
99
+ this.say(`\nDone. \`crombolo content revert ${result.runId}\` puts it back.`)
100
+ return {
101
+ runId: result.runId,
102
+ bundle: result.bundle,
103
+ counts: c,
104
+ backup: result.backup.path ?? null,
105
+ published: result.published,
106
+ notPublished: result.publishFailures,
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,21 @@
1
+ import { BaseCommand } from '../../base.ts'
2
+ import { publishArgs, publishFlags, publishPages, publishSpec } from '../../support/publish.ts'
3
+
4
+ export default class ContentPublish extends BaseCommand {
5
+ static override summary = 'take a page live; --descendants for the branch below it'
6
+ static override description = '--culture per culture; --at and --until schedule it instead.'
7
+ static override args = publishArgs
8
+ static override flags = publishFlags
9
+ static override audit = {
10
+ category: 'content',
11
+ action: 'Publish',
12
+ entityType: 'Document',
13
+ } as const
14
+ static override spec = publishSpec
15
+
16
+ async run() {
17
+ const { args, flags } = await this.parse(ContentPublish)
18
+ this.auditKey = args.page
19
+ return publishPages(this, true, args.page, flags)
20
+ }
21
+ }