claude-code-session-manager 0.25.0 → 0.26.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/dist/index.html CHANGED
@@ -7,10 +7,10 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
- <script type="module" crossorigin src="./assets/index-H0IXEKiC.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-C61eFtxs.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
12
12
  <link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
13
- <link rel="stylesheet" crossorigin href="./assets/index-Cu9X6oyA.css">
13
+ <link rel="stylesheet" crossorigin href="./assets/index-BKkf6gtA.css">
14
14
  </head>
15
15
  <body class="bg-bg text-fg font-sans antialiased">
16
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
5
5
  "type": "module",
6
6
  "main": "src/main/index.cjs",
@@ -0,0 +1,183 @@
1
+ /**
2
+ * dod-batchkey.test.cjs — unit tests for definitionOfDone.cjs helpers.
3
+ *
4
+ * Run: timeout 120 node --test src/main/__tests__/dod-batchkey.test.cjs
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { test } = require('node:test');
10
+ const assert = require('node:assert/strict');
11
+ const os = require('node:os');
12
+ const fs = require('node:fs');
13
+ const path = require('node:path');
14
+ const { batchKey, reportPathFor, reportExists } = require('../lib/definitionOfDone.cjs');
15
+
16
+ // ─── helpers ──────────────────────────────────────────────────────────────────
17
+
18
+ function makeTmpDir() {
19
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'dod-batchkey-test-'));
20
+ }
21
+
22
+ function rmdir(dir) {
23
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* */ }
24
+ }
25
+
26
+ function makeJob(slug, runId) {
27
+ return { slug, runId };
28
+ }
29
+
30
+ // ─── batchKey: order-independence ─────────────────────────────────────────────
31
+
32
+ test('batchKey is order-independent', () => {
33
+ const jobsA = [
34
+ makeJob('101-foo', '2026-06-01T10-00-00-000Z'),
35
+ makeJob('102-bar', '2026-06-01T11-00-00-000Z'),
36
+ ];
37
+ const jobsB = [
38
+ makeJob('102-bar', '2026-06-01T11-00-00-000Z'),
39
+ makeJob('101-foo', '2026-06-01T10-00-00-000Z'),
40
+ ];
41
+ assert.strictEqual(batchKey(jobsA), batchKey(jobsB));
42
+ });
43
+
44
+ // ─── batchKey: adding a real job changes the key ─────────────────────────────
45
+
46
+ test('adding a real job changes the batchKey', () => {
47
+ const base = [
48
+ makeJob('101-foo', '2026-06-01T10-00-00-000Z'),
49
+ ];
50
+ const extended = [
51
+ makeJob('101-foo', '2026-06-01T10-00-00-000Z'),
52
+ makeJob('102-bar', '2026-06-01T11-00-00-000Z'),
53
+ ];
54
+ assert.notStrictEqual(batchKey(base), batchKey(extended));
55
+ });
56
+
57
+ // ─── batchKey: adding a meta/dod job does NOT change the key ─────────────────
58
+
59
+ test('adding a dod-prefixed slug does not change batchKey', () => {
60
+ const base = [makeJob('101-foo', '2026-06-01T10-00-00-000Z')];
61
+ const withDod = [
62
+ makeJob('101-foo', '2026-06-01T10-00-00-000Z'),
63
+ makeJob('dod-gate', '2026-06-01T12-00-00-000Z'),
64
+ ];
65
+ assert.strictEqual(batchKey(base), batchKey(withDod));
66
+ });
67
+
68
+ test('adding a dod-suffixed slug does not change batchKey', () => {
69
+ const base = [makeJob('101-foo', '2026-06-01T10-00-00-000Z')];
70
+ const withDod = [
71
+ makeJob('101-foo', '2026-06-01T10-00-00-000Z'),
72
+ makeJob('gate-dod', '2026-06-01T12-00-00-000Z'),
73
+ ];
74
+ assert.strictEqual(batchKey(base), batchKey(withDod));
75
+ });
76
+
77
+ test('adding a definition-of-done slug does not change batchKey', () => {
78
+ const base = [makeJob('101-foo', '2026-06-01T10-00-00-000Z')];
79
+ const withDod = [
80
+ makeJob('101-foo', '2026-06-01T10-00-00-000Z'),
81
+ makeJob('definition-of-done-pass', '2026-06-01T12-00-00-000Z'),
82
+ ];
83
+ assert.strictEqual(batchKey(base), batchKey(withDod));
84
+ });
85
+
86
+ test('adding an inline dod slug (dod between dashes) does not change batchKey', () => {
87
+ const base = [makeJob('101-foo', '2026-06-01T10-00-00-000Z')];
88
+ const withDod = [
89
+ makeJob('101-foo', '2026-06-01T10-00-00-000Z'),
90
+ makeJob('batch-dod-check', '2026-06-01T12-00-00-000Z'),
91
+ ];
92
+ assert.strictEqual(batchKey(base), batchKey(withDod));
93
+ });
94
+
95
+ // ─── batchKey: empty set ──────────────────────────────────────────────────────
96
+
97
+ test('empty job list produces a stable key', () => {
98
+ assert.strictEqual(typeof batchKey([]), 'string');
99
+ assert.strictEqual(batchKey([]), batchKey([]));
100
+ });
101
+
102
+ // ─── batchKey: same slug different runId produces different keys ──────────────
103
+
104
+ test('same slug but different runId produces different keys', () => {
105
+ const a = [makeJob('101-foo', '2026-06-01T10-00-00-000Z')];
106
+ const b = [makeJob('101-foo', '2026-06-01T11-00-00-000Z')];
107
+ assert.notStrictEqual(batchKey(a), batchKey(b));
108
+ });
109
+
110
+ // ─── reportPathFor ────────────────────────────────────────────────────────────
111
+
112
+ test('reportPathFor returns a path ending with definition-of-done-<key>.md', () => {
113
+ const key = batchKey([makeJob('101-foo', '2026-06-01T10-00-00-000Z')]);
114
+ const p = reportPathFor(key);
115
+ assert.ok(p.endsWith(`definition-of-done-${key}.md`), `path: ${p}`);
116
+ assert.ok(p.includes('scheduled-plans/runs/'), `path: ${p}`);
117
+ });
118
+
119
+ // ─── reportExists ─────────────────────────────────────────────────────────────
120
+
121
+ test('reportExists returns false when runs/ dir does not exist', () => {
122
+ const key = batchKey([makeJob('101-foo', '2026-06-01T10-00-00-000Z')]);
123
+ const tmpDir = makeTmpDir();
124
+ try {
125
+ const runsDir = path.join(tmpDir, 'runs');
126
+ // runsDir intentionally not created
127
+ assert.strictEqual(reportExists(key, runsDir), false);
128
+ } finally {
129
+ rmdir(tmpDir);
130
+ }
131
+ });
132
+
133
+ test('reportExists returns false when no matching file exists', () => {
134
+ const key = batchKey([makeJob('101-foo', '2026-06-01T10-00-00-000Z')]);
135
+ const tmpDir = makeTmpDir();
136
+ try {
137
+ const runsDir = path.join(tmpDir, 'runs');
138
+ const runDir = path.join(runsDir, '2026-06-01T10-00-00-000Z');
139
+ fs.mkdirSync(runDir, { recursive: true });
140
+ fs.writeFileSync(path.join(runDir, 'some-other-file.md'), '# other');
141
+ assert.strictEqual(reportExists(key, runsDir), false);
142
+ } finally {
143
+ rmdir(tmpDir);
144
+ }
145
+ });
146
+
147
+ test('reportExists returns true when matching file exists in any run subdir', () => {
148
+ const key = batchKey([makeJob('101-foo', '2026-06-01T10-00-00-000Z')]);
149
+ const tmpDir = makeTmpDir();
150
+ try {
151
+ const runsDir = path.join(tmpDir, 'runs');
152
+ const runDir = path.join(runsDir, '2026-06-01T10-00-00-000Z');
153
+ fs.mkdirSync(runDir, { recursive: true });
154
+ fs.writeFileSync(
155
+ path.join(runDir, `definition-of-done-${key}.md`),
156
+ '# DoD report'
157
+ );
158
+ assert.strictEqual(reportExists(key, runsDir), true);
159
+ } finally {
160
+ rmdir(tmpDir);
161
+ }
162
+ });
163
+
164
+ test('reportExists finds match in a nested run directory (not first subdir)', () => {
165
+ const key = batchKey([makeJob('101-foo', '2026-06-01T10-00-00-000Z')]);
166
+ const tmpDir = makeTmpDir();
167
+ try {
168
+ const runsDir = path.join(tmpDir, 'runs');
169
+ // Two subdirs; match is in the second
170
+ const runDir1 = path.join(runsDir, '2026-06-01T09-00-00-000Z');
171
+ const runDir2 = path.join(runsDir, '2026-06-01T10-00-00-000Z');
172
+ fs.mkdirSync(runDir1, { recursive: true });
173
+ fs.mkdirSync(runDir2, { recursive: true });
174
+ fs.writeFileSync(path.join(runDir1, 'other.md'), '# other');
175
+ fs.writeFileSync(
176
+ path.join(runDir2, `definition-of-done-${key}.md`),
177
+ '# DoD report'
178
+ );
179
+ assert.strictEqual(reportExists(key, runsDir), true);
180
+ } finally {
181
+ rmdir(tmpDir);
182
+ }
183
+ });
@@ -0,0 +1,197 @@
1
+ /**
2
+ * dod-drain-hook.test.cjs — drives the DoD drain handler.
3
+ *
4
+ * Run: timeout 120 node --test src/main/__tests__/dod-drain-hook.test.cjs
5
+ *
6
+ * Scenarios:
7
+ * 1. Drain with completed jobs → writes report once.
8
+ * 2. Second drain over same completed set → no-op (idempotent).
9
+ * 3. SM_DOD_DISABLE=1 → no report.
10
+ * 4. state.paused set → no report.
11
+ * 5. cancelToken.cancelled = true → no report.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const { test, beforeEach, afterEach } = require('node:test');
17
+ const assert = require('node:assert/strict');
18
+ const os = require('node:os');
19
+ const fs = require('node:fs');
20
+ const path = require('node:path');
21
+ const { runDefinitionOfDoneOnDrain } = require('../lib/dodDrainHook.cjs');
22
+
23
+ // ─── helpers ──────────────────────────────────────────────────────────────────
24
+
25
+ function makeTmpDir() {
26
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'dod-drain-hook-test-'));
27
+ }
28
+
29
+ function rmdir(dir) {
30
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* */ }
31
+ }
32
+
33
+ /** Write a minimal PRD and return a completed-job record. */
34
+ function writePrd(prdsDir, slug, cwd) {
35
+ const body = [
36
+ '---',
37
+ `title: ${slug}`,
38
+ `cwd: ${cwd}`,
39
+ 'estimateMinutes: 5',
40
+ '---',
41
+ '',
42
+ '# Goal',
43
+ '',
44
+ 'Test fixture.',
45
+ '',
46
+ '# Acceptance criteria',
47
+ '',
48
+ '- [ ] `timeout 10 node -e "process.exit(0)"` passes.',
49
+ '',
50
+ '# Out of scope',
51
+ '',
52
+ '- N/A',
53
+ ].join('\n');
54
+ fs.writeFileSync(path.join(prdsDir, `${slug}.md`), body);
55
+ return { slug, cwd, status: 'completed', runId: '2026-06-14T00-00-00-000Z' };
56
+ }
57
+
58
+ /** Count DoD report files under runsDir (shallow scan). */
59
+ function countReports(runsDir) {
60
+ let count = 0;
61
+ try {
62
+ for (const entry of fs.readdirSync(runsDir, { withFileTypes: true })) {
63
+ if (!entry.isDirectory()) continue;
64
+ const dir = path.join(runsDir, entry.name);
65
+ for (const f of fs.readdirSync(dir)) {
66
+ if (f.startsWith('definition-of-done-')) count++;
67
+ }
68
+ }
69
+ } catch { /* runsDir may not exist */ }
70
+ return count;
71
+ }
72
+
73
+ // ─── tests ────────────────────────────────────────────────────────────────────
74
+
75
+ test('drain with completed jobs writes exactly one report', async () => {
76
+ const tmpDir = makeTmpDir();
77
+ const prdsDir = path.join(tmpDir, 'prds');
78
+ const runsDir = path.join(tmpDir, 'runs');
79
+ const cwd = path.join(tmpDir, 'project');
80
+ fs.mkdirSync(prdsDir);
81
+ fs.mkdirSync(cwd);
82
+
83
+ const job = writePrd(prdsDir, '101-test', cwd);
84
+ const state = { paused: null, jobs: [job] };
85
+ const cancelToken = { cancelled: false };
86
+ const savedDisable = process.env.SM_DOD_DISABLE;
87
+ delete process.env.SM_DOD_DISABLE;
88
+
89
+ try {
90
+ await runDefinitionOfDoneOnDrain(state, { cancelToken, prdsDir, runsDir });
91
+ assert.strictEqual(countReports(runsDir), 1);
92
+ } finally {
93
+ if (savedDisable !== undefined) process.env.SM_DOD_DISABLE = savedDisable;
94
+ rmdir(tmpDir);
95
+ }
96
+ });
97
+
98
+ test('second drain over same completed set is a no-op (idempotent)', async () => {
99
+ const tmpDir = makeTmpDir();
100
+ const prdsDir = path.join(tmpDir, 'prds');
101
+ const runsDir = path.join(tmpDir, 'runs');
102
+ const cwd = path.join(tmpDir, 'project');
103
+ fs.mkdirSync(prdsDir);
104
+ fs.mkdirSync(cwd);
105
+
106
+ const job = writePrd(prdsDir, '102-test', cwd);
107
+ const state = { paused: null, jobs: [job] };
108
+ const cancelToken = { cancelled: false };
109
+ const savedDisable = process.env.SM_DOD_DISABLE;
110
+ delete process.env.SM_DOD_DISABLE;
111
+
112
+ try {
113
+ await runDefinitionOfDoneOnDrain(state, { cancelToken, prdsDir, runsDir });
114
+ const afterFirst = countReports(runsDir);
115
+ assert.strictEqual(afterFirst, 1, 'first call must write one report');
116
+
117
+ await runDefinitionOfDoneOnDrain(state, { cancelToken, prdsDir, runsDir });
118
+ const afterSecond = countReports(runsDir);
119
+ assert.strictEqual(afterSecond, 1, 'second call with same jobs must be no-op');
120
+ } finally {
121
+ if (savedDisable !== undefined) process.env.SM_DOD_DISABLE = savedDisable;
122
+ rmdir(tmpDir);
123
+ }
124
+ });
125
+
126
+ test('SM_DOD_DISABLE=1 → no report written', async () => {
127
+ const tmpDir = makeTmpDir();
128
+ const prdsDir = path.join(tmpDir, 'prds');
129
+ const runsDir = path.join(tmpDir, 'runs');
130
+ const cwd = path.join(tmpDir, 'project');
131
+ fs.mkdirSync(prdsDir);
132
+ fs.mkdirSync(cwd);
133
+
134
+ writePrd(prdsDir, '103-test', cwd);
135
+ const job = { slug: '103-test', cwd, status: 'completed', runId: '2026-06-14T00-00-00-001Z' };
136
+ const state = { paused: null, jobs: [job] };
137
+ const cancelToken = { cancelled: false };
138
+ const savedDisable = process.env.SM_DOD_DISABLE;
139
+ process.env.SM_DOD_DISABLE = '1';
140
+
141
+ try {
142
+ await runDefinitionOfDoneOnDrain(state, { cancelToken, prdsDir, runsDir });
143
+ assert.strictEqual(countReports(runsDir), 0, 'SM_DOD_DISABLE=1 must suppress the report');
144
+ } finally {
145
+ if (savedDisable !== undefined) process.env.SM_DOD_DISABLE = savedDisable;
146
+ else delete process.env.SM_DOD_DISABLE;
147
+ rmdir(tmpDir);
148
+ }
149
+ });
150
+
151
+ test('paused state → no report written', async () => {
152
+ const tmpDir = makeTmpDir();
153
+ const prdsDir = path.join(tmpDir, 'prds');
154
+ const runsDir = path.join(tmpDir, 'runs');
155
+ const cwd = path.join(tmpDir, 'project');
156
+ fs.mkdirSync(prdsDir);
157
+ fs.mkdirSync(cwd);
158
+
159
+ writePrd(prdsDir, '104-test', cwd);
160
+ const job = { slug: '104-test', cwd, status: 'completed', runId: '2026-06-14T00-00-00-002Z' };
161
+ const state = { paused: { reason: 'rate_limit', since: new Date().toISOString(), resumeAt: null }, jobs: [job] };
162
+ const cancelToken = { cancelled: false };
163
+ const savedDisable = process.env.SM_DOD_DISABLE;
164
+ delete process.env.SM_DOD_DISABLE;
165
+
166
+ try {
167
+ await runDefinitionOfDoneOnDrain(state, { cancelToken, prdsDir, runsDir });
168
+ assert.strictEqual(countReports(runsDir), 0, 'paused state must suppress the report');
169
+ } finally {
170
+ if (savedDisable !== undefined) process.env.SM_DOD_DISABLE = savedDisable;
171
+ rmdir(tmpDir);
172
+ }
173
+ });
174
+
175
+ test('cancelToken.cancelled=true → no report written', async () => {
176
+ const tmpDir = makeTmpDir();
177
+ const prdsDir = path.join(tmpDir, 'prds');
178
+ const runsDir = path.join(tmpDir, 'runs');
179
+ const cwd = path.join(tmpDir, 'project');
180
+ fs.mkdirSync(prdsDir);
181
+ fs.mkdirSync(cwd);
182
+
183
+ writePrd(prdsDir, '105-test', cwd);
184
+ const job = { slug: '105-test', cwd, status: 'completed', runId: '2026-06-14T00-00-00-003Z' };
185
+ const state = { paused: null, jobs: [job] };
186
+ const cancelToken = { cancelled: true };
187
+ const savedDisable = process.env.SM_DOD_DISABLE;
188
+ delete process.env.SM_DOD_DISABLE;
189
+
190
+ try {
191
+ await runDefinitionOfDoneOnDrain(state, { cancelToken, prdsDir, runsDir });
192
+ assert.strictEqual(countReports(runsDir), 0, 'cancelled token must suppress the report');
193
+ } finally {
194
+ if (savedDisable !== undefined) process.env.SM_DOD_DISABLE = savedDisable;
195
+ rmdir(tmpDir);
196
+ }
197
+ });