@crombolo/cli 0.10.0 → 0.11.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/bin/crombolo.ts CHANGED
@@ -2111,7 +2111,10 @@ function help(): void {
2111
2111
  }
2112
2112
 
2113
2113
  try {
2114
+ // Before dispatching, which rewrites `bundle install` as `content import`.
2115
+ const audited = commandAudit()
2114
2116
  await dispatch()
2117
+ if (audited) await ops.recordCommand(await siteConfig(), audited)
2115
2118
  } catch (error) {
2116
2119
  if (!(error instanceof ops.CommandError)) throw error
2117
2120
  for (const problem of error.problems) console.error(` ${problem}`)
@@ -2119,6 +2122,52 @@ try {
2119
2122
  process.exit(1)
2120
2123
  }
2121
2124
 
2125
+ // A declaration, not a const: the top-level dispatch above runs before this line.
2126
+ function capital(word: string): string {
2127
+ return `${word.charAt(0).toUpperCase()}${word.slice(1)}`
2128
+ }
2129
+
2130
+ /** The audit log entry for a command that changes the environment; undefined for one that only reads. */
2131
+ function commandAudit(): ops.CommandAudit | undefined {
2132
+ if (flags.has('--dry-run') || flags.has('--plan') || flags.has('--help')) return undefined
2133
+ const sub = positional[0]
2134
+ const typed = `crombolo ${command}${sub ? ` ${sub}` : ''}`
2135
+ const entry = (
2136
+ category: ops.CommandAudit['category'],
2137
+ action: string,
2138
+ entityType?: string,
2139
+ entityKey?: string,
2140
+ ): ops.CommandAudit => ({ category, action, command: typed, entityType, entityKey })
2141
+ switch (command) {
2142
+ case 'admin':
2143
+ return sub === 'reset-password' ? entry('user', 'ResetPassword', 'User') : undefined
2144
+ case 'schema':
2145
+ if (sub === 'sync') return entry('schema', 'Sync', 'Schema')
2146
+ return sub === 'purge' ? entry('schema', 'Purge', 'Schema') : undefined
2147
+ case 'content':
2148
+ case 'bundle':
2149
+ if (sub === 'purge') return entry('system', 'Purge', 'Bundle', positional[1])
2150
+ if (sub === 'import' || sub === 'install') return entry('content', 'Import', 'Bundle')
2151
+ return sub === 'publish' || sub === 'unpublish' || sub === 'revert'
2152
+ ? entry('content', capital(sub), 'Document', positional[1])
2153
+ : undefined
2154
+ case 'dictionary':
2155
+ return sub === 'import' ? entry('settings', 'Import', 'DictionaryItem') : undefined
2156
+ case 'domains':
2157
+ return sub && sub !== 'list'
2158
+ ? entry('settings', `${capital(sub)}Domains`, 'Domain')
2159
+ : undefined
2160
+ case 'upgrade':
2161
+ if (sub === undefined) return entry('system', 'Upgrade')
2162
+ return sub === 'check' && flags.has('--fix') ? entry('system', 'UpgradeFix') : undefined
2163
+ case 'maintenance':
2164
+ if (sub === 'pause') return entry('system', 'PauseEditing')
2165
+ return sub === 'resume' ? entry('system', 'ResumeEditing') : undefined
2166
+ default:
2167
+ return undefined
2168
+ }
2169
+ }
2170
+
2122
2171
  async function dispatch(): Promise<void> {
2123
2172
  switch (command) {
2124
2173
  case 'start':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crombolo/cli",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "The crombolo command line: scaffolding a site, schema, views, domains, content transfer and upgrades.",
5
5
  "license": "MIT",
6
6
  "author": "Crombolo",
@@ -34,11 +34,11 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@crombolo/auth": "0.10.0",
38
- "@crombolo/core": "0.10.0",
39
- "@crombolo/data": "0.10.0",
40
- "@crombolo/schema": "0.10.0",
41
- "@crombolo/server": "0.10.0",
42
- "@crombolo/transfer": "0.10.0"
37
+ "@crombolo/auth": "0.11.0",
38
+ "@crombolo/core": "0.11.0",
39
+ "@crombolo/data": "0.11.0",
40
+ "@crombolo/schema": "0.11.0",
41
+ "@crombolo/server": "0.11.0",
42
+ "@crombolo/transfer": "0.11.0"
43
43
  }
44
44
  }
@@ -5,8 +5,10 @@
5
5
  * a platform's own tooling — gets the same results as data. One implementation,
6
6
  * so the two can never disagree about what an import or an upgrade does.
7
7
  */
8
+ import { userInfo } from 'node:os'
8
9
  import { ROOT_ACCESS } from '@crombolo/core'
9
10
  import {
11
+ type AuditCategory,
10
12
  bundleStateKey,
11
13
  bundleTablesIn,
12
14
  connect,
@@ -52,6 +54,7 @@ import {
52
54
  planBundleUpgrades,
53
55
  readBlobs,
54
56
  readBundle,
57
+ recordAudit,
55
58
  syncOptionsFor,
56
59
  type TransferInput,
57
60
  VERSION,
@@ -565,6 +568,44 @@ export async function backup(
565
568
  })
566
569
  }
567
570
 
571
+ export interface CommandAudit {
572
+ category: AuditCategory
573
+ action: string
574
+ /** The command as typed, without its arguments, which can hold a password. */
575
+ command: string
576
+ entityType?: string
577
+ entityKey?: string
578
+ }
579
+
580
+ /**
581
+ * Records a command that changed this environment, in the audit log, as the
582
+ * account that ran it — `CROMBOLO_ACTOR` when a pipeline names who it runs for.
583
+ * Never fails the command: what it records has already happened.
584
+ */
585
+ export async function recordCommand(config: CromboloConfig, entry: CommandAudit): Promise<void> {
586
+ let actor = Bun.env.CROMBOLO_ACTOR
587
+ try {
588
+ actor ??= userInfo().username
589
+ } catch {
590
+ actor ??= undefined
591
+ }
592
+ try {
593
+ await withDatabase(config, (db) =>
594
+ recordAudit(db, {
595
+ category: entry.category,
596
+ action: entry.action,
597
+ source: 'cli',
598
+ userName: actor ?? null,
599
+ entityType: entry.entityType ?? null,
600
+ entityKey: entry.entityKey ?? null,
601
+ detail: entry.command,
602
+ }),
603
+ )
604
+ } catch {
605
+ // No database to write to, or one without the log yet: the command still happened.
606
+ }
607
+ }
608
+
568
609
  async function withDatabase<T>(config: CromboloConfig, use: (db: Db) => Promise<T>): Promise<T> {
569
610
  const db = await connect({ file: config.sqliteFile })
570
611
  try {
@@ -6,7 +6,7 @@
6
6
  "content"
7
7
  ],
8
8
  "peerDependencies": {
9
- "@crombolo/crombolo": "^0.10.0"
9
+ "@crombolo/crombolo": "^0.11.0"
10
10
  },
11
11
  "crombolo": {
12
12
  "id": "basic",
@@ -6,7 +6,7 @@
6
6
  "content"
7
7
  ],
8
8
  "peerDependencies": {
9
- "@crombolo/crombolo": "^0.10.0"
9
+ "@crombolo/crombolo": "^0.11.0"
10
10
  },
11
11
  "crombolo": {
12
12
  "id": "demo-harbourstone",
@@ -6,7 +6,7 @@
6
6
  "content"
7
7
  ],
8
8
  "peerDependencies": {
9
- "@crombolo/crombolo": "^0.10.0"
9
+ "@crombolo/crombolo": "^0.11.0"
10
10
  },
11
11
  "crombolo": {
12
12
  "id": "starter",