run-dmcp 0.1.0 → 0.3.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 +101 -11
- package/dist/bin/run-dmcp.d.ts +2 -0
- package/dist/bin/run-dmcp.js +55 -0
- package/dist/db/connection.d.ts +32 -0
- package/dist/db/connection.js +38 -16
- package/dist/db/schema.d.ts +29 -1
- package/dist/db/schema.js +594 -10
- package/dist/http/server.js +25 -4
- package/dist/index.d.ts +69 -2
- package/dist/index.js +262 -92
- package/dist/mcp-server.d.ts +49 -0
- package/dist/mcp-server.js +127 -0
- package/dist/reader/turnReader.d.ts +185 -0
- package/dist/reader/turnReader.js +288 -0
- package/dist/register/batch.js +5 -79
- package/dist/register/mcp-resources.d.ts +9 -0
- package/dist/register/mcp-resources.js +16 -62
- package/dist/register/render.d.ts +17 -0
- package/dist/register/render.js +50 -0
- package/dist/register/resolve.d.ts +14 -0
- package/dist/register/resolve.js +102 -0
- package/dist/register/resources.js +13 -6
- package/dist/register/timeline.d.ts +2 -0
- package/dist/register/timeline.js +311 -0
- package/dist/rpg/index.d.ts +29 -0
- package/dist/rpg/index.js +55 -0
- package/dist/rpg/register/abilities.d.ts +2 -0
- package/dist/rpg/register/abilities.js +165 -0
- package/dist/rpg/register/batch.d.ts +2 -0
- package/dist/rpg/register/batch.js +92 -0
- package/dist/rpg/register/combat.d.ts +2 -0
- package/dist/rpg/register/combat.js +207 -0
- package/dist/rpg/register/mcp-prompts.d.ts +2 -0
- package/dist/rpg/register/mcp-prompts.js +684 -0
- package/dist/rpg/register/mcp-resources.d.ts +2 -0
- package/dist/rpg/register/mcp-resources.js +61 -0
- package/dist/rpg/register/quests.d.ts +2 -0
- package/dist/rpg/register/quests.js +118 -0
- package/dist/rpg/register/status.d.ts +2 -0
- package/dist/rpg/register/status.js +130 -0
- package/dist/rpg/register/tables.d.ts +2 -0
- package/dist/rpg/register/tables.js +146 -0
- package/dist/rpg/tools/ability.d.ts +48 -0
- package/dist/rpg/tools/ability.js +238 -0
- package/dist/rpg/tools/combat.d.ts +13 -0
- package/dist/rpg/tools/combat.js +195 -0
- package/dist/rpg/tools/dice.d.ts +23 -0
- package/dist/rpg/tools/dice.js +111 -0
- package/dist/rpg/tools/quest.d.ts +34 -0
- package/dist/rpg/tools/quest.js +164 -0
- package/dist/rpg/tools/status.d.ts +36 -0
- package/dist/rpg/tools/status.js +218 -0
- package/dist/rpg/tools/tables.d.ts +33 -0
- package/dist/rpg/tools/tables.js +209 -0
- package/dist/schemas/index.d.ts +12 -12
- package/dist/timeline/adjudication.d.ts +150 -0
- package/dist/timeline/adjudication.js +174 -0
- package/dist/timeline/changes.d.ts +108 -0
- package/dist/timeline/changes.js +169 -0
- package/dist/timeline/checkpoint.d.ts +69 -0
- package/dist/timeline/checkpoint.js +131 -0
- package/dist/timeline/clock.d.ts +89 -0
- package/dist/timeline/clock.js +173 -0
- package/dist/timeline/constrained.d.ts +220 -0
- package/dist/timeline/constrained.js +671 -0
- package/dist/timeline/export.d.ts +181 -0
- package/dist/timeline/export.js +339 -0
- package/dist/timeline/irreversible.d.ts +87 -0
- package/dist/timeline/irreversible.js +108 -0
- package/dist/timeline/kinds.d.ts +14 -0
- package/dist/timeline/kinds.js +22 -0
- package/dist/timeline/narration.d.ts +175 -0
- package/dist/timeline/narration.js +259 -0
- package/dist/timeline/projection.d.ts +97 -0
- package/dist/timeline/projection.js +330 -0
- package/dist/timeline/provenance.d.ts +66 -0
- package/dist/timeline/provenance.js +45 -0
- package/dist/timeline/registry.d.ts +95 -0
- package/dist/timeline/registry.js +124 -0
- package/dist/timeline/render.d.ts +121 -0
- package/dist/timeline/render.js +187 -0
- package/dist/timeline/replay.d.ts +86 -0
- package/dist/timeline/replay.js +126 -0
- package/dist/timeline/resolve.d.ts +262 -0
- package/dist/timeline/resolve.js +226 -0
- package/dist/timeline/schema.d.ts +13 -0
- package/dist/timeline/schema.js +264 -0
- package/dist/timeline/t.d.ts +80 -0
- package/dist/timeline/t.js +37 -0
- package/dist/tools/audio.js +13 -9
- package/dist/tools/constraint.d.ts +44 -80
- package/dist/tools/constraint.js +115 -124
- package/dist/tools/game.js +33 -1
- package/dist/tools/images.js +17 -10
- package/dist/tools/relationship.d.ts +83 -2
- package/dist/tools/relationship.js +139 -62
- package/dist/tools/resource.d.ts +33 -8
- package/dist/tools/resource.js +106 -153
- package/dist/tools/time.js +18 -3
- package/dist/types/index.d.ts +20 -2
- package/dist/utils/media-path.d.ts +52 -0
- package/dist/utils/media-path.js +106 -0
- package/dist/utils/output-schemas.d.ts +594 -3
- package/dist/utils/output-schemas.js +4 -1
- package/dist/utils/webui.d.ts +32 -0
- package/dist/utils/webui.js +54 -1
- package/package.json +25 -5
- package/dist/__tests__/engineVocabulary.test.d.ts +0 -1
- package/dist/__tests__/engineVocabulary.test.js +0 -147
- package/dist/db/__tests__/connection.test.d.ts +0 -1
- package/dist/db/__tests__/connection.test.js +0 -72
- package/dist/db/__tests__/testDb.d.ts +0 -33
- package/dist/db/__tests__/testDb.js +0 -41
- package/dist/test-setup.d.ts +0 -1
- package/dist/test-setup.js +0 -13
- package/dist/tools/__tests__/audio.test.d.ts +0 -1
- package/dist/tools/__tests__/audio.test.js +0 -59
- package/dist/tools/__tests__/conserved.test.d.ts +0 -1
- package/dist/tools/__tests__/conserved.test.js +0 -488
- package/dist/tools/__tests__/constraint.test.d.ts +0 -1
- package/dist/tools/__tests__/constraint.test.js +0 -212
- package/dist/tools/__tests__/expiry-consequences.test.d.ts +0 -1
- package/dist/tools/__tests__/expiry-consequences.test.js +0 -110
- package/dist/tools/__tests__/images.test.d.ts +0 -1
- package/dist/tools/__tests__/images.test.js +0 -59
- package/dist/tools/__tests__/relationship.test.d.ts +0 -1
- package/dist/tools/__tests__/relationship.test.js +0 -132
- package/dist/tools/__tests__/resource-constraints.test.d.ts +0 -1
- package/dist/tools/__tests__/resource-constraints.test.js +0 -131
- package/dist/tools/__tests__/resource.test.d.ts +0 -1
- package/dist/tools/__tests__/resource.test.js +0 -190
- package/dist/tools/__tests__/time.test.d.ts +0 -1
- package/dist/tools/__tests__/time.test.js +0 -404
- package/dist/tools/__tests__/timers.test.d.ts +0 -1
- package/dist/tools/__tests__/timers.test.js +0 -426
- package/dist/tools/__tests__/world.test.d.ts +0 -1
- package/dist/tools/__tests__/world.test.js +0 -70
- package/dist/utils/__tests__/json.test.d.ts +0 -1
- package/dist/utils/__tests__/json.test.js +0 -55
- package/dist/utils/__tests__/validation.test.d.ts +0 -1
- package/dist/utils/__tests__/validation.test.js +0 -90
package/README.md
CHANGED
|
@@ -12,21 +12,110 @@ holds full adjudication discretion over a world that only ever stores *now*.
|
|
|
12
12
|
|
|
13
13
|
## Status
|
|
14
14
|
|
|
15
|
-
**0.
|
|
16
|
-
|
|
15
|
+
**0.3.0.** The base is the predecessor's engine plus four pieces of generic mechanism that were built
|
|
16
|
+
for it and offered back to it:
|
|
17
17
|
|
|
18
18
|
- **Atomicity** — `withTransaction()` wired into the multi-write operations that were running
|
|
19
19
|
non-atomically.
|
|
20
|
-
- **Declarative constraints** — resources can be declared `bounded
|
|
21
|
-
|
|
20
|
+
- **Declarative constraints** — resources can be declared `bounded` or `monotonic`, and the store
|
|
21
|
+
enforces it rather than trusting every caller. `resolve_only` — every direct write rejected, so a
|
|
22
|
+
value can move only through an adjudicating call — joined them alongside the resolve protocol, and
|
|
23
|
+
is enforced by a trigger rather than by a checker anyone has to remember to call; see
|
|
24
|
+
[docs/DESIGN.md](docs/DESIGN.md) §5.2a.
|
|
22
25
|
- **Conserved resource sets** — a set of resources can be declared conserved, with an atomic transfer
|
|
23
26
|
that never silently clamps.
|
|
24
27
|
- **On-expiry consequences** — scheduled events and timers can carry a consequence that actually
|
|
25
28
|
lands when they expire, rather than expiring into nothing.
|
|
26
29
|
|
|
30
|
+
0.1.1 added the packaging half of that: importing the library starts nothing, the database resolves
|
|
31
|
+
to the consuming application rather than into `node_modules`, and a consumer can bring up its own
|
|
32
|
+
tables through the migration hook below. 0.3.0 adds the consuming half — the entity/property spine is
|
|
33
|
+
exported as library functions, so a consumer that deletes its own vendored copy has something to
|
|
34
|
+
import rather than a server to make tool calls into.
|
|
35
|
+
|
|
27
36
|
The timeline that gives this project its reason to exist — interval-versioned facts, `replay(t)`,
|
|
28
|
-
irreversibility, `changes_within` — is
|
|
29
|
-
|
|
37
|
+
irreversibility, timeline export, `changes_within` — is built, and every write of world state appends
|
|
38
|
+
to it in the same transaction, through generated triggers rather than edited write sites.
|
|
39
|
+
|
|
40
|
+
**There is one versioning substrate, and it is the timeline.** A resource's value is a constrained
|
|
41
|
+
numeric fact: declared constraints (`bounded`, `monotonic`, conserved sets, `irreversible`) are
|
|
42
|
+
checked at a single choke point, and "what did this value used to be" is answered by the fact
|
|
43
|
+
intervals rather than by a history table beside them. The former `resource_history` and
|
|
44
|
+
`relationship_history` tables no longer accept writes. See [docs/DESIGN.md](docs/DESIGN.md), which is
|
|
45
|
+
the authority, §5.4 for that decision and §11 for the order the rest lands in.
|
|
46
|
+
|
|
47
|
+
## Running it, and depending on it
|
|
48
|
+
|
|
49
|
+
These are two different things, and the package keeps them apart.
|
|
50
|
+
|
|
51
|
+
**As an application** — `run-dmcp` (or `node dist/bin/run-dmcp.js`) serves MCP over stdio and the web
|
|
52
|
+
UI alongside it. `DMCP_HTTP_PORT` moves the web UI; `DMCP_NO_HTTP=1` turns it off entirely, which is
|
|
53
|
+
what a host that spawns this as a subprocess wants: a referee has no use for an admin page, and a
|
|
54
|
+
server it cannot close squats a port.
|
|
55
|
+
|
|
56
|
+
**As a dependency** — importing the package starts nothing. No listener, no database file, no work at
|
|
57
|
+
all: the entry point is exports, and the application lives behind `bin`. A consumer decides when the
|
|
58
|
+
schema comes up, where the database lives, and whether anything listens.
|
|
59
|
+
|
|
60
|
+
**The package root is the core, and the tabletop surface is a layer above it.** Dice, combat,
|
|
61
|
+
abilities, status effects, random tables and quests are genuinely game-shaped — an optional
|
|
62
|
+
dependency, not part of the engine (see [docs/DESIGN.md](docs/DESIGN.md) §8). A consumer that only
|
|
63
|
+
needs entities, facts, events and the timeline imports `run-dmcp` and calls `createCoreMcpServer`. A
|
|
64
|
+
consumer that wants the full tabletop surface imports `run-dmcp/rpg` and calls `createMcpServer` —
|
|
65
|
+
same name, same options, the whole assembly this package has always served:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { initializeSchema, type SchemaMigration } from "run-dmcp";
|
|
69
|
+
import { createMcpServer } from "run-dmcp/rpg";
|
|
70
|
+
|
|
71
|
+
const migrations: SchemaMigration[] = [
|
|
72
|
+
{
|
|
73
|
+
name: "my-tables",
|
|
74
|
+
up(db) {
|
|
75
|
+
db.exec(`CREATE TABLE IF NOT EXISTS my_table (id TEXT PRIMARY KEY)`);
|
|
76
|
+
try {
|
|
77
|
+
db.exec(`ALTER TABLE my_table ADD COLUMN added_later TEXT`);
|
|
78
|
+
} catch {
|
|
79
|
+
// Already added -- migrations run on every startup, so they must be idempotent.
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
initializeSchema({ migrations }); // engine tables first, then yours, one pass, one database
|
|
86
|
+
const server = createMcpServer(); // built, not started -- connect it to a transport yourself
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The hook is a parameter rather than a global `register()` because a parameter cannot be registered
|
|
90
|
+
too late to run. There is no framework behind it: no version table, no record of what already ran, no
|
|
91
|
+
down-migrations. Every migration runs on every startup, exactly like the engine's own DDL — which is
|
|
92
|
+
what forces them to be idempotent, and it is tested against an existing database, not just a fresh
|
|
93
|
+
one.
|
|
94
|
+
|
|
95
|
+
The database lands in the consuming application: `DMCP_DB_PATH` if set, else an existing
|
|
96
|
+
`~/.local/share/dmcp`, else `./data/games.db` relative to the working directory. Never inside the
|
|
97
|
+
installed package.
|
|
98
|
+
|
|
99
|
+
**The spine is importable, not only callable over a transport.** Games, locations, characters,
|
|
100
|
+
factions, relationships, resources, secrets, items, notes, tags, time and timers are library
|
|
101
|
+
functions first and MCP tools second, so a consumer reads and writes its own world directly and only
|
|
102
|
+
serves the tools it actually wants an LLM to reach. That includes the constrained-write choke point:
|
|
103
|
+
a number that moves through `updateResourceValue` is checked against its declared constraints and its
|
|
104
|
+
history is `facts`, queryable with `valueHistory`.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { createGame, createResource, updateResourceValue, valueHistory } from "run-dmcp";
|
|
108
|
+
|
|
109
|
+
const game = createGame({ name: "The Granary", setting: "…", style: "…" });
|
|
110
|
+
const grain = createResource({ gameId: game.id, ownerType: "game", name: "grain", value: 100 });
|
|
111
|
+
|
|
112
|
+
updateResourceValue({ resourceId: grain.id, mode: "delta", value: -30, reason: "the winter ration" });
|
|
113
|
+
valueHistory(grain.id, "value"); // → the transitions, with the reason each one carried
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The annotations, input limits, error envelope and logger the engine's own tools are built from are
|
|
117
|
+
exported too (`ANNOTATIONS`, `LIMITS`, `errors`, `createLogger`), so a consumer's own tools can
|
|
118
|
+
refuse and bound the way these do instead of re-implementing it.
|
|
30
119
|
|
|
31
120
|
## Provenance
|
|
32
121
|
|
|
@@ -35,9 +124,10 @@ commit was 2026-01-06. This repository carries its full history rather than a sq
|
|
|
35
124
|
first commit here is his, from 2025-12-30.
|
|
36
125
|
|
|
37
126
|
It is **not** a GitHub fork, deliberately — a repository inside another's fork network is discoverable
|
|
38
|
-
only as "a fork of" and cannot own its issue tracker. The four offers listed above
|
|
39
|
-
requests against the original
|
|
40
|
-
|
|
127
|
+
only as "a fork of" and cannot own its issue tracker. The four offers listed above were opened as pull
|
|
128
|
+
requests against the original ([#6](https://github.com/shawnrushefsky/dmcp/pull/6)–[#9](https://github.com/shawnrushefsky/dmcp/pull/9))
|
|
129
|
+
and withdrawn on 2026-08-18 after seven months without a maintainer response; they are merged here
|
|
130
|
+
instead. Their diffs remain readable upstream at `refs/pull/6..9/head`.
|
|
41
131
|
|
|
42
132
|
MIT, and his copyright notice travels with the code. See [LICENSE](LICENSE).
|
|
43
133
|
|
|
@@ -67,12 +157,12 @@ npm run typecheck # tsc --noEmit
|
|
|
67
157
|
npm run test:run # vitest, one shot
|
|
68
158
|
npm run build # tsc + client build
|
|
69
159
|
|
|
70
|
-
npm run dev # tsx src/
|
|
160
|
+
npm run dev # tsx src/bin/run-dmcp.ts
|
|
71
161
|
```
|
|
72
162
|
|
|
73
163
|
CI runs lint, typecheck, tests and build on every push, for both the server and the client.
|
|
74
164
|
|
|
75
|
-
Local MCP inspection: `npx @modelcontextprotocol/inspector node dist/
|
|
165
|
+
Local MCP inspection: `npx @modelcontextprotocol/inspector node dist/bin/run-dmcp.js`
|
|
76
166
|
|
|
77
167
|
## License
|
|
78
168
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The application.
|
|
3
|
+
//
|
|
4
|
+
// Everything this file does to the machine -- create the database, bind a
|
|
5
|
+
// port, hold stdio open until it is killed -- is exactly what a library must
|
|
6
|
+
// not do on import, which is why it lives here and not in src/index.ts. The
|
|
7
|
+
// package's `bin` points at this file; the package's `main` points at the
|
|
8
|
+
// library, which starts nothing.
|
|
9
|
+
//
|
|
10
|
+
// The web UI runs by default, as it always has. DMCP_NO_HTTP turns it off, for
|
|
11
|
+
// a host that spawns this as an MCP subprocess and has no use for an admin
|
|
12
|
+
// page it cannot close.
|
|
13
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
|
+
import { closeDatabase } from "../db/connection.js";
|
|
15
|
+
import { initializeSchema } from "../db/schema.js";
|
|
16
|
+
import { startHttpServer } from "../http/server.js";
|
|
17
|
+
// The full assembly -- core plus the RPG layer -- now lives one layer up
|
|
18
|
+
// (design §8, issue #17). The application always served the full surface,
|
|
19
|
+
// so it reaches for it here rather than the core-only `createCoreMcpServer`
|
|
20
|
+
// in ../mcp-server.js.
|
|
21
|
+
import { createMcpServer } from "../rpg/index.js";
|
|
22
|
+
import { httpPortFromEnv, setHttpPort, webUiEnabled } from "../utils/webui.js";
|
|
23
|
+
import { createLogger } from "../utils/logger.js";
|
|
24
|
+
const log = createLogger("bin");
|
|
25
|
+
async function main() {
|
|
26
|
+
// An application owns its database, so this is where the schema is brought
|
|
27
|
+
// up -- not at import time, and not in any module a consumer might load.
|
|
28
|
+
initializeSchema();
|
|
29
|
+
const server = createMcpServer();
|
|
30
|
+
if (webUiEnabled(process.env)) {
|
|
31
|
+
const actualPort = await startHttpServer(httpPortFromEnv(process.env));
|
|
32
|
+
setHttpPort(actualPort);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
log.info("Web UI disabled by DMCP_NO_HTTP; no port will be bound");
|
|
36
|
+
}
|
|
37
|
+
// Start MCP server with stdio transport
|
|
38
|
+
const transport = new StdioServerTransport();
|
|
39
|
+
await server.connect(transport);
|
|
40
|
+
}
|
|
41
|
+
// Handle cleanup
|
|
42
|
+
process.on("SIGINT", () => {
|
|
43
|
+
closeDatabase();
|
|
44
|
+
process.exit(0);
|
|
45
|
+
});
|
|
46
|
+
process.on("SIGTERM", () => {
|
|
47
|
+
closeDatabase();
|
|
48
|
+
process.exit(0);
|
|
49
|
+
});
|
|
50
|
+
main().catch((error) => {
|
|
51
|
+
log.error("Server error", { error: error instanceof Error ? error.message : String(error) });
|
|
52
|
+
console.error(error);
|
|
53
|
+
closeDatabase();
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
package/dist/db/connection.d.ts
CHANGED
|
@@ -1,4 +1,36 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
|
+
export interface DataPathInputs {
|
|
3
|
+
env: NodeJS.ProcessEnv;
|
|
4
|
+
cwd: string;
|
|
5
|
+
home: string;
|
|
6
|
+
exists: (path: string) => boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolve database path with the following priority:
|
|
10
|
+
* 1. `env.DMCP_DB_PATH` -- the explicit path, wins over everything. Passed
|
|
11
|
+
* through verbatim (including the literal ":memory:"), with
|
|
12
|
+
* `dataDir = dirname(dbPath)`.
|
|
13
|
+
* 2. The XDG data directory -- `env.XDG_DATA_HOME || join(home, ".local",
|
|
14
|
+
* "share")`, plus "/dmcp/games.db" -- but only when that directory
|
|
15
|
+
* already exists. An existing `~/.local/share/dmcp` means a previous
|
|
16
|
+
* install's data already lives there, and moving the default out from
|
|
17
|
+
* under it would orphan someone's saved state.
|
|
18
|
+
* 3. Otherwise `join(cwd, "data", "games.db")` -- the CONSUMING
|
|
19
|
+
* application's working directory. This is the fix for the "installed as
|
|
20
|
+
* a dependency" bug: the default lands where the application runs, never
|
|
21
|
+
* where the package is installed. `cwd` and `home` are supplied by the
|
|
22
|
+
* caller (see `resolveDataPath` below) and the package's own install
|
|
23
|
+
* location is never an input to this function, so it cannot resolve
|
|
24
|
+
* there -- structurally, not by a runtime check.
|
|
25
|
+
*
|
|
26
|
+
* Pure and deterministically testable: no ambient reads (`process.env`,
|
|
27
|
+
* `process.cwd()`, `os.homedir()`) and no filesystem access except through
|
|
28
|
+
* the injected `exists`. See `src/db/__tests__/dataPath.test.ts`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveDataPathFrom(inputs: DataPathInputs): {
|
|
31
|
+
dataDir: string;
|
|
32
|
+
dbPath: string;
|
|
33
|
+
};
|
|
2
34
|
export declare function getDatabase(): Database.Database;
|
|
3
35
|
export declare function closeDatabase(): void;
|
|
4
36
|
/**
|
package/dist/db/connection.js
CHANGED
|
@@ -1,41 +1,63 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
|
-
import { fileURLToPath } from "url";
|
|
3
2
|
import { dirname, join } from "path";
|
|
4
3
|
import { mkdirSync, existsSync } from "fs";
|
|
5
4
|
import { homedir } from "os";
|
|
6
5
|
import { createLogger } from "../utils/logger.js";
|
|
7
6
|
const log = createLogger("db");
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = dirname(__filename);
|
|
10
7
|
/**
|
|
11
8
|
* Resolve database path with the following priority:
|
|
12
|
-
* 1. DMCP_DB_PATH
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* 1. `env.DMCP_DB_PATH` -- the explicit path, wins over everything. Passed
|
|
10
|
+
* through verbatim (including the literal ":memory:"), with
|
|
11
|
+
* `dataDir = dirname(dbPath)`.
|
|
12
|
+
* 2. The XDG data directory -- `env.XDG_DATA_HOME || join(home, ".local",
|
|
13
|
+
* "share")`, plus "/dmcp/games.db" -- but only when that directory
|
|
14
|
+
* already exists. An existing `~/.local/share/dmcp` means a previous
|
|
15
|
+
* install's data already lives there, and moving the default out from
|
|
16
|
+
* under it would orphan someone's saved state.
|
|
17
|
+
* 3. Otherwise `join(cwd, "data", "games.db")` -- the CONSUMING
|
|
18
|
+
* application's working directory. This is the fix for the "installed as
|
|
19
|
+
* a dependency" bug: the default lands where the application runs, never
|
|
20
|
+
* where the package is installed. `cwd` and `home` are supplied by the
|
|
21
|
+
* caller (see `resolveDataPath` below) and the package's own install
|
|
22
|
+
* location is never an input to this function, so it cannot resolve
|
|
23
|
+
* there -- structurally, not by a runtime check.
|
|
24
|
+
*
|
|
25
|
+
* Pure and deterministically testable: no ambient reads (`process.env`,
|
|
26
|
+
* `process.cwd()`, `os.homedir()`) and no filesystem access except through
|
|
27
|
+
* the injected `exists`. See `src/db/__tests__/dataPath.test.ts`.
|
|
15
28
|
*/
|
|
16
|
-
function
|
|
29
|
+
export function resolveDataPathFrom(inputs) {
|
|
30
|
+
const { env, cwd, home, exists } = inputs;
|
|
17
31
|
// Priority 1: Explicit environment variable
|
|
18
|
-
if (
|
|
19
|
-
const dbPath =
|
|
32
|
+
if (env.DMCP_DB_PATH) {
|
|
33
|
+
const dbPath = env.DMCP_DB_PATH;
|
|
20
34
|
const dataDir = dirname(dbPath);
|
|
21
35
|
log.info("Using database path from DMCP_DB_PATH", { dbPath });
|
|
22
36
|
return { dataDir, dbPath };
|
|
23
37
|
}
|
|
24
|
-
// Priority 2: XDG Base Directory spec
|
|
25
|
-
const xdgDataHome =
|
|
38
|
+
// Priority 2: XDG Base Directory spec, only if it already exists
|
|
39
|
+
const xdgDataHome = env.XDG_DATA_HOME || join(home, ".local", "share");
|
|
26
40
|
const xdgDataDir = join(xdgDataHome, "dmcp");
|
|
27
41
|
const xdgDbPath = join(xdgDataDir, "games.db");
|
|
28
|
-
|
|
29
|
-
if (existsSync(xdgDataDir) || !existsSync(join(__dirname, "..", "..", "package.json"))) {
|
|
42
|
+
if (exists(xdgDataDir)) {
|
|
30
43
|
log.info("Using XDG data directory", { dbPath: xdgDbPath });
|
|
31
44
|
return { dataDir: xdgDataDir, dbPath: xdgDbPath };
|
|
32
45
|
}
|
|
33
|
-
// Priority 3: Fallback to
|
|
34
|
-
const fallbackDataDir = join(
|
|
46
|
+
// Priority 3: Fallback to the consuming application's working directory
|
|
47
|
+
const fallbackDataDir = join(cwd, "data");
|
|
35
48
|
const fallbackDbPath = join(fallbackDataDir, "games.db");
|
|
36
|
-
log.debug("Using
|
|
49
|
+
log.debug("Using cwd-relative data directory", { dbPath: fallbackDbPath });
|
|
37
50
|
return { dataDir: fallbackDataDir, dbPath: fallbackDbPath };
|
|
38
51
|
}
|
|
52
|
+
/** Thin real-world wrapper around `resolveDataPathFrom`. */
|
|
53
|
+
function resolveDataPath() {
|
|
54
|
+
return resolveDataPathFrom({
|
|
55
|
+
env: process.env,
|
|
56
|
+
cwd: process.cwd(),
|
|
57
|
+
home: homedir(),
|
|
58
|
+
exists: existsSync,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
39
61
|
// Resolved lazily (on first getDatabase() call after startup, or after every
|
|
40
62
|
// closeDatabase()) rather than once at module-import time. This makes
|
|
41
63
|
// DMCP_DB_PATH re-readable at any point before the first real connection is
|
package/dist/db/schema.d.ts
CHANGED
|
@@ -1 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
/**
|
|
3
|
+
* A schema change a consuming application wants applied to the same SQLite
|
|
4
|
+
* file the engine's own tables live in, during the engine's own startup
|
|
5
|
+
* pass. Passed as an array to `initializeSchema()`, not registered through
|
|
6
|
+
* a global function: a registry would make the engine's schema depend on
|
|
7
|
+
* module import order and on side effects at import time, which is exactly
|
|
8
|
+
* what this release is removing elsewhere. A parameter can't be "registered
|
|
9
|
+
* too late" -- it's either in the array passed to the one call that matters,
|
|
10
|
+
* or it isn't.
|
|
11
|
+
*
|
|
12
|
+
* There is no framework: no version table, no record of what already ran,
|
|
13
|
+
* no down-migrations. Every migration in the array runs on every startup,
|
|
14
|
+
* exactly like the engine's own DDL below, which is what forces `up` to be
|
|
15
|
+
* idempotent (`CREATE TABLE IF NOT EXISTS`, and
|
|
16
|
+
* `try { db.exec("ALTER TABLE ...") } catch {}` for added columns).
|
|
17
|
+
*
|
|
18
|
+
* Migrations run after every core table has been created, so `up` may
|
|
19
|
+
* declare a foreign key into one (e.g. `games`).
|
|
20
|
+
*/
|
|
21
|
+
export interface SchemaMigration {
|
|
22
|
+
/** Stable identifier. Used for duplicate detection and error messages. */
|
|
23
|
+
name: string;
|
|
24
|
+
/** Applies the migration. MUST be idempotent: it runs on every startup. */
|
|
25
|
+
up(db: Database.Database): void;
|
|
26
|
+
}
|
|
27
|
+
export declare function initializeSchema(options?: {
|
|
28
|
+
migrations?: readonly SchemaMigration[];
|
|
29
|
+
}): void;
|