@cavelang/cli 0.33.0 → 0.34.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/src/cli.ts CHANGED
@@ -41,8 +41,8 @@ import { parseArgs } from 'node:util'
41
41
  import { Version } from '@cavelang/core'
42
42
  import { parseDocument, Token } from '@cavelang/parser'
43
43
  import { Registry, standardRegistry } from '@cavelang/canonical'
44
- import { Sensitivity, backup as backupStore, defaultDbPath, open, restoreBackup, verifyBackup } from '@cavelang/store'
45
- import type { Store } from '@cavelang/store'
44
+ import { LocateError, Sensitivity, backup as backupStore, defaultDbPath, openAt, restoreBackup, verifyBackup } from '@cavelang/store'
45
+ import type { OpenIntent, Store } from '@cavelang/store'
46
46
  import { defaultLimit as defaultQueryLimit, maxLimit as maxQueryLimit, page as caveQueryPage } from '@cavelang/query'
47
47
  import {
48
48
  check as caveCheck, defaultMinScore, defaultStaleDays, gatedIngest, judgePrompt, parseJudgeReply,
@@ -142,10 +142,12 @@ Usage:
142
142
  cave help [command] this text, or one command's options and examples
143
143
 
144
144
  Every command answers --help. --db defaults to $CAVE_DB, or cave.db in
145
- the current directory. The spec lives in the .claude/skills/ directory at
145
+ the current directory, and names a SQLite store or a CAVE text file: text
146
+ replays into an in-memory store for commands that only read (writes refuse
147
+ it), and a read never creates a missing database. The spec lives in the .claude/skills/ directory at
146
148
  the repository root (section index in README.md).`
147
149
 
148
- const dbHelp = `--db <path> database file (default: $CAVE_DB, or cave.db)`
150
+ const dbHelp = `--db <path> store: a SQLite file, or CAVE text read into memory (default: $CAVE_DB, or cave.db)`
149
151
 
150
152
  /** Per-command help, printed for \`cave <command> --help\` and \`cave help <command>\`. */
151
153
  export const commandHelp: Record<string, string> = {
@@ -286,6 +288,7 @@ with the answer; fully bound patterns print the raw line, comment included.
286
288
 
287
289
  Examples:
288
290
  cave query '?x USES jwt'
291
+ cave query --db notes.cave '?x USES jwt'
289
292
  cave query --db k.db '?x HAS bug: ?bug #security'
290
293
  cave query --db k.db '?cause CAUSE app/crash' 'WHERE conf >= 0.5'
291
294
  cave query --db k.db 'terrier EXTENDS+ animal'
@@ -782,6 +785,21 @@ const readStdin = (): string => {
782
785
  }
783
786
  }
784
787
 
788
+ /**
789
+ * Opens the store `--db` names (spec §13.7): a SQLite file, or a CAVE text
790
+ * file replayed into memory. `read` never creates, migrates, or changes a
791
+ * file — a missing path is an error, not an empty database; `scratch` is a
792
+ * dry run that appends inside a rolled-back transaction, so it may write
793
+ * but creates and migrates nothing; `write` creates a missing SQLite store,
794
+ * migrates an older one, and refuses text files with the materialization
795
+ * hint.
796
+ */
797
+ const openDb = (values: { db?: string, 'no-prelude'?: boolean }, intent: OpenIntent): Store =>
798
+ openAt(values.db ?? defaultDbPath(), {
799
+ intent,
800
+ ...values['no-prelude'] === true ? { registry: Registry.empty } : {}
801
+ })
802
+
785
803
  const readInput = (files: readonly string[]): string =>
786
804
  files.length === 0 || (files.length === 1 && files[0] === '-') ?
787
805
  readStdin() :
@@ -865,7 +883,7 @@ const ingestCommand = (name: 'add' | 'import') => (argv: readonly string[]): Out
865
883
  allowPositionals: true
866
884
  })
867
885
  const input = readInput(positionals)
868
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
886
+ const store = openDb(values, 'write')
869
887
  try {
870
888
  const options = {
871
889
  strict: values.strict === true,
@@ -932,7 +950,7 @@ export const queryCommand = (argv: readonly string[]): Output => {
932
950
  return fail('cave query: a pattern is required (spec §12.1)\n')
933
951
  }
934
952
  const pattern = positionals.join('\n')
935
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
953
+ const store = openDb(values, 'read')
936
954
  try {
937
955
  const limit = values.limit === undefined ? defaultQueryLimit : Number(values.limit)
938
956
  const result = caveQueryPage(store, pattern, {
@@ -1007,7 +1025,7 @@ export const searchCommand = (argv: readonly string[]): Output => {
1007
1025
  if (!Number.isInteger(limit) || limit < 1 || limit > maxQueryLimit) {
1008
1026
  return fail(`cave search: --limit must be an integer from 1 to ${maxQueryLimit}\n`)
1009
1027
  }
1010
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1028
+ const store = openDb(values, 'read')
1011
1029
  try {
1012
1030
  const raw = values.raw === true
1013
1031
  const found = store.search(query, { raw, limit: limit + 1 })
@@ -1057,7 +1075,7 @@ export const resolveCommand = (argv: readonly string[]): Output => {
1057
1075
  allowPositionals: false
1058
1076
  })
1059
1077
  const percent = (value: number): string => `${Math.round(value * 1000) / 10}%`
1060
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1078
+ const store = openDb(values, 'read')
1061
1079
  try {
1062
1080
  if (values.policy === true) {
1063
1081
  const policy = store.resolutionPolicy()
@@ -1120,7 +1138,12 @@ export const deriveCommand = (argv: readonly string[]): Output => {
1120
1138
  if (maxPasses !== undefined && (!Number.isInteger(maxPasses) || maxPasses < 1)) {
1121
1139
  return fail(`cave derive: --max-passes expects a positive integer, got ${JSON.stringify(values['max-passes'])}\n`)
1122
1140
  }
1123
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1141
+ // The intent follows the branch order below: listing reads, retracting
1142
+ // writes, a dry run appends inside a rolled-back transaction.
1143
+ const store = openDb(values,
1144
+ values.list === true ? 'read' :
1145
+ values.retract !== undefined ? 'write' :
1146
+ values['dry-run'] === true ? 'scratch' : 'write')
1124
1147
  try {
1125
1148
  if (values.list === true) {
1126
1149
  const rules = listRules(store)
@@ -1274,7 +1297,14 @@ export const actCommand = (argv: readonly string[]): Output => {
1274
1297
  },
1275
1298
  allowPositionals: true
1276
1299
  })
1277
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1300
+ // The intent follows the branch order below: declaring and retracting
1301
+ // write, listing reads, a dry-run execution appends inside a rolled-back
1302
+ // transaction.
1303
+ const store = openDb(values,
1304
+ values.declare === true ? 'write' :
1305
+ values.list === true ? 'read' :
1306
+ values.retract !== undefined ? 'write' :
1307
+ values['dry-run'] === true ? 'scratch' : 'write')
1278
1308
  try {
1279
1309
  if (values.declare === true) {
1280
1310
  const declaration = declareActions(store, readInput(positionals))
@@ -1395,7 +1425,7 @@ export const checkCommand = (argv: readonly string[]): Output => {
1395
1425
  if (!Number.isFinite(staleDays) || staleDays < 0) {
1396
1426
  return fail(`cave check: --stale expects a non-negative number of days, got ${JSON.stringify(values.stale)}\n`)
1397
1427
  }
1398
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1428
+ const store = openDb(values, 'read')
1399
1429
  try {
1400
1430
  const report = caveCheck(store, { staleDays })
1401
1431
  const out = values.json === true ?
@@ -1444,7 +1474,15 @@ export const backupCommand = (argv: readonly string[]): Output => {
1444
1474
  if (values.out === undefined) {
1445
1475
  return fail('cave backup: --out <file> is required\n')
1446
1476
  }
1447
- const store = open(values.db ?? defaultDbPath())
1477
+ const dbPath = values.db ?? defaultDbPath()
1478
+ // A text store is replayed into memory, so the backup API cannot see that
1479
+ // the destination aliases the source: refuse before the file could be
1480
+ // replaced by a SQLite snapshot of itself (a SQLite source is covered by
1481
+ // the API's own check).
1482
+ if (sameFile(dbPath, values.out)) {
1483
+ return fail(`cave backup: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1484
+ }
1485
+ const store = openAt(dbPath, { intent: 'read' })
1448
1486
  try {
1449
1487
  return ok(snapshotLine('created', backupStore(store, values.out, { force: values.force === true })))
1450
1488
  } finally {
@@ -1527,7 +1565,7 @@ export const suggestAliasCommand = async (
1527
1565
  if (timeoutSeconds !== undefined && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
1528
1566
  return fail(`cave suggest-alias: --timeout must be a positive number of seconds, got '${values.timeout}'\n`)
1529
1567
  }
1530
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1568
+ const store = openDb(values, values.write === true ? 'write' : 'read')
1531
1569
  try {
1532
1570
  let suggestions = suggestAliases(store, { minScore, ...limit === undefined ? {} : { limit } })
1533
1571
  if (values.agent !== undefined && suggestions.length > 0) {
@@ -1584,7 +1622,7 @@ export const syncCommand = (argv: readonly string[]): Output => {
1584
1622
  return fail('cave sync: exactly one source is required — a CAVE store file, ;@-annotated text, or - for stdin (spec §28.5)\n')
1585
1623
  }
1586
1624
  const dbPath = values.db ?? defaultDbPath()
1587
- const store = open(dbPath, values['no-prelude'] === true ? { registry: Registry.empty } : {})
1625
+ const store = openDb(values, values['dry-run'] === true ? 'scratch' : 'write')
1588
1626
  try {
1589
1627
  const options = {
1590
1628
  into: values.into === undefined ? labelOf(dbPath) : sanitizeLabel(values.into),
@@ -1645,7 +1683,10 @@ export const reportCommand = (argv: readonly string[]): Output => {
1645
1683
  if (maximum === undefined) {
1646
1684
  return fail(`cave report: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`)
1647
1685
  }
1648
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1686
+ if (values.out !== undefined && sameFile(values.db ?? defaultDbPath(), values.out)) {
1687
+ return fail(`cave report: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1688
+ }
1689
+ const store = openDb(values, 'read')
1649
1690
  try {
1650
1691
  const rendered = caveReport(store, template, {
1651
1692
  aliases: values.aliases === true,
@@ -1713,7 +1754,7 @@ export const exportCommand = (argv: readonly string[]): Output => {
1713
1754
  if (values.out !== undefined && sameFile(db, values.out)) {
1714
1755
  return fail(`cave export: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1715
1756
  }
1716
- const store = open(db, values['no-prelude'] === true ? { registry: Registry.empty } : {})
1757
+ const store = openDb(values, 'read')
1717
1758
  try {
1718
1759
  const text = store.exportText({ current: values.current === true, tx: values.tx === true, maxSensitivity: maximum })
1719
1760
  if (values.out === undefined) {
@@ -1752,7 +1793,7 @@ export const generateCommand = (argv: readonly string[]): Output => {
1752
1793
  if (values.out !== undefined && sameFile(db, values.out)) {
1753
1794
  return fail(`cave generate: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1754
1795
  }
1755
- const store = open(db, values['no-prelude'] === true ? { registry: Registry.empty } : {})
1796
+ const store = openDb(values, 'read')
1756
1797
  try {
1757
1798
  const generated = generateClient(store, version === undefined ? {} : { version })
1758
1799
  if (!generated.ok) {
@@ -1817,7 +1858,7 @@ export const reconstructCommand = async (
1817
1858
  if (timeoutSeconds !== undefined && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
1818
1859
  return fail(`cave reconstruct: --timeout must be a positive number of seconds, got '${values.timeout}'\n`)
1819
1860
  }
1820
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1861
+ const store = openDb(values, 'read')
1821
1862
  try {
1822
1863
  const graph = sqliteStore(store)
1823
1864
  const result = values.agent === undefined ?
@@ -1902,6 +1943,19 @@ export const cave = (argv: readonly string[]): Output => {
1902
1943
  (rest.includes('--help') || rest.includes('-h'))) {
1903
1944
  return ok(`${commandHelp[canonical]}\n`)
1904
1945
  }
1946
+ try {
1947
+ return run(canonical, rest)
1948
+ } catch (error) {
1949
+ // A --db path that is not what the command needs (spec §13.7) is a
1950
+ // usage failure, reported the way dispatch reports thrown errors.
1951
+ if (error instanceof LocateError) {
1952
+ return fail(`cave ${canonical}: ${error.message}\n`)
1953
+ }
1954
+ throw error
1955
+ }
1956
+ }
1957
+
1958
+ const run = (canonical: undefined | string, rest: readonly string[]): Output => {
1905
1959
  switch (canonical) {
1906
1960
  case 'parse':
1907
1961
  return parseCommand(rest)
@@ -1948,6 +2002,6 @@ export const cave = (argv: readonly string[]): Output => {
1948
2002
  case '-h':
1949
2003
  return ok(`${usage}\n`)
1950
2004
  default:
1951
- return fail(`cave: unknown command ${JSON.stringify(command)}\n\n${usage}\n`, 2)
2005
+ return fail(`cave: unknown command ${JSON.stringify(canonical)}\n\n${usage}\n`, 2)
1952
2006
  }
1953
2007
  }
package/src/doctor.ts CHANGED
@@ -249,7 +249,7 @@ const inspectDatabase = (path: string, source: DatabaseConfiguration['source']):
249
249
  configuration,
250
250
  checks: [check('store.database', 'warn',
251
251
  `Database schema ${version} needs migration to schema ${Schema.currentVersion}`,
252
- 'Back up the database, then open it with a normal cave command to migrate it.')]
252
+ 'Close every user and copy the database file as a rollback point, then open it with a writing command such as cave add to migrate it; reads never migrate.')]
253
253
  }
254
254
  }
255
255