@druumen/sessions-db 0.1.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/CHANGELOG.md +249 -0
- package/LICENSE +201 -0
- package/NOTICE +10 -0
- package/README.md +250 -0
- package/cli/_write-helpers.mjs +99 -0
- package/cli/alias.mjs +115 -0
- package/cli/argparse.mjs +296 -0
- package/cli/close.mjs +116 -0
- package/cli/find.mjs +185 -0
- package/cli/format.mjs +277 -0
- package/cli/link-parent.mjs +133 -0
- package/cli/link.mjs +132 -0
- package/cli/rebuild.mjs +98 -0
- package/cli/sessions-db-session-start-main.mjs +454 -0
- package/cli/sessions-db-session-start.mjs +56 -0
- package/cli/sessions-db.mjs +119 -0
- package/cli/sweep.mjs +171 -0
- package/cli/tree.mjs +127 -0
- package/lib/git-context.mjs +479 -0
- package/lib/identity.mjs +616 -0
- package/lib/index.mjs +145 -0
- package/lib/init.mjs +185 -0
- package/lib/lock.mjs +86 -0
- package/lib/operations.mjs +490 -0
- package/lib/paths.mjs +199 -0
- package/lib/projection.mjs +496 -0
- package/lib/sanitize.mjs +131 -0
- package/lib/storage.mjs +759 -0
- package/lib/sweep.mjs +209 -0
- package/lib/transcript.mjs +230 -0
- package/lib/types.mjs +276 -0
- package/lib/uuid.mjs +116 -0
- package/lib/watch.mjs +217 -0
- package/package.json +53 -0
- package/types/git-context.d.mts +98 -0
- package/types/identity.d.mts +658 -0
- package/types/index.d.mts +10 -0
- package/types/index.d.ts +127 -0
- package/types/init.d.mts +53 -0
- package/types/lock.d.mts +18 -0
- package/types/operations.d.mts +204 -0
- package/types/paths.d.mts +54 -0
- package/types/projection.d.mts +79 -0
- package/types/sanitize.d.mts +39 -0
- package/types/storage.d.mts +276 -0
- package/types/sweep.d.mts +58 -0
- package/types/transcript.d.mts +59 -0
- package/types/types.d.mts +255 -0
- package/types/uuid.d.mts +17 -0
- package/types/watch.d.mts +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# @druumen/sessions-db
|
|
2
|
+
|
|
3
|
+
Cross-session traceability for [Claude Code](https://claude.com/claude-code).
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Records every Claude Code session start (cwd, branch, transcript file,
|
|
8
|
+
sanitized first prompt) into a local JSONL event log + projection cache.
|
|
9
|
+
Provides 3-priority identity reconciliation across forks, resumes, and
|
|
10
|
+
hub-spoke (sub-agent) relationships, so you can find related sessions
|
|
11
|
+
across days and worktrees without losing the thread.
|
|
12
|
+
|
|
13
|
+
**Local-only**: no network egress. All data stays on your machine.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @druumen/sessions-db
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Node.js 18 or newer. Zero runtime dependencies.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
### Library API
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import {
|
|
29
|
+
initProjection,
|
|
30
|
+
loadProjection,
|
|
31
|
+
watchProjection,
|
|
32
|
+
setAlias,
|
|
33
|
+
setParent,
|
|
34
|
+
closeSession,
|
|
35
|
+
runSweep,
|
|
36
|
+
} from '@druumen/sessions-db';
|
|
37
|
+
|
|
38
|
+
// 1. Bootstrap storage at .dru-code/ in current cwd. Idempotent — safe
|
|
39
|
+
// to call on every app start.
|
|
40
|
+
const init = await initProjection({ rootPath: process.cwd() + '/.dru-code' });
|
|
41
|
+
if (!init.ok) throw new Error(init.error);
|
|
42
|
+
|
|
43
|
+
// 2. Load the current projection (sessions + meta).
|
|
44
|
+
const projection = await loadProjection({ rootPath: init.paths.eventsJsonl.replace(/\/[^/]+$/, '') });
|
|
45
|
+
console.log(Object.keys(projection.sessions).length, 'sessions');
|
|
46
|
+
|
|
47
|
+
// 3. Watch for changes (debounced 80ms).
|
|
48
|
+
const watcher = watchProjection(rootPath, (event) => {
|
|
49
|
+
console.log('changed:', event.type);
|
|
50
|
+
});
|
|
51
|
+
// Later: watcher.dispose();
|
|
52
|
+
|
|
53
|
+
// 4. Mutate via the operations API. Each call returns
|
|
54
|
+
// { ok: true, event_id } or { ok: false, error }.
|
|
55
|
+
await setAlias({ stableId: 'sess_xxx', alias: 'my session', rootPath });
|
|
56
|
+
await setParent({ childId: 'sess_xxx', parentId: 'sess_yyy', rootPath });
|
|
57
|
+
await closeSession({
|
|
58
|
+
stableId: 'sess_xxx',
|
|
59
|
+
outcome: 'done',
|
|
60
|
+
reason: 'shipped',
|
|
61
|
+
rootPath,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 5. Sweep activity_state transitions (active → idle → archived).
|
|
65
|
+
const sweep = await runSweep({ rootPath, dryRun: true });
|
|
66
|
+
console.log(sweep.transitions.length, 'pending transitions');
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
All operations are lock-safe (single-writer through an exclusive-create
|
|
70
|
+
lockfile) and idempotent at the projection level. Errors return as
|
|
71
|
+
`{ ok: false, error }` rather than throwing — system-class failures (disk
|
|
72
|
+
full, permission denied) and business-class failures (cycle, missing
|
|
73
|
+
session) share the same shape.
|
|
74
|
+
|
|
75
|
+
### CLI
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install -g @druumen/sessions-db
|
|
79
|
+
sessions-db --help
|
|
80
|
+
|
|
81
|
+
sessions-db find --limit 10 # list recent sessions
|
|
82
|
+
sessions-db tree sess_019e0f2d-c6e3... # ancestry / descendants
|
|
83
|
+
sessions-db alias sess_019e0f2d-c6e3 "label" # human-readable alias
|
|
84
|
+
sessions-db link sess_xxx --task feat-foo.md # link to ticket / project
|
|
85
|
+
sessions-db link-parent sess_child sess_parent
|
|
86
|
+
sessions-db close sess_xxx --outcome done --reason "shipped"
|
|
87
|
+
sessions-db rebuild # rebuild projection from events
|
|
88
|
+
sessions-db sweep --dry-run # preview activity transitions
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The CLI is the same surface as the library API; both write through the
|
|
92
|
+
same primitives, so a workflow that mixes hook-driven CLI commands with
|
|
93
|
+
programmatic library calls observes a consistent projection.
|
|
94
|
+
|
|
95
|
+
### Hook setup (Claude Code SessionStart)
|
|
96
|
+
|
|
97
|
+
Add to your `~/.claude/settings.json`:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"hooks": {
|
|
102
|
+
"SessionStart": [{
|
|
103
|
+
"hooks": [{
|
|
104
|
+
"type": "command",
|
|
105
|
+
"command": "node /absolute/path/to/node_modules/@druumen/sessions-db/cli/sessions-db-session-start.mjs",
|
|
106
|
+
"timeout": 5
|
|
107
|
+
}]
|
|
108
|
+
}]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The hook is bootstrap-safe by design:
|
|
114
|
+
|
|
115
|
+
- Kill switch: set `DRUUMEN_SESSIONS_DB_DISABLED=1` to no-op the hook
|
|
116
|
+
without removing it from settings.
|
|
117
|
+
- 2-second hard timeout on every operation; the hook always exits 0 so
|
|
118
|
+
it never blocks Claude Code start, even on disk full / permission
|
|
119
|
+
denied / lockfile contention.
|
|
120
|
+
- Errors are logged to stderr (visible in Claude Code's session log)
|
|
121
|
+
but never surfaced as user-facing failures.
|
|
122
|
+
|
|
123
|
+
## Path resolution
|
|
124
|
+
|
|
125
|
+
When you don't pass an explicit `rootPath`, sessions-db walks a 5-priority
|
|
126
|
+
chain. First hit wins:
|
|
127
|
+
|
|
128
|
+
1. `opts.rootPath` — explicit caller arg (highest priority).
|
|
129
|
+
2. `DRUUMEN_SESSIONS_DB_ROOT` — env var override (cockpit Setup Wizard,
|
|
130
|
+
CI matrix runs, ops incident pinning).
|
|
131
|
+
3. cwd-ascend (≤12 levels) for an existing
|
|
132
|
+
`tickets/_logs/sessions-db.json` — preserves the druumen-monorepo
|
|
133
|
+
experience: any sessions-db command from anywhere inside the worktree
|
|
134
|
+
finds the canonical root.
|
|
135
|
+
4. cwd-ascend (≤12 levels) for an existing `.dru-code/sessions-db.json`
|
|
136
|
+
— the new convention for fresh installs that have already been
|
|
137
|
+
initialized once.
|
|
138
|
+
5. Default: `<cwd>/.dru-code/` — what fresh `initProjection({})` lands
|
|
139
|
+
when no existing storage is found. Cockpit marketplace's first
|
|
140
|
+
install creates this dir.
|
|
141
|
+
|
|
142
|
+
The ascend bound caps the worst-case stat budget at 24 (two candidate
|
|
143
|
+
file checks × 12 levels) before falling through to the default — the
|
|
144
|
+
resolver never accidentally walks to `/` on a slow networked mount.
|
|
145
|
+
|
|
146
|
+
The same three filenames are used at every layout:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
<root>/sessions-db-events.jsonl # append-only SSoT
|
|
150
|
+
<root>/sessions-db.json # projection cache
|
|
151
|
+
<root>/sessions-db.json.lock # exclusive-create lockfile
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Privacy
|
|
155
|
+
|
|
156
|
+
`first_prompt_preview` stores a sanitized 200-char excerpt of the first
|
|
157
|
+
user message in each session, so operators can recognize sessions in
|
|
158
|
+
the projection without re-opening transcripts. Sanitization strips:
|
|
159
|
+
|
|
160
|
+
- IDE-injected wrappers: `<ide_opened_file>`, `<ide_selection>`
|
|
161
|
+
- Slash command wrappers: `<command-name>`, `<command-message>`,
|
|
162
|
+
`<command-args>`
|
|
163
|
+
- System reminders: `<system-reminder>`, `<system>`, `<thinking>`
|
|
164
|
+
- Tool-use blocks: `<tool_use>`, `<tool_result>`, `<parameter>`,
|
|
165
|
+
`<function_calls>`
|
|
166
|
+
|
|
167
|
+
NFKC normalization is applied **before** stripping so fullwidth-bracket
|
|
168
|
+
splice attacks (e.g. `<system-reminder>`) cannot bypass the redactor.
|
|
169
|
+
The strip is double-pass — when removing one wrapper exposes a fresh
|
|
170
|
+
inner wrapper, the second pass catches it. Truncation is UTF-16
|
|
171
|
+
codepoint-safe (200 codepoints, not 200 bytes) so multi-byte characters
|
|
172
|
+
are not split mid-glyph.
|
|
173
|
+
|
|
174
|
+
### Privacy opt-out (available in 0.1.0)
|
|
175
|
+
|
|
176
|
+
To disable preview storage entirely — useful for marketplace audits,
|
|
177
|
+
shared-machine deployments, or any user who'd rather not persist the
|
|
178
|
+
human-readable first prompt:
|
|
179
|
+
|
|
180
|
+
**Library API:**
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
import { recordSessionSeen } from '@druumen/sessions-db';
|
|
184
|
+
|
|
185
|
+
await recordSessionSeen({
|
|
186
|
+
claudeSessionId,
|
|
187
|
+
// ...other opts...
|
|
188
|
+
storeFirstPrompt: false, // payload.first_prompt_preview = null
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Hook env var (Claude Code SessionStart):**
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
DRUUMEN_SESSIONS_DB_STORE_PREVIEW=0 \
|
|
196
|
+
claude code # or whatever spawns the hook
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`'0'` and `'false'` (case-insensitive) opt out; anything else (or unset)
|
|
200
|
+
keeps the default. Default is `true` — backward compatible with the
|
|
201
|
+
0.1.0-dev preview behavior.
|
|
202
|
+
|
|
203
|
+
Fingerprints (`first_human_prompt_v1`, `lineage_prefix_v1`) and
|
|
204
|
+
`transcript_file` metadata are intentionally **not** affected by this
|
|
205
|
+
opt-out, so identity reconciliation (resume / fork detection) keeps
|
|
206
|
+
working for opt-out users.
|
|
207
|
+
|
|
208
|
+
## Schema
|
|
209
|
+
|
|
210
|
+
The events log (`sessions-db-events.jsonl`) is the single source of
|
|
211
|
+
truth; the projection (`sessions-db.json`) is a derivable cache. Run
|
|
212
|
+
`sessions-db rebuild` at any time to regenerate the projection from
|
|
213
|
+
events — useful after manual events-log inspection / surgery.
|
|
214
|
+
|
|
215
|
+
`schema_version: 2` is the stable contract for the entire 0.1.x line.
|
|
216
|
+
Reducers stay backward-compatible: new optional fields may appear in
|
|
217
|
+
0.1.x minor releases, but no existing field is removed or repurposed.
|
|
218
|
+
Schema-breaking changes (rename, type change, removal) ship at 0.2.0+.
|
|
219
|
+
|
|
220
|
+
## Versioning
|
|
221
|
+
|
|
222
|
+
0.x semver:
|
|
223
|
+
|
|
224
|
+
- **Patch** (0.1.x): bug fixes, doc, internal refactors. No API change.
|
|
225
|
+
- **Minor** (0.x.0): additive only. New library exports, new CLI
|
|
226
|
+
subcommands, new optional projection fields. Existing surface is
|
|
227
|
+
unchanged.
|
|
228
|
+
- **Major** (1.0.0): commits the API as stable. Until then, treat 0.x as
|
|
229
|
+
"settling" — pin `>=0.1.0 <0.2.0` in your `package.json` if you want
|
|
230
|
+
field-additive but no breaking changes inside the 0.1 line.
|
|
231
|
+
|
|
232
|
+
Schema-breaking changes always coincide with at least a 0.x minor bump
|
|
233
|
+
(0.2.0+) and ship with a documented migration path.
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
Apache 2.0 — see [LICENSE](./LICENSE) and [NOTICE](./NOTICE).
|
|
238
|
+
|
|
239
|
+
## Roadmap
|
|
240
|
+
|
|
241
|
+
- **0.1.0** (current): Library + CLI + hook + 3-priority identity +
|
|
242
|
+
cross-platform (macOS / Linux verified in CI; Windows pending runner) +
|
|
243
|
+
privacy opt-out (`storeFirstPrompt: false` /
|
|
244
|
+
`DRUUMEN_SESSIONS_DB_STORE_PREVIEW=0`).
|
|
245
|
+
- **0.2.0** (TBD): parent_candidate auto-promote heuristic, outcome
|
|
246
|
+
auto-derive on `/task-done` linkage.
|
|
247
|
+
- **0.3.0** (TBD): Multi-machine sync (schema_version=3 break,
|
|
248
|
+
documented migration).
|
|
249
|
+
- **0.4.0+** (TBD): Web UI / VS Code Sessions panel via
|
|
250
|
+
[Druumen Cockpit](https://druumen.com).
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for write subcommands (alias / link / link-parent / close).
|
|
3
|
+
*
|
|
4
|
+
* Day 3: the actual mutation logic lives in `lib/operations.mjs`. These
|
|
5
|
+
* helpers handle the CLI surface only:
|
|
6
|
+
* - rendering planned events for `--dry-run`
|
|
7
|
+
* - mapping the library result `{ ok, event_id?, error? }` into stdout /
|
|
8
|
+
* stderr / exit code in three formats: human (default), `--json`,
|
|
9
|
+
* `--quiet`.
|
|
10
|
+
*
|
|
11
|
+
* Why keep the helpers? The five write handlers all share the same
|
|
12
|
+
* presentation logic — centralizing it keeps each handler focused on flag
|
|
13
|
+
* plumbing + the operations-call signature mapping.
|
|
14
|
+
*
|
|
15
|
+
* Note on output messages: the test suite regex-matches phrases like
|
|
16
|
+
* `ok: <op> written for <stable_id>` and `error: stable_id not found`. Any
|
|
17
|
+
* change to wording here MUST be paired with a sweep of __tests__/cli/*.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { newEvent } from '../lib/storage.mjs';
|
|
21
|
+
import { formatJSON } from './format.mjs';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Render a planned event for --dry-run. Always returns the event so callers
|
|
25
|
+
* can post-process if they need to. Output goes to stdout for easy piping.
|
|
26
|
+
*
|
|
27
|
+
* The intent is that the rendered output be machine-grep-able (op + stable_id
|
|
28
|
+
* + payload as JSON) so pipelines can audit what would change without
|
|
29
|
+
* actually writing.
|
|
30
|
+
*/
|
|
31
|
+
export function renderDryRun({ op, stableId, payload, json = false }) {
|
|
32
|
+
const event = newEvent({ op, stable_id: stableId, payload });
|
|
33
|
+
if (json) {
|
|
34
|
+
process.stdout.write(formatJSON({ dry_run: true, event }));
|
|
35
|
+
} else {
|
|
36
|
+
process.stdout.write(`[dry-run] would write event:\n`);
|
|
37
|
+
process.stdout.write(` op: ${op}\n`);
|
|
38
|
+
process.stdout.write(` stable_id: ${stableId}\n`);
|
|
39
|
+
process.stdout.write(` payload: ${JSON.stringify(payload)}\n`);
|
|
40
|
+
}
|
|
41
|
+
return event;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Standard success / failure feedback for write commands.
|
|
46
|
+
*
|
|
47
|
+
* Result shape comes straight from `lib/operations.mjs` —
|
|
48
|
+
* `{ ok, event_id?, error? }`. We render and return the exit code the caller
|
|
49
|
+
* should hand to process.exit().
|
|
50
|
+
*
|
|
51
|
+
* Exit code policy:
|
|
52
|
+
* - 0 = success
|
|
53
|
+
* - 1 = business error (stable_id not found, validation failure, lock
|
|
54
|
+
* timeout, cycle detection — anything `operations.*` returned with
|
|
55
|
+
* `{ ok: false }`)
|
|
56
|
+
*
|
|
57
|
+
* `--quiet` swallows stdout but preserves the exit code so cron / scripted
|
|
58
|
+
* usage stays observable via `$?`.
|
|
59
|
+
*/
|
|
60
|
+
export function reportResult({ result, op, stableId, json, quiet, extra = {} }) {
|
|
61
|
+
if (quiet) return result.ok ? 0 : 1;
|
|
62
|
+
if (json) {
|
|
63
|
+
process.stdout.write(formatJSON({
|
|
64
|
+
ok: result.ok,
|
|
65
|
+
op,
|
|
66
|
+
stable_id: stableId,
|
|
67
|
+
event_id: result.event_id,
|
|
68
|
+
error: result.error,
|
|
69
|
+
...extra,
|
|
70
|
+
}));
|
|
71
|
+
} else if (result.ok) {
|
|
72
|
+
process.stdout.write(`ok: ${op} written for ${stableId} (event_id=${result.event_id})\n`);
|
|
73
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
74
|
+
process.stdout.write(` ${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}\n`);
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
process.stderr.write(`error: ${op} failed for ${stableId}: ${result.error}\n`);
|
|
78
|
+
}
|
|
79
|
+
return result.ok ? 0 : 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Special-case the "stable_id not found" error so the CLI prints the
|
|
84
|
+
* historical exact phrase the tests pin against:
|
|
85
|
+
*
|
|
86
|
+
* error: stable_id not found: <id>
|
|
87
|
+
*
|
|
88
|
+
* The operations layer uses the same wording for that error, but it embeds
|
|
89
|
+
* it inside the call's `result.error`. When the wrapper detects this prefix
|
|
90
|
+
* it re-emits the bare phrase to stderr so the existing test regex
|
|
91
|
+
* `/stable_id not found/` and operator muscle memory keep working.
|
|
92
|
+
*
|
|
93
|
+
* Returns the exit code the handler should hand to process.exit() —
|
|
94
|
+
* typically 1 for a not-found, but the caller may pass `code` to override.
|
|
95
|
+
*/
|
|
96
|
+
export function reportStableIdNotFound(error, code = 1) {
|
|
97
|
+
process.stderr.write(`error: ${error}\n`);
|
|
98
|
+
return code;
|
|
99
|
+
}
|
package/cli/alias.mjs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sessions-db alias <stable_id> <alias>` — set or change the human-readable
|
|
3
|
+
* alias.
|
|
4
|
+
* `sessions-db alias <stable_id> --clear` — remove the alias (sets to null).
|
|
5
|
+
*
|
|
6
|
+
* Day 3 refactor: this handler is a thin wrapper around
|
|
7
|
+
* `lib/operations.setAlias` — argparse + dry-run rendering + result-to-exit
|
|
8
|
+
* mapping only. Existence-check is performed by the operation BEFORE the
|
|
9
|
+
* write so a typo'd stable_id surfaces as a clean exit-1 message instead
|
|
10
|
+
* of a synthesized empty session record.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { setAlias } from '../lib/operations.mjs';
|
|
14
|
+
import { ArgparseError, formatHelp, parseArgs } from './argparse.mjs';
|
|
15
|
+
import { renderDryRun, reportResult, reportStableIdNotFound } from './_write-helpers.mjs';
|
|
16
|
+
|
|
17
|
+
const SPEC = {
|
|
18
|
+
positional: [
|
|
19
|
+
{ name: 'stable_id', required: true },
|
|
20
|
+
{ name: 'alias', required: false },
|
|
21
|
+
],
|
|
22
|
+
flags: {
|
|
23
|
+
'--clear': { type: 'boolean' },
|
|
24
|
+
'--dry-run': { type: 'boolean' },
|
|
25
|
+
'--json': { type: 'boolean' },
|
|
26
|
+
'--root': { type: 'string' },
|
|
27
|
+
'--quiet': { type: 'boolean' },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const HELP = formatHelp({
|
|
32
|
+
usage: 'sessions-db alias <stable_id> <alias> | sessions-db alias <stable_id> --clear',
|
|
33
|
+
summary: 'Set, change, or clear the human-readable alias for a session.',
|
|
34
|
+
flags: [
|
|
35
|
+
{ name: '--clear', desc: 'remove the alias (sets to null)' },
|
|
36
|
+
{ name: '--dry-run', desc: 'print the planned event but do not write' },
|
|
37
|
+
{ name: '--json', desc: 'JSON output (machine-readable)' },
|
|
38
|
+
{ name: '--root <p>', desc: 'override storage root (default cwd)' },
|
|
39
|
+
],
|
|
40
|
+
examples: [
|
|
41
|
+
'sessions-db alias sess_01970000-... pricing-overhaul-main',
|
|
42
|
+
'sessions-db alias sess_01970000-... --clear',
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export async function run(argv) {
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
parsed = parseArgs(argv, SPEC);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (err instanceof ArgparseError) {
|
|
52
|
+
process.stderr.write(`error: ${err.message}\n\n${HELP}`);
|
|
53
|
+
process.exit(err.exitCode);
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (parsed.helpRequested) {
|
|
59
|
+
process.stdout.write(HELP);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const stableId = parsed.positional.stable_id;
|
|
64
|
+
const aliasArg = parsed.positional.alias;
|
|
65
|
+
const clear = parsed.flags['--clear'] === true;
|
|
66
|
+
const root = parsed.flags['--root'];
|
|
67
|
+
const dryRun = parsed.flags['--dry-run'] === true;
|
|
68
|
+
const json = parsed.flags['--json'] === true;
|
|
69
|
+
const quiet = parsed.flags['--quiet'] === true;
|
|
70
|
+
|
|
71
|
+
// Mutually-exclusive intent check: must be EITHER alias positional OR
|
|
72
|
+
// --clear. Both or neither is an argparse-class error (exit 2). The
|
|
73
|
+
// operations layer also rejects this combination, but doing the check
|
|
74
|
+
// here keeps the error stream identical to the historical CLI behavior
|
|
75
|
+
// (no library prefix in the message).
|
|
76
|
+
if (clear && aliasArg !== undefined) {
|
|
77
|
+
process.stderr.write(`error: alias and --clear are mutually exclusive\n`);
|
|
78
|
+
process.exit(2);
|
|
79
|
+
}
|
|
80
|
+
if (!clear && aliasArg === undefined) {
|
|
81
|
+
process.stderr.write(`error: provide an alias positional or --clear\n`);
|
|
82
|
+
process.exit(2);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (dryRun) {
|
|
86
|
+
const payload = clear ? { alias: null } : { alias: aliasArg };
|
|
87
|
+
renderDryRun({ op: 'alias_set', stableId, payload, json });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const opts = root ? { root } : {};
|
|
92
|
+
const result = clear
|
|
93
|
+
? await setAlias({ stableId, clear: true, ...opts })
|
|
94
|
+
: await setAlias({ stableId, alias: aliasArg, ...opts });
|
|
95
|
+
|
|
96
|
+
// Stable-id-not-found gets the historical "error: stable_id not found:
|
|
97
|
+
// <id>" phrasing (no `op failed for ...` prefix). Operations returns the
|
|
98
|
+
// bare phrase as `result.error`; we recognize it and bypass reportResult
|
|
99
|
+
// for that one case so muscle-memory regex `/stable_id not found/` keeps
|
|
100
|
+
// matching.
|
|
101
|
+
if (!result.ok && typeof result.error === 'string'
|
|
102
|
+
&& result.error.startsWith('stable_id not found:')) {
|
|
103
|
+
if (!quiet) {
|
|
104
|
+
const code = reportStableIdNotFound(result.error);
|
|
105
|
+
process.exit(code);
|
|
106
|
+
}
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const code = reportResult({
|
|
111
|
+
result, op: 'alias_set', stableId, json, quiet,
|
|
112
|
+
extra: clear ? { cleared: true } : { alias: aliasArg },
|
|
113
|
+
});
|
|
114
|
+
if (code !== 0) process.exit(code);
|
|
115
|
+
}
|