@cavelang/cli 0.34.0 → 0.36.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/README.md +225 -13
- package/dist/internal/act/action.js +2 -1
- package/dist/internal/act/action.js.map +1 -1
- package/dist/internal/act/declare.d.ts.map +1 -1
- package/dist/internal/act/declare.js +82 -53
- package/dist/internal/act/declare.js.map +1 -1
- package/dist/internal/act/engine.d.ts +4 -3
- package/dist/internal/act/engine.d.ts.map +1 -1
- package/dist/internal/act/engine.js +121 -32
- package/dist/internal/act/engine.js.map +1 -1
- package/dist/internal/automate/cancellation.d.ts +3 -0
- package/dist/internal/automate/cancellation.d.ts.map +1 -0
- package/dist/internal/automate/cancellation.js +38 -0
- package/dist/internal/automate/cancellation.js.map +1 -0
- package/dist/internal/automate/declare.d.ts.map +1 -1
- package/dist/internal/automate/declare.js +49 -24
- package/dist/internal/automate/declare.js.map +1 -1
- package/dist/internal/automate/engine.d.ts +7 -3
- package/dist/internal/automate/engine.d.ts.map +1 -1
- package/dist/internal/automate/engine.js +196 -93
- package/dist/internal/automate/engine.js.map +1 -1
- package/dist/internal/automate/error-message.d.ts +3 -0
- package/dist/internal/automate/error-message.d.ts.map +1 -0
- package/dist/internal/automate/error-message.js +10 -0
- package/dist/internal/automate/error-message.js.map +1 -0
- package/dist/internal/automate/main.d.ts.map +1 -1
- package/dist/internal/automate/main.js +240 -35
- package/dist/internal/automate/main.js.map +1 -1
- package/dist/internal/automate/options.d.ts +4 -0
- package/dist/internal/automate/options.d.ts.map +1 -0
- package/dist/internal/automate/options.js +19 -0
- package/dist/internal/automate/options.js.map +1 -0
- package/dist/internal/connect/declared.d.ts +204 -0
- package/dist/internal/connect/declared.d.ts.map +1 -0
- package/dist/internal/connect/declared.js +610 -0
- package/dist/internal/connect/declared.js.map +1 -0
- package/dist/internal/connect/error-message.d.ts +3 -0
- package/dist/internal/connect/error-message.d.ts.map +1 -0
- package/dist/internal/connect/error-message.js +10 -0
- package/dist/internal/connect/error-message.js.map +1 -0
- package/dist/internal/connect/federation.d.ts +9 -0
- package/dist/internal/connect/federation.d.ts.map +1 -0
- package/dist/internal/connect/federation.js +19 -0
- package/dist/internal/connect/federation.js.map +1 -0
- package/dist/internal/connect/fetch-failure.d.ts +3 -0
- package/dist/internal/connect/fetch-failure.d.ts.map +1 -0
- package/dist/internal/connect/fetch-failure.js +36 -0
- package/dist/internal/connect/fetch-failure.js.map +1 -0
- package/dist/internal/connect/index.d.ts +4 -2
- package/dist/internal/connect/index.d.ts.map +1 -1
- package/dist/internal/connect/index.js +3 -1
- package/dist/internal/connect/index.js.map +1 -1
- package/dist/internal/connect/main.d.ts +1 -0
- package/dist/internal/connect/main.d.ts.map +1 -1
- package/dist/internal/connect/main.js +575 -59
- package/dist/internal/connect/main.js.map +1 -1
- package/dist/internal/connect/options.d.ts +3 -0
- package/dist/internal/connect/options.d.ts.map +1 -0
- package/dist/internal/connect/options.js +9 -0
- package/dist/internal/connect/options.js.map +1 -0
- package/dist/internal/connect/records.d.ts +3 -0
- package/dist/internal/connect/records.d.ts.map +1 -0
- package/dist/internal/connect/records.js +21 -0
- package/dist/internal/connect/records.js.map +1 -0
- package/dist/internal/connect/run.d.ts +48 -19
- package/dist/internal/connect/run.d.ts.map +1 -1
- package/dist/internal/connect/run.js +185 -77
- package/dist/internal/connect/run.js.map +1 -1
- package/dist/internal/connect/source.d.ts +36 -3
- package/dist/internal/connect/source.d.ts.map +1 -1
- package/dist/internal/connect/source.js +378 -39
- package/dist/internal/connect/source.js.map +1 -1
- package/dist/internal/connect/template.d.ts +20 -2
- package/dist/internal/connect/template.d.ts.map +1 -1
- package/dist/internal/connect/template.js +53 -9
- package/dist/internal/connect/template.js.map +1 -1
- package/dist/internal/connect/utf8.d.ts +3 -0
- package/dist/internal/connect/utf8.d.ts.map +1 -0
- package/dist/internal/connect/utf8.js +12 -0
- package/dist/internal/connect/utf8.js.map +1 -0
- package/dist/internal/eval/cleanup.d.ts +5 -0
- package/dist/internal/eval/cleanup.d.ts.map +1 -0
- package/dist/internal/eval/cleanup.js +50 -0
- package/dist/internal/eval/cleanup.js.map +1 -0
- package/dist/internal/eval/error-message.d.ts +3 -0
- package/dist/internal/eval/error-message.d.ts.map +1 -0
- package/dist/internal/eval/error-message.js +10 -0
- package/dist/internal/eval/error-message.js.map +1 -0
- package/dist/internal/eval/judge.d.ts.map +1 -1
- package/dist/internal/eval/judge.js +3 -31
- package/dist/internal/eval/judge.js.map +1 -1
- package/dist/internal/eval/loop.d.ts.map +1 -1
- package/dist/internal/eval/loop.js +18 -4
- package/dist/internal/eval/loop.js.map +1 -1
- package/dist/internal/eval/main.d.ts.map +1 -1
- package/dist/internal/eval/main.js +22 -10
- package/dist/internal/eval/main.js.map +1 -1
- package/dist/internal/eval/options.d.ts +6 -0
- package/dist/internal/eval/options.d.ts.map +1 -0
- package/dist/internal/eval/options.js +31 -0
- package/dist/internal/eval/options.js.map +1 -0
- package/dist/internal/eval/queries.d.ts.map +1 -1
- package/dist/internal/eval/queries.js +9 -6
- package/dist/internal/eval/queries.js.map +1 -1
- package/dist/internal/eval/run.d.ts.map +1 -1
- package/dist/internal/eval/run.js +112 -24
- package/dist/internal/eval/run.js.map +1 -1
- package/dist/internal/eval/score.d.ts.map +1 -1
- package/dist/internal/eval/score.js +9 -2
- package/dist/internal/eval/score.js.map +1 -1
- package/dist/internal/eval/suite.d.ts.map +1 -1
- package/dist/internal/eval/suite.js +14 -2
- package/dist/internal/eval/suite.js.map +1 -1
- package/dist/internal/ingest/cleanup.d.ts +5 -0
- package/dist/internal/ingest/cleanup.d.ts.map +1 -0
- package/dist/internal/ingest/cleanup.js +50 -0
- package/dist/internal/ingest/cleanup.js.map +1 -0
- package/dist/internal/ingest/content.d.ts +6 -0
- package/dist/internal/ingest/content.d.ts.map +1 -0
- package/dist/internal/ingest/content.js +25 -0
- package/dist/internal/ingest/content.js.map +1 -0
- package/dist/internal/ingest/context.d.ts.map +1 -1
- package/dist/internal/ingest/context.js +25 -7
- package/dist/internal/ingest/context.js.map +1 -1
- package/dist/internal/ingest/error-message.d.ts +3 -0
- package/dist/internal/ingest/error-message.d.ts.map +1 -0
- package/dist/internal/ingest/error-message.js +10 -0
- package/dist/internal/ingest/error-message.js.map +1 -0
- package/dist/internal/ingest/files.d.ts +4 -12
- package/dist/internal/ingest/files.d.ts.map +1 -1
- package/dist/internal/ingest/files.js +22 -10
- package/dist/internal/ingest/files.js.map +1 -1
- package/dist/internal/ingest/main.d.ts.map +1 -1
- package/dist/internal/ingest/main.js +48 -17
- package/dist/internal/ingest/main.js.map +1 -1
- package/dist/internal/ingest/options.d.ts +6 -0
- package/dist/internal/ingest/options.d.ts.map +1 -0
- package/dist/internal/ingest/options.js +27 -0
- package/dist/internal/ingest/options.js.map +1 -0
- package/dist/internal/ingest/prompt.d.ts.map +1 -1
- package/dist/internal/ingest/prompt.js +14 -10
- package/dist/internal/ingest/prompt.js.map +1 -1
- package/dist/internal/ingest/run.d.ts +9 -2
- package/dist/internal/ingest/run.d.ts.map +1 -1
- package/dist/internal/ingest/run.js +206 -62
- package/dist/internal/ingest/run.js.map +1 -1
- package/dist/internal/ingest/timeout.d.ts +3 -0
- package/dist/internal/ingest/timeout.d.ts.map +1 -0
- package/dist/internal/ingest/timeout.js +14 -0
- package/dist/internal/ingest/timeout.js.map +1 -0
- package/dist/internal/ingest/web.d.ts +2 -11
- package/dist/internal/ingest/web.d.ts.map +1 -1
- package/dist/internal/ingest/web.js +131 -25
- package/dist/internal/ingest/web.js.map +1 -1
- package/dist/internal/loop/budgets.d.ts +3 -0
- package/dist/internal/loop/budgets.d.ts.map +1 -0
- package/dist/internal/loop/budgets.js +10 -0
- package/dist/internal/loop/budgets.js.map +1 -0
- package/dist/internal/loop/index.d.ts +2 -0
- package/dist/internal/loop/index.d.ts.map +1 -1
- package/dist/internal/loop/index.js +2 -0
- package/dist/internal/loop/index.js.map +1 -1
- package/dist/internal/loop/json-answer.d.ts +7 -0
- package/dist/internal/loop/json-answer.d.ts.map +1 -0
- package/dist/internal/loop/json-answer.js +79 -0
- package/dist/internal/loop/json-answer.js.map +1 -0
- package/dist/internal/loop/json-spans.d.ts +3 -0
- package/dist/internal/loop/json-spans.d.ts.map +1 -0
- package/dist/internal/loop/json-spans.js +88 -0
- package/dist/internal/loop/json-spans.js.map +1 -0
- package/dist/internal/loop/llm.d.ts +3 -2
- package/dist/internal/loop/llm.d.ts.map +1 -1
- package/dist/internal/loop/llm.js +115 -32
- package/dist/internal/loop/llm.js.map +1 -1
- package/dist/internal/loop/process.d.ts +3 -1
- package/dist/internal/loop/process.d.ts.map +1 -1
- package/dist/internal/loop/process.js +159 -59
- package/dist/internal/loop/process.js.map +1 -1
- package/dist/internal/loop/read-snapshot.d.ts +4 -0
- package/dist/internal/loop/read-snapshot.d.ts.map +1 -0
- package/dist/internal/loop/read-snapshot.js +29 -0
- package/dist/internal/loop/read-snapshot.js.map +1 -0
- package/dist/internal/loop/reconstruct.d.ts +6 -5
- package/dist/internal/loop/reconstruct.d.ts.map +1 -1
- package/dist/internal/loop/reconstruct.js +19 -8
- package/dist/internal/loop/reconstruct.js.map +1 -1
- package/dist/internal/loop/score-settings.d.ts +5 -0
- package/dist/internal/loop/score-settings.d.ts.map +1 -0
- package/dist/internal/loop/score-settings.js +14 -0
- package/dist/internal/loop/score-settings.js.map +1 -0
- package/dist/internal/loop/sqlite.d.ts.map +1 -1
- package/dist/internal/loop/sqlite.js +15 -7
- package/dist/internal/loop/sqlite.js.map +1 -1
- package/dist/internal/loop/store.d.ts.map +1 -1
- package/dist/internal/loop/store.js +11 -7
- package/dist/internal/loop/store.js.map +1 -1
- package/dist/internal/loop/timeout.d.ts +3 -0
- package/dist/internal/loop/timeout.d.ts.map +1 -0
- package/dist/internal/loop/timeout.js +13 -0
- package/dist/internal/loop/timeout.js.map +1 -0
- package/dist/internal/mcp/error-message.d.ts +3 -0
- package/dist/internal/mcp/error-message.d.ts.map +1 -0
- package/dist/internal/mcp/error-message.js +10 -0
- package/dist/internal/mcp/error-message.js.map +1 -0
- package/dist/internal/mcp/main.d.ts.map +1 -1
- package/dist/internal/mcp/main.js +44 -27
- package/dist/internal/mcp/main.js.map +1 -1
- package/dist/internal/mcp/read-snapshot.d.ts +4 -0
- package/dist/internal/mcp/read-snapshot.d.ts.map +1 -0
- package/dist/internal/mcp/read-snapshot.js +22 -0
- package/dist/internal/mcp/read-snapshot.js.map +1 -0
- package/dist/internal/mcp/server.d.ts.map +1 -1
- package/dist/internal/mcp/server.js +181 -21
- package/dist/internal/mcp/server.js.map +1 -1
- package/dist/internal/mcp/source.d.ts +3 -0
- package/dist/internal/mcp/source.d.ts.map +1 -0
- package/dist/internal/mcp/source.js +13 -0
- package/dist/internal/mcp/source.js.map +1 -0
- package/dist/internal/mcp/tools.d.ts.map +1 -1
- package/dist/internal/mcp/tools.js +135 -72
- package/dist/internal/mcp/tools.js.map +1 -1
- package/dist/internal/rules/declarations.d.ts +6 -0
- package/dist/internal/rules/declarations.d.ts.map +1 -0
- package/dist/internal/rules/declarations.js +14 -0
- package/dist/internal/rules/declarations.js.map +1 -0
- package/dist/internal/rules/declare.d.ts.map +1 -1
- package/dist/internal/rules/declare.js +30 -16
- package/dist/internal/rules/declare.js.map +1 -1
- package/dist/internal/rules/engine.d.ts +8 -7
- package/dist/internal/rules/engine.d.ts.map +1 -1
- package/dist/internal/rules/engine.js +150 -44
- package/dist/internal/rules/engine.js.map +1 -1
- package/dist/internal/rules/rule.d.ts.map +1 -1
- package/dist/internal/rules/rule.js +1 -22
- package/dist/internal/rules/rule.js.map +1 -1
- package/dist/internal/shape/alias-root.d.ts +3 -0
- package/dist/internal/shape/alias-root.d.ts.map +1 -0
- package/dist/internal/shape/alias-root.js +13 -0
- package/dist/internal/shape/alias-root.js.map +1 -0
- package/dist/internal/shape/check.d.ts +15 -3
- package/dist/internal/shape/check.d.ts.map +1 -1
- package/dist/internal/shape/check.js +230 -100
- package/dist/internal/shape/check.js.map +1 -1
- package/dist/internal/shape/client.d.ts.map +1 -1
- package/dist/internal/shape/client.js +42 -20
- package/dist/internal/shape/client.js.map +1 -1
- package/dist/internal/shape/constraints.d.ts +8 -0
- package/dist/internal/shape/constraints.d.ts.map +1 -0
- package/dist/internal/shape/constraints.js +17 -0
- package/dist/internal/shape/constraints.js.map +1 -0
- package/dist/internal/shape/edit-distance.d.ts +3 -0
- package/dist/internal/shape/edit-distance.d.ts.map +1 -0
- package/dist/internal/shape/edit-distance.js +33 -0
- package/dist/internal/shape/edit-distance.js.map +1 -0
- package/dist/internal/shape/gate.d.ts.map +1 -1
- package/dist/internal/shape/gate.js +0 -0
- package/dist/internal/shape/gate.js.map +1 -1
- package/dist/internal/shape/snapshot.d.ts +4 -0
- package/dist/internal/shape/snapshot.d.ts.map +1 -0
- package/dist/internal/shape/snapshot.js +20 -0
- package/dist/internal/shape/snapshot.js.map +1 -0
- package/dist/internal/shape/suggest.d.ts +8 -6
- package/dist/internal/shape/suggest.d.ts.map +1 -1
- package/dist/internal/shape/suggest.js +208 -92
- package/dist/internal/shape/suggest.js.map +1 -1
- package/dist/internal/sync/error-message.d.ts +3 -0
- package/dist/internal/sync/error-message.d.ts.map +1 -0
- package/dist/internal/sync/error-message.js +10 -0
- package/dist/internal/sync/error-message.js.map +1 -0
- package/dist/internal/sync/index.d.ts +3 -2
- package/dist/internal/sync/index.d.ts.map +1 -1
- package/dist/internal/sync/index.js +3 -2
- package/dist/internal/sync/index.js.map +1 -1
- package/dist/internal/sync/sync.d.ts +8 -5
- package/dist/internal/sync/sync.d.ts.map +1 -1
- package/dist/internal/sync/sync.js +322 -65
- package/dist/internal/sync/sync.js.map +1 -1
- package/dist/internal/view/api.d.ts +3 -1
- package/dist/internal/view/api.d.ts.map +1 -1
- package/dist/internal/view/api.js +154 -90
- package/dist/internal/view/api.js.map +1 -1
- package/dist/internal/view/error-message.d.ts +3 -0
- package/dist/internal/view/error-message.d.ts.map +1 -0
- package/dist/internal/view/error-message.js +10 -0
- package/dist/internal/view/error-message.js.map +1 -0
- package/dist/internal/view/main.d.ts.map +1 -1
- package/dist/internal/view/main.js +63 -9
- package/dist/internal/view/main.js.map +1 -1
- package/dist/internal/view/page.d.ts.map +1 -1
- package/dist/internal/view/page.js +119 -20
- package/dist/internal/view/page.js.map +1 -1
- package/dist/internal/view/report.d.ts.map +1 -1
- package/dist/internal/view/report.js +180 -145
- package/dist/internal/view/report.js.map +1 -1
- package/dist/internal/view/scope.d.ts +6 -2
- package/dist/internal/view/scope.d.ts.map +1 -1
- package/dist/internal/view/scope.js +107 -24
- package/dist/internal/view/scope.js.map +1 -1
- package/dist/internal/view/sensitivity.d.ts +4 -0
- package/dist/internal/view/sensitivity.d.ts.map +1 -0
- package/dist/internal/view/sensitivity.js +11 -0
- package/dist/internal/view/sensitivity.js.map +1 -0
- package/dist/internal/view/server.d.ts.map +1 -1
- package/dist/internal/view/server.js +57 -20
- package/dist/internal/view/server.js.map +1 -1
- package/dist/src/atomic-output.d.ts +3 -0
- package/dist/src/atomic-output.d.ts.map +1 -0
- package/dist/src/atomic-output.js +67 -0
- package/dist/src/atomic-output.js.map +1 -0
- package/dist/src/cli.d.ts +17 -2
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +333 -195
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands.d.ts +2 -2
- package/dist/src/commands.js +2 -2
- package/dist/src/commands.js.map +1 -1
- package/dist/src/dispatch.d.ts.map +1 -1
- package/dist/src/dispatch.js +10 -3
- package/dist/src/dispatch.js.map +1 -1
- package/dist/src/doctor.d.ts +1 -1
- package/dist/src/doctor.d.ts.map +1 -1
- package/dist/src/doctor.js +192 -91
- package/dist/src/doctor.js.map +1 -1
- package/dist/src/error-message.d.ts +3 -0
- package/dist/src/error-message.d.ts.map +1 -0
- package/dist/src/error-message.js +20 -0
- package/dist/src/error-message.js.map +1 -0
- package/dist/src/hooks-config.d.ts +3 -0
- package/dist/src/hooks-config.d.ts.map +1 -0
- package/dist/src/hooks-config.js +23 -0
- package/dist/src/hooks-config.js.map +1 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/package.json +23 -20
- package/src/atomic-output.ts +47 -0
- package/src/cli.ts +345 -171
- package/src/commands.ts +2 -2
- package/src/dispatch.ts +10 -3
- package/src/doctor.ts +207 -100
- package/src/error-message.ts +13 -0
- package/src/hooks-config.ts +22 -0
- package/src/index.ts +1 -1
|
@@ -2,39 +2,65 @@
|
|
|
2
2
|
* `cave connect` entry — argument parsing, pass orchestration (single,
|
|
3
3
|
* `--watch`, `--query`), and report rendering around `run.ts`.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { rethrowFetchFailure } from "./fetch-failure.js";
|
|
6
|
+
import { errorMessage } from "./error-message.js";
|
|
7
|
+
import { existsSync, watch as watchFs } from 'node:fs';
|
|
8
|
+
import { readText } from "./utf8.js";
|
|
6
9
|
import { basename, dirname, resolve } from 'node:path';
|
|
7
10
|
import { parseArgs } from 'node:util';
|
|
8
11
|
import { Registry } from '@cavelang/canonical';
|
|
9
|
-
import { defaultDbPath, kindOf, open, openAt } from '@cavelang/store';
|
|
10
|
-
import { Record as QueryRecord } from '@cavelang/query';
|
|
12
|
+
import { LocateError, defaultDbPath, kindOf, open, openAt } from '@cavelang/store';
|
|
13
|
+
import { Record as QueryRecord, query as caveQuery } from '@cavelang/query';
|
|
14
|
+
import * as Declared from "./declared.js";
|
|
11
15
|
import * as Source from "./source.js";
|
|
12
16
|
import * as Template from "./template.js";
|
|
13
|
-
import { connect
|
|
17
|
+
import { connect } from "./run.js";
|
|
18
|
+
import { withFederatedSource } from "./federation.js";
|
|
14
19
|
const usage = `cave connect — deterministic structured ingestion through a mapping template (spec §23)
|
|
15
20
|
|
|
16
21
|
Usage:
|
|
17
22
|
cave connect [--db <path>] <source> --map <file> [options]
|
|
23
|
+
cave connect [--db <path>] [--name <n>] [--force] [--prune] [--dry-run] [--watch]
|
|
24
|
+
cave connect [--db <path>] --list
|
|
25
|
+
cave connect [--db <path>] --query '<pattern>'
|
|
18
26
|
|
|
19
27
|
The source is a .csv/.tsv/.json/.jsonl/.ndjson file, a SQLite database
|
|
20
|
-
(with --table or --sql), or an http(s) URL serving JSON or
|
|
28
|
+
(with --table or --sql), or an http(s) URL serving JSON, JSONL, CSV or TSV. The
|
|
21
29
|
mapping is an ordinary CAVE document whose ?field variables stand for
|
|
22
30
|
record fields; variable-free blocks append once per run, variable blocks
|
|
23
31
|
instantiate once per record — no LLM in the loop, same input, same claims.
|
|
32
|
+
A short mapping can be written inline, and --sql reshapes any tabular
|
|
33
|
+
source through SQLite before the mapping sees it.
|
|
24
34
|
Mapped claims retain the physical source; CSV/TSV and JSONL records also carry
|
|
25
35
|
their exact one-based inclusive line span (spec §9.8).
|
|
26
36
|
|
|
37
|
+
Without a source, the store's declared sources run (spec §23.4): every
|
|
38
|
+
source/<name> entity whose current claims name a path, with the other
|
|
39
|
+
options as attributes — HAS map:, key:, format:, delimiter:, table:, sql:,
|
|
40
|
+
records:. A .cave path needs no map (the file is its own template). Paths
|
|
41
|
+
resolve against the store's directory. Declared sources stamp
|
|
42
|
+
@src:<name>/<key> and keep digests under source/<name>/<key>, so the
|
|
43
|
+
source/<name> entity also carries the §26.3 policy for what it yields. A
|
|
44
|
+
CAVE text file used as --db follows its declared sources on every open.
|
|
45
|
+
|
|
27
46
|
Options:
|
|
28
47
|
--db <path> knowledge database (default: $CAVE_DB, or cave.db)
|
|
29
|
-
--map <file>
|
|
48
|
+
--map <file|inline> mapping template (required): a file, or the template
|
|
49
|
+
itself — a comma-separated list of claim lines,
|
|
50
|
+
'?name IS person, ?name WORKS-AT ?company'
|
|
30
51
|
--name <name> source name for record identity (default: file basename)
|
|
31
52
|
--key <field> record key field — keyed records diff against their
|
|
32
53
|
previous claims on change; unkeyed records are
|
|
33
54
|
content-addressed
|
|
34
55
|
--format <fmt> csv | tsv | json | jsonl | sqlite (default: by extension)
|
|
35
56
|
--delimiter <char> CSV field delimiter (default , — tab for .tsv)
|
|
36
|
-
--table <name> SQLite table to read (SELECT *)
|
|
37
|
-
|
|
57
|
+
--table <name> SQLite table to read (SELECT *); for other formats
|
|
58
|
+
the temporary table --sql runs against (records)
|
|
59
|
+
--sql <query> SQLite query (alternative to --table); for csv, tsv,
|
|
60
|
+
json and jsonl the records load into a temporary
|
|
61
|
+
in-memory table, values as parsed (CSV cells are
|
|
62
|
+
text: cast for arithmetic), and the query's rows are
|
|
63
|
+
the records
|
|
38
64
|
--records <path> dot path to the record array inside a JSON document
|
|
39
65
|
--force re-map records whose digest is unchanged
|
|
40
66
|
--prune retract claims of records that disappeared from the source
|
|
@@ -42,19 +68,40 @@ Options:
|
|
|
42
68
|
--watch keep running; re-map when the source or mapping changes
|
|
43
69
|
--query <pattern> federation-lite (spec §23.3): map, query the union,
|
|
44
70
|
roll back — nothing persists; uses an in-memory store
|
|
45
|
-
when the database file does not exist
|
|
71
|
+
when the database file does not exist; without a
|
|
72
|
+
source, overlays every declared source
|
|
73
|
+
--list print the declared sources as cave connect invocations
|
|
74
|
+
--name <name> without a source: run only this declared source
|
|
46
75
|
--json with --query: emit matches as JSON
|
|
47
76
|
--all with --query: match all beliefs, not just current ones
|
|
48
77
|
--aliases with --query: resolve entities through ALIAS claims
|
|
49
78
|
--no-prelude open the store without the standard §5.5 registry
|
|
50
79
|
|
|
51
80
|
Examples:
|
|
81
|
+
cave connect --db k.db --list
|
|
82
|
+
cave connect --db k.db
|
|
83
|
+
cave connect --db k.db --watch
|
|
84
|
+
cave query --db notes.cave '?who WORKS-AT acme' # a text store follows its sources
|
|
52
85
|
cave connect people.csv --map people.map.cave --db k.db --key id
|
|
53
86
|
cave connect crm.sqlite --table contacts --map contacts.map.cave --key email
|
|
54
87
|
cave connect https://api.example.com/deps.json --records data.items --map deps.map.cave
|
|
55
88
|
cave connect events.jsonl --map events.map.cave --watch
|
|
56
89
|
cave connect people.csv --map people.map.cave --query '?who WORKS-AT acme'`;
|
|
57
|
-
|
|
90
|
+
/** Abort and fatal callbacks share one owned completion boundary. */
|
|
91
|
+
const watchEnd = (signal) => {
|
|
92
|
+
let failure;
|
|
93
|
+
let wake;
|
|
94
|
+
const done = new Promise(resolve => { wake = resolve; });
|
|
95
|
+
signal?.addEventListener('abort', wake, { once: true });
|
|
96
|
+
if (signal?.aborted)
|
|
97
|
+
wake();
|
|
98
|
+
return {
|
|
99
|
+
fail(error) { failure ??= { error }; wake(); },
|
|
100
|
+
async wait() { await done; if (failure !== undefined)
|
|
101
|
+
throw failure.error; },
|
|
102
|
+
close() { signal?.removeEventListener('abort', wake); }
|
|
103
|
+
};
|
|
104
|
+
};
|
|
58
105
|
const renderReport = (report) => {
|
|
59
106
|
const lines = [
|
|
60
107
|
`connect: ${report.records} record(s): ${report.mapped} mapped, ${report.skipped} skipped (unchanged)` +
|
|
@@ -69,25 +116,35 @@ const renderReport = (report) => {
|
|
|
69
116
|
}
|
|
70
117
|
for (const failure of report.failures) {
|
|
71
118
|
lines.push(` ${failure.record}: FAILED`);
|
|
72
|
-
|
|
119
|
+
for (const problem of failure.problems)
|
|
120
|
+
lines.push(` ${problem}`);
|
|
73
121
|
}
|
|
74
122
|
return lines.join('\n');
|
|
75
123
|
};
|
|
76
|
-
|
|
124
|
+
/** `--map` is a template file, or the template itself written inline (spec §23.1). */
|
|
125
|
+
const loadMapping = (spec) => Template.isInline(spec) && !existsSync(spec) ?
|
|
126
|
+
Template.parse(Template.inlineDocument(spec)) :
|
|
127
|
+
Template.parse(readText(spec));
|
|
77
128
|
const sourceOptions = (values, context) => ({
|
|
78
129
|
...values.format === undefined ? {} : { format: values.format },
|
|
79
130
|
...values.delimiter === undefined ? {} : { delimiter: values.delimiter },
|
|
80
131
|
...values.table === undefined ? {} : { table: values.table },
|
|
81
132
|
...values.sql === undefined ? {} : { sql: values.sql },
|
|
82
133
|
...values.records === undefined ? {} : { records: values.records },
|
|
134
|
+
...context.signal === undefined ? {} : { signal: context.signal },
|
|
83
135
|
...context.fetchImpl === undefined ? {} : { fetchImpl: context.fetchImpl }
|
|
84
136
|
});
|
|
85
137
|
const loadSource = async (source, values, context) => {
|
|
86
138
|
try {
|
|
87
|
-
|
|
139
|
+
context.signal?.throwIfAborted();
|
|
140
|
+
const loaded = await Source.load(source, sourceOptions(values, context));
|
|
141
|
+
context.signal?.throwIfAborted();
|
|
142
|
+
return loaded;
|
|
88
143
|
}
|
|
89
144
|
catch (error) {
|
|
90
|
-
|
|
145
|
+
if (context.signal?.aborted)
|
|
146
|
+
rethrowFetchFailure(context.signal, error);
|
|
147
|
+
throw new Error(`load ${source}: ${errorMessage(error)}`, { cause: error });
|
|
91
148
|
}
|
|
92
149
|
};
|
|
93
150
|
const runPass = async (store, source, values, name, io, context) => {
|
|
@@ -135,6 +192,25 @@ const runDry = async (source, values, io, context) => {
|
|
|
135
192
|
io.stdout.write(`${sections.join('\n\n')}\n`);
|
|
136
193
|
return failures > 0 ? 1 : 0;
|
|
137
194
|
};
|
|
195
|
+
/** Own the command store until its work settles, preserving cleanup failures. */
|
|
196
|
+
const withCommandStore = async (store, run) => {
|
|
197
|
+
let result;
|
|
198
|
+
try {
|
|
199
|
+
result = await run();
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
try {
|
|
203
|
+
store.close();
|
|
204
|
+
}
|
|
205
|
+
catch (closeError) {
|
|
206
|
+
const messages = [error, closeError].map(errorMessage);
|
|
207
|
+
throw new AggregateError([error, closeError], `command failed: ${messages[0]}; store close also failed: ${messages[1]}`, { cause: error });
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
store.close();
|
|
212
|
+
return result;
|
|
213
|
+
};
|
|
138
214
|
const runQuery = async (source, values, name, io, context) => {
|
|
139
215
|
const { mapping, problems } = loadMapping(values.map);
|
|
140
216
|
if (mapping === undefined) {
|
|
@@ -145,14 +221,17 @@ const runQuery = async (source, values, name, io, context) => {
|
|
|
145
221
|
const db = values.db ?? defaultDbPath();
|
|
146
222
|
const registry = values['no-prelude'] === true ? { registry: Registry.empty } : {};
|
|
147
223
|
// Federation over a store that does not exist yet queries the source alone.
|
|
148
|
-
const store = kindOf(db) === 'missing' ? open(':memory:', registry) : openAt(db, { intent: 'scratch', ...registry });
|
|
149
|
-
|
|
150
|
-
const { matches, report } =
|
|
224
|
+
const store = kindOf(db) === 'missing' ? open(':memory:', registry) : openAt(db, { intent: 'scratch', assemble: Declared.assemble, ...registry });
|
|
225
|
+
return await withCommandStore(store, () => {
|
|
226
|
+
const { result: { matches, records }, report } = withFederatedSource(store, mapping, loaded.records, {
|
|
151
227
|
name,
|
|
152
228
|
source,
|
|
153
229
|
...loaded.spans === undefined ? {} : { spans: loaded.spans },
|
|
154
230
|
...values.key === undefined ? {} : { key: values.key }
|
|
155
|
-
},
|
|
231
|
+
}, () => {
|
|
232
|
+
const matches = caveQuery(store, values.query, { all: values.all === true, aliases: values.aliases === true });
|
|
233
|
+
return { matches, records: values.json === true ? matches.map(match => QueryRecord.of(store, match)) : undefined };
|
|
234
|
+
});
|
|
156
235
|
// A failed record means the union was incomplete — matches still print
|
|
157
236
|
// as partial results, but the exit code must not read as success.
|
|
158
237
|
const code = report.failures.length > 0 ? 1 : 0;
|
|
@@ -160,7 +239,7 @@ const runQuery = async (source, values, name, io, context) => {
|
|
|
160
239
|
io.stderr.write(`${renderReport(report)}\n`);
|
|
161
240
|
}
|
|
162
241
|
if (values.json === true) {
|
|
163
|
-
io.stdout.write(`${JSON.stringify(
|
|
242
|
+
io.stdout.write(`${JSON.stringify(records, undefined, 2)}\n`);
|
|
164
243
|
return code;
|
|
165
244
|
}
|
|
166
245
|
if (matches.length === 0) {
|
|
@@ -175,18 +254,55 @@ const runQuery = async (source, values, name, io, context) => {
|
|
|
175
254
|
});
|
|
176
255
|
io.stdout.write(`${lines.join('\n')}\n`);
|
|
177
256
|
return code;
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
/** Finish every owned watch resource before returning or propagating failures. */
|
|
260
|
+
const watchSession = async (run, stop, watchers, active) => {
|
|
261
|
+
const errors = [];
|
|
262
|
+
let result;
|
|
263
|
+
try {
|
|
264
|
+
result = await run();
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
errors.push(error);
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
stop();
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
errors.push(error);
|
|
274
|
+
}
|
|
275
|
+
for (const watcher of watchers()) {
|
|
276
|
+
try {
|
|
277
|
+
watcher.close();
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
errors.push(error);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
await active();
|
|
178
285
|
}
|
|
179
|
-
|
|
180
|
-
|
|
286
|
+
catch (error) {
|
|
287
|
+
errors.push(error);
|
|
288
|
+
}
|
|
289
|
+
if (errors.length === 1)
|
|
290
|
+
throw errors[0];
|
|
291
|
+
if (errors.length > 1) {
|
|
292
|
+
throw new AggregateError(errors, `watch session failed: ${errors.map(errorMessage).join('; ')}`, { cause: errors[0] });
|
|
181
293
|
}
|
|
294
|
+
return result;
|
|
182
295
|
};
|
|
183
296
|
const runWatch = async (store, source, values, name, io, context) => {
|
|
297
|
+
let stopped = false;
|
|
298
|
+
const end = watchEnd(io.signal);
|
|
299
|
+
const fail = (error) => { stopped = true; end.fail(error); };
|
|
184
300
|
const passOnce = async () => {
|
|
185
301
|
try {
|
|
186
302
|
await runPass(store, source, values, name, io, context);
|
|
187
303
|
}
|
|
188
304
|
catch (error) {
|
|
189
|
-
io.stderr.write(`cave connect watch pass: ${
|
|
305
|
+
io.stderr.write(`cave connect watch pass: ${errorMessage(error)}\n`);
|
|
190
306
|
}
|
|
191
307
|
};
|
|
192
308
|
let running = false;
|
|
@@ -199,63 +315,458 @@ const runWatch = async (store, source, values, name, io, context) => {
|
|
|
199
315
|
return;
|
|
200
316
|
}
|
|
201
317
|
running = true;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
318
|
+
try {
|
|
319
|
+
do {
|
|
320
|
+
queued = false;
|
|
321
|
+
await passOnce();
|
|
322
|
+
} while (queued && !stopped && !io.signal?.aborted);
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
running = false;
|
|
326
|
+
}
|
|
207
327
|
};
|
|
208
328
|
const schedule = context.schedule ?? ((callback, delayMs) => setTimeout(() => { void callback(); }, delayMs));
|
|
209
329
|
const cancelScheduled = context.cancelScheduled ?? (handle => clearTimeout(handle));
|
|
210
330
|
const trigger = () => {
|
|
331
|
+
if (stopped || io.signal?.aborted)
|
|
332
|
+
return;
|
|
211
333
|
if (running) {
|
|
212
334
|
queued = true;
|
|
213
335
|
return;
|
|
214
336
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
timer =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
active =
|
|
224
|
-
|
|
337
|
+
try {
|
|
338
|
+
if (timer !== undefined)
|
|
339
|
+
cancelScheduled(timer);
|
|
340
|
+
timer = schedule(async () => {
|
|
341
|
+
timer = undefined;
|
|
342
|
+
if (stopped)
|
|
343
|
+
return;
|
|
344
|
+
const pass = fire();
|
|
345
|
+
active = pass;
|
|
346
|
+
try {
|
|
347
|
+
await pass;
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
fail(error);
|
|
351
|
+
}
|
|
352
|
+
if (active === pass)
|
|
353
|
+
active = undefined;
|
|
354
|
+
}, 200);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
fail(error);
|
|
358
|
+
}
|
|
225
359
|
};
|
|
226
360
|
// Watch the parent directories — editors replace files on save, and a
|
|
227
361
|
// watcher on the file itself dies with the old inode.
|
|
228
|
-
const targets = [...new Set([resolve(source), resolve(values.map)])];
|
|
362
|
+
const targets = [...new Set([resolve(source), ...Template.isInline(values.map) && !existsSync(values.map) ? [] : [resolve(values.map)]])];
|
|
229
363
|
const watch = context.watch ?? ((path, listener) => watchFs(path, listener));
|
|
230
|
-
const watchers =
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
364
|
+
const watchers = [];
|
|
365
|
+
return watchSession(async () => {
|
|
366
|
+
for (const target of targets) {
|
|
367
|
+
const subscription = watch(dirname(target), (_event, filename) => {
|
|
368
|
+
// Filename-less events rescan; editors may replace the watched file.
|
|
369
|
+
if (filename === null || filename.toString() === basename(target))
|
|
370
|
+
trigger();
|
|
371
|
+
});
|
|
372
|
+
watchers.push(subscription);
|
|
373
|
+
subscription.on?.('error', error => { if (!stopped)
|
|
374
|
+
fail(error); });
|
|
375
|
+
}
|
|
239
376
|
// Install watchers before the initial pass. A save during startup is
|
|
240
377
|
// therefore either read by this pass or queued for the next one.
|
|
241
|
-
await
|
|
378
|
+
await fire();
|
|
379
|
+
if (stopped || io.signal?.aborted) {
|
|
380
|
+
await end.wait();
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
242
383
|
io.stdout.write('watching (ctrl-c to stop)\n');
|
|
243
|
-
await
|
|
384
|
+
await end.wait();
|
|
244
385
|
return 0;
|
|
386
|
+
}, () => {
|
|
387
|
+
stopped = true;
|
|
388
|
+
end.close();
|
|
389
|
+
if (timer !== undefined)
|
|
390
|
+
cancelScheduled(timer);
|
|
391
|
+
}, () => watchers, () => active);
|
|
392
|
+
};
|
|
393
|
+
const printMatches = (matches, pattern, io, records) => {
|
|
394
|
+
if (records !== undefined) {
|
|
395
|
+
io.stdout.write(`${JSON.stringify(records, undefined, 2)}\n`);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (matches.length === 0) {
|
|
399
|
+
io.stdout.write('no matches\n');
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const lines = matches.map(match => {
|
|
403
|
+
const bindings = Object.entries(match.bindings)
|
|
404
|
+
.map(([variable, value]) => `?${variable} = ${value}`)
|
|
405
|
+
.join(' ');
|
|
406
|
+
return bindings !== '' ? bindings : match.row?.raw_line ?? pattern;
|
|
407
|
+
});
|
|
408
|
+
io.stdout.write(`${lines.join('\n')}\n`);
|
|
409
|
+
};
|
|
410
|
+
const registryOf = (values) => values['no-prelude'] === true ? { registry: Registry.empty } : {};
|
|
411
|
+
/** The declared sources of the store, or the one `--name` selects. */
|
|
412
|
+
const selectDeclared = (store, values) => {
|
|
413
|
+
const declared = Declared.declaredSources(store);
|
|
414
|
+
if (values.name === undefined) {
|
|
415
|
+
return declared;
|
|
416
|
+
}
|
|
417
|
+
const selected = declared.filter(source => source.name === values.name);
|
|
418
|
+
if (selected.length === 0) {
|
|
419
|
+
throw new Error(`no declared source/${values.name}` +
|
|
420
|
+
(declared.length === 0 ? ' — the store declares no sources' : ` — declared: ${declared.map(source => source.name).join(', ')}`));
|
|
421
|
+
}
|
|
422
|
+
return selected;
|
|
423
|
+
};
|
|
424
|
+
const declaredPass = async (store, root, values, io, context) => {
|
|
425
|
+
const dir = Declared.directoryOf(root);
|
|
426
|
+
let failed = 0;
|
|
427
|
+
const done = new Map();
|
|
428
|
+
// `--name` selects one declaration and what it declares in turn. The
|
|
429
|
+
// declarations are re-read after every source: a followed .cave source
|
|
430
|
+
// may declare new sources or re-declare existing ones, and the current
|
|
431
|
+
// declaration is what runs, until nothing changes — as `assemble` does.
|
|
432
|
+
// The selection starts as the named source and everything it already
|
|
433
|
+
// owns, so a parent that fails to load does not hide its descendants.
|
|
434
|
+
const allowed = values.name === undefined ? undefined :
|
|
435
|
+
new Set(Declared.closure(store, selectDeclared(store, values).map(declared => declared.name)).map(declared => declared.name));
|
|
436
|
+
const version = Declared.versionCounter();
|
|
437
|
+
for (;;) {
|
|
438
|
+
io.signal?.throwIfAborted();
|
|
439
|
+
const next = Declared.declaredSources(store)
|
|
440
|
+
.find(declared => (allowed === undefined || allowed.has(declared.name)) && done.get(declared.name) !== Declared.signature(declared));
|
|
441
|
+
if (next === undefined) {
|
|
442
|
+
return failed > 0 ? 1 : 0;
|
|
443
|
+
}
|
|
444
|
+
version(next.name);
|
|
445
|
+
done.set(next.name, Declared.signature(next));
|
|
446
|
+
try {
|
|
447
|
+
const ready = await Declared.prepare(next, dir, context.fetchImpl, io.signal);
|
|
448
|
+
io.signal?.throwIfAborted();
|
|
449
|
+
const before = Declared.signatures(Declared.declaredSources(store));
|
|
450
|
+
const report = Declared.run(store, ready, { force: values.force === true, prune: values.prune === true });
|
|
451
|
+
if (allowed !== undefined) {
|
|
452
|
+
// The selection grows by whatever this source changed — added,
|
|
453
|
+
// re-declared, or retracted declarations alike — and by what it
|
|
454
|
+
// still owns, so an unchanged source keeps its descendants.
|
|
455
|
+
for (const name of Declared.changedNames(before, Declared.signatures(Declared.declaredSources(store))))
|
|
456
|
+
allowed.add(name);
|
|
457
|
+
for (const name of Declared.ownedDeclarations(store, next.name))
|
|
458
|
+
allowed.add(name);
|
|
459
|
+
}
|
|
460
|
+
io.stdout.write(`source/${next.name}: ${renderReport(report).replace(/^connect: /, '')}\n`);
|
|
461
|
+
if (report.failures.length > 0)
|
|
462
|
+
failed += 1;
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
io.signal?.throwIfAborted();
|
|
466
|
+
io.stderr.write(`source/${next.name} (${next.path}): ${errorMessage(error)}\n`);
|
|
467
|
+
failed += 1;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
const discoverOptions = (values, context, force = values.force === true) => ({
|
|
472
|
+
...values.name === undefined ? {} : { only: values.name },
|
|
473
|
+
...context.fetchImpl === undefined ? {} : { fetchImpl: context.fetchImpl },
|
|
474
|
+
...context.signal === undefined ? {} : { signal: context.signal },
|
|
475
|
+
force,
|
|
476
|
+
prune: values.prune === true
|
|
477
|
+
});
|
|
478
|
+
/** Previews every declared source, nested declarations included, without touching the store. */
|
|
479
|
+
const declaredDry = async (store, root, values, io, context) => {
|
|
480
|
+
const sections = [];
|
|
481
|
+
let failures = 0;
|
|
482
|
+
const sequence = await Declared.discover(store, root, discoverOptions(values, context));
|
|
483
|
+
io.signal?.throwIfAborted();
|
|
484
|
+
for (const ready of sequence) {
|
|
485
|
+
sections.push(`; === source/${ready.declared.name} (${ready.declared.path})`);
|
|
486
|
+
if (ready.mapping.prelude !== '') {
|
|
487
|
+
sections.push(`; --- prelude\n\n${ready.mapping.prelude.trimEnd()}`);
|
|
488
|
+
}
|
|
489
|
+
ready.records.forEach((record, at) => {
|
|
490
|
+
const instantiation = Template.instantiate(ready.mapping.templates, name => Template.fieldOf(record, name));
|
|
491
|
+
if (instantiation.problems.length > 0) {
|
|
492
|
+
failures += 1;
|
|
493
|
+
sections.push([`; --- record ${at + 1}`, ...instantiation.problems.map(problem => `; FAILED — ${problem}`)].join('\n'));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
sections.push(`; --- record ${at + 1}\n\n${instantiation.text.trimEnd()}`);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
io.stdout.write(`${sections.join('\n\n')}\n`);
|
|
500
|
+
return failures > 0 ? 1 : 0;
|
|
501
|
+
};
|
|
502
|
+
const rollback = Symbol('cave-connect declared query rollback');
|
|
503
|
+
/**
|
|
504
|
+
* Federation-lite over every declared source (spec §23.3, §23.4): the
|
|
505
|
+
* sources load first, URLs included, then append inside one transaction
|
|
506
|
+
* that rolls back after the query.
|
|
507
|
+
*/
|
|
508
|
+
const stale = Symbol('cave-connect declared query stale');
|
|
509
|
+
const declaredQuery = async (store, root, values, io, context) => {
|
|
510
|
+
// The overlay applies every source (force), so discovery simulates that.
|
|
511
|
+
// The sequence was discovered on a snapshot; a writer may have changed
|
|
512
|
+
// the declarations since, so the replay checks and rediscovers if so.
|
|
513
|
+
for (let attempt = 0; attempt < Declared.overlayAttempts; attempt += 1) {
|
|
514
|
+
const { sequence, baseline } = await Declared.discovery(store, root, discoverOptions(values, context, true));
|
|
515
|
+
io.signal?.throwIfAborted();
|
|
516
|
+
let matches;
|
|
517
|
+
let records;
|
|
518
|
+
let failed = 0;
|
|
519
|
+
try {
|
|
520
|
+
store.transaction(() => {
|
|
521
|
+
if (!Declared.sameDeclarations(baseline, Declared.declarationState(store))) {
|
|
522
|
+
throw stale;
|
|
523
|
+
}
|
|
524
|
+
for (const source of sequence) {
|
|
525
|
+
const report = Declared.run(store, source, { force: true, prune: values.prune === true });
|
|
526
|
+
if (report.failures.length > 0) {
|
|
527
|
+
failed += 1;
|
|
528
|
+
io.stderr.write(`source/${source.declared.name}: ${renderReport(report).replace(/^connect: /, '')}\n`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
matches = caveQuery(store, values.query, { all: values.all === true, aliases: values.aliases === true });
|
|
532
|
+
if (values.json === true)
|
|
533
|
+
records = matches.map(match => QueryRecord.of(store, match));
|
|
534
|
+
throw rollback;
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
if (error === stale) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (error !== rollback) {
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
printMatches(matches, values.query, io, records);
|
|
546
|
+
return failed > 0 ? 1 : 0;
|
|
245
547
|
}
|
|
246
|
-
|
|
548
|
+
throw Declared.staleOverlay();
|
|
549
|
+
};
|
|
550
|
+
const declaredWatch = async (store, root, values, io, context) => {
|
|
551
|
+
let stopped = false;
|
|
552
|
+
const end = watchEnd(io.signal);
|
|
553
|
+
const fail = (error) => { stopped = true; end.fail(error); };
|
|
554
|
+
const dir = Declared.directoryOf(root);
|
|
555
|
+
const passOnce = async () => {
|
|
556
|
+
try {
|
|
557
|
+
await declaredPass(store, root, values, io, context);
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
io.stderr.write(`cave connect watch pass: ${errorMessage(error)}\n`);
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
if (!stopped && !io.signal?.aborted)
|
|
564
|
+
refreshWatchers();
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
// Keep current subscriptions available to retry on the next save.
|
|
568
|
+
io.stderr.write(`cave connect watch subscriptions: ${errorMessage(error)}\n`);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
let running = false;
|
|
572
|
+
let queued = false;
|
|
573
|
+
let timer;
|
|
574
|
+
let active;
|
|
575
|
+
const fire = async () => {
|
|
576
|
+
if (running) {
|
|
577
|
+
queued = true;
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
running = true;
|
|
581
|
+
try {
|
|
582
|
+
do {
|
|
583
|
+
queued = false;
|
|
584
|
+
await passOnce();
|
|
585
|
+
} while (queued && !stopped && !io.signal?.aborted);
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
running = false;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
const schedule = context.schedule ?? ((callback, delayMs) => setTimeout(() => { void callback(); }, delayMs));
|
|
592
|
+
const cancelScheduled = context.cancelScheduled ?? (handle => clearTimeout(handle));
|
|
593
|
+
const trigger = () => {
|
|
594
|
+
if (stopped || io.signal?.aborted)
|
|
595
|
+
return;
|
|
596
|
+
if (running) {
|
|
597
|
+
queued = true;
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
if (timer !== undefined)
|
|
602
|
+
cancelScheduled(timer);
|
|
603
|
+
timer = schedule(async () => {
|
|
604
|
+
timer = undefined;
|
|
605
|
+
if (stopped)
|
|
606
|
+
return;
|
|
607
|
+
// Retained so shutdown waits for a pass in flight (a URL fetch, say)
|
|
608
|
+
// instead of closing the store under it.
|
|
609
|
+
const pass = fire();
|
|
610
|
+
active = pass;
|
|
611
|
+
try {
|
|
612
|
+
await pass;
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
fail(error);
|
|
616
|
+
}
|
|
617
|
+
if (active === pass)
|
|
618
|
+
active = undefined;
|
|
619
|
+
}, 200);
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
fail(error);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
// Every local declared file and mapping; URL sources only run on file
|
|
626
|
+
// changes. A followed .cave source may declare more, so the set is
|
|
627
|
+
// refreshed after every pass.
|
|
628
|
+
const watch = context.watch ?? ((path, listener) => watchFs(path, listener));
|
|
629
|
+
const watched = new Map();
|
|
630
|
+
const refreshWatchers = () => {
|
|
631
|
+
let declared;
|
|
632
|
+
try {
|
|
633
|
+
// A named watch watches the selection and everything it owns.
|
|
634
|
+
declared = values.name === undefined ?
|
|
635
|
+
selectDeclared(store, values) :
|
|
636
|
+
Declared.closure(store, selectDeclared(store, values).map(source => source.name));
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const targets = new Set(declared.flatMap(source => [
|
|
642
|
+
...Source.isUrl(source.path) ? [] : [Declared.resolvePath(source.path, dir)],
|
|
643
|
+
// A map is a file when it resolves to one, as the loader decides; only
|
|
644
|
+
// otherwise is it inline and nothing to watch.
|
|
645
|
+
...source.map === undefined || (Template.isInline(source.map) && !existsSync(Declared.resolvePath(source.map, dir))) ?
|
|
646
|
+
[] : [Declared.resolvePath(source.map, dir)]
|
|
647
|
+
]));
|
|
648
|
+
for (const target of targets) {
|
|
649
|
+
if (watched.has(target))
|
|
650
|
+
continue;
|
|
651
|
+
let active = true;
|
|
652
|
+
const subscription = watch(dirname(target), (_event, filename) => {
|
|
653
|
+
if (!active)
|
|
654
|
+
return;
|
|
655
|
+
if (filename === null || filename.toString() === basename(target)) {
|
|
656
|
+
trigger();
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
watched.set(target, { close: () => { active = false; subscription.close(); } });
|
|
660
|
+
subscription.on?.('error', error => { if (active && !stopped)
|
|
661
|
+
fail(error); });
|
|
662
|
+
}
|
|
663
|
+
const errors = [];
|
|
664
|
+
for (const [target, subscription] of watched) {
|
|
665
|
+
if (targets.has(target))
|
|
666
|
+
continue;
|
|
667
|
+
// Retire ownership before closing: a failed close must not suppress a
|
|
668
|
+
// fresh subscription if this path is declared again on a later pass.
|
|
669
|
+
watched.delete(target);
|
|
670
|
+
try {
|
|
671
|
+
subscription.close();
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
errors.push(error);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (errors.length === 1)
|
|
678
|
+
throw errors[0];
|
|
679
|
+
if (errors.length > 1) {
|
|
680
|
+
throw new AggregateError(errors, `retiring watch subscriptions failed: ${errors.map(errorMessage).join('; ')}`, { cause: errors[0] });
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
return watchSession(async () => {
|
|
684
|
+
refreshWatchers();
|
|
685
|
+
if (watched.size === 0) {
|
|
686
|
+
io.stderr.write('cave connect: nothing to watch — no declared local source\n');
|
|
687
|
+
return 1;
|
|
688
|
+
}
|
|
689
|
+
await fire();
|
|
690
|
+
if (stopped || io.signal?.aborted) {
|
|
691
|
+
await end.wait();
|
|
692
|
+
return 0;
|
|
693
|
+
}
|
|
694
|
+
io.stdout.write('watching (ctrl-c to stop)\n');
|
|
695
|
+
await end.wait();
|
|
696
|
+
return 0;
|
|
697
|
+
}, () => {
|
|
698
|
+
stopped = true;
|
|
699
|
+
end.close();
|
|
247
700
|
if (timer !== undefined)
|
|
248
701
|
cancelScheduled(timer);
|
|
249
|
-
|
|
250
|
-
|
|
702
|
+
}, () => watched.values(), () => active);
|
|
703
|
+
};
|
|
704
|
+
/**
|
|
705
|
+
* `cave connect` without a source (spec §23.4): the store's declared
|
|
706
|
+
* sources. `--list` prints them, `--dry-run` previews, `--query` overlays
|
|
707
|
+
* them in a rolled-back transaction, `--watch` re-runs on file changes,
|
|
708
|
+
* and the plain form runs one pass over each.
|
|
709
|
+
*/
|
|
710
|
+
const runDeclared = async (values, io, context) => {
|
|
711
|
+
const perSource = ['map', 'key', 'format', 'delimiter', 'table', 'sql', 'records']
|
|
712
|
+
.filter(option => values[option] !== undefined);
|
|
713
|
+
if (perSource.length > 0) {
|
|
714
|
+
io.stderr.write(`cave connect: ${perSource.map(option => `--${option}`).join(', ')} describe a source argument — ` +
|
|
715
|
+
'without one, declare them on the source/<name> entity (spec §23.4)\n');
|
|
716
|
+
return 1;
|
|
717
|
+
}
|
|
718
|
+
const db = values.db ?? defaultDbPath();
|
|
719
|
+
try {
|
|
720
|
+
if (values.list === true) {
|
|
721
|
+
const store = openAt(db, { intent: 'read', assemble: Declared.assemble, ...registryOf(values) });
|
|
722
|
+
return await withCommandStore(store, () => {
|
|
723
|
+
const declared = selectDeclared(store, values);
|
|
724
|
+
io.stdout.write(declared.length === 0 ? 'no declared sources\n' : `${declared.map(Declared.describe).join('\n')}\n`);
|
|
725
|
+
return 0;
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
if (values['dry-run'] === true || values.query !== undefined) {
|
|
729
|
+
// Discovery works on a private snapshot, so the dry run only reads;
|
|
730
|
+
// the overlay appends into the store's own rolled-back transaction.
|
|
731
|
+
const store = openAt(db, { intent: values.query === undefined ? 'read' : 'scratch', assemble: Declared.assemble, ...registryOf(values) });
|
|
732
|
+
return await withCommandStore(store, async () => {
|
|
733
|
+
return values.query === undefined ?
|
|
734
|
+
await declaredDry(store, db, values, io, context) :
|
|
735
|
+
await declaredQuery(store, db, values, io, context);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
const store = openAt(db, { intent: 'write', ...registryOf(values) });
|
|
739
|
+
return await withCommandStore(store, async () => {
|
|
740
|
+
return values.watch === true ?
|
|
741
|
+
await declaredWatch(store, db, values, io, context) :
|
|
742
|
+
await declaredPass(store, db, values, io, context);
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
io.stderr.write(`cave connect: ${errorMessage(error)}\n`);
|
|
747
|
+
return error instanceof LocateError ? 1 : 1;
|
|
251
748
|
}
|
|
252
749
|
};
|
|
253
750
|
export const runConnect = async (argv, context = {}) => {
|
|
751
|
+
const signal = context.signal;
|
|
752
|
+
if (signal?.aborted === true)
|
|
753
|
+
return 0;
|
|
754
|
+
context = {
|
|
755
|
+
signal,
|
|
756
|
+
stdout: context.stdout,
|
|
757
|
+
stderr: context.stderr,
|
|
758
|
+
fetchImpl: context.fetchImpl,
|
|
759
|
+
watch: context.watch,
|
|
760
|
+
schedule: context.schedule,
|
|
761
|
+
cancelScheduled: context.cancelScheduled
|
|
762
|
+
};
|
|
254
763
|
const io = {
|
|
255
764
|
stdout: context.stdout ?? process.stdout,
|
|
256
765
|
stderr: context.stderr ?? process.stderr,
|
|
257
|
-
...
|
|
766
|
+
...signal === undefined ? {} : { signal }
|
|
258
767
|
};
|
|
768
|
+
if (Boolean(io.signal?.aborted))
|
|
769
|
+
return 0;
|
|
259
770
|
const { values, positionals } = parseArgs({
|
|
260
771
|
args: [...argv],
|
|
261
772
|
options: {
|
|
@@ -277,6 +788,7 @@ export const runConnect = async (argv, context = {}) => {
|
|
|
277
788
|
all: { type: 'boolean' },
|
|
278
789
|
aliases: { type: 'boolean' },
|
|
279
790
|
'no-prelude': { type: 'boolean' },
|
|
791
|
+
list: { type: 'boolean' },
|
|
280
792
|
help: { type: 'boolean', short: 'h' }
|
|
281
793
|
},
|
|
282
794
|
allowPositionals: true
|
|
@@ -285,11 +797,18 @@ export const runConnect = async (argv, context = {}) => {
|
|
|
285
797
|
io.stdout.write(`${usage}\n`);
|
|
286
798
|
return 0;
|
|
287
799
|
}
|
|
800
|
+
if (positionals.length === 0) {
|
|
801
|
+
return runDeclared(values, io, context);
|
|
802
|
+
}
|
|
288
803
|
const [source] = positionals;
|
|
289
804
|
if (source === undefined || positionals.length !== 1) {
|
|
290
805
|
io.stderr.write(`cave connect: exactly one source is required\n\n${usage}\n`);
|
|
291
806
|
return 1;
|
|
292
807
|
}
|
|
808
|
+
if (values.list === true) {
|
|
809
|
+
io.stderr.write('cave connect: --list takes no source\n');
|
|
810
|
+
return 1;
|
|
811
|
+
}
|
|
293
812
|
if (values.map === undefined) {
|
|
294
813
|
io.stderr.write(`cave connect: --map <file> is required\n\n${usage}\n`);
|
|
295
814
|
return 1;
|
|
@@ -318,18 +837,15 @@ export const runConnect = async (argv, context = {}) => {
|
|
|
318
837
|
intent: 'write',
|
|
319
838
|
...values['no-prelude'] === true ? { registry: Registry.empty } : {}
|
|
320
839
|
});
|
|
321
|
-
|
|
840
|
+
return await withCommandStore(store, async () => {
|
|
322
841
|
if (values.watch === true) {
|
|
323
842
|
return await runWatch(store, source, values, name, io, context);
|
|
324
843
|
}
|
|
325
844
|
return await runPass(store, source, values, name, io, context);
|
|
326
|
-
}
|
|
327
|
-
finally {
|
|
328
|
-
store.close();
|
|
329
|
-
}
|
|
845
|
+
});
|
|
330
846
|
}
|
|
331
847
|
catch (error) {
|
|
332
|
-
io.stderr.write(`cave connect: ${
|
|
848
|
+
io.stderr.write(`cave connect: ${errorMessage(error)}\n`);
|
|
333
849
|
return 1;
|
|
334
850
|
}
|
|
335
851
|
};
|