@skillsmith/cli 0.6.1 → 0.6.3

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/src/commands/index.d.ts +1 -0
  4. package/dist/src/commands/index.d.ts.map +1 -1
  5. package/dist/src/commands/index.js +2 -0
  6. package/dist/src/commands/index.js.map +1 -1
  7. package/dist/src/commands/info.js +1 -1
  8. package/dist/src/commands/install.js +1 -1
  9. package/dist/src/commands/install.js.map +1 -1
  10. package/dist/src/commands/search.d.ts.map +1 -1
  11. package/dist/src/commands/search.helpers.d.ts +29 -0
  12. package/dist/src/commands/search.helpers.d.ts.map +1 -1
  13. package/dist/src/commands/search.helpers.js +39 -1
  14. package/dist/src/commands/search.helpers.js.map +1 -1
  15. package/dist/src/commands/search.js +26 -2
  16. package/dist/src/commands/search.js.map +1 -1
  17. package/dist/src/commands/search.test.d.ts +10 -0
  18. package/dist/src/commands/search.test.d.ts.map +1 -0
  19. package/dist/src/commands/search.test.js +179 -0
  20. package/dist/src/commands/search.test.js.map +1 -0
  21. package/dist/src/commands/sync.d.ts.map +1 -1
  22. package/dist/src/commands/sync.helpers.d.ts +29 -1
  23. package/dist/src/commands/sync.helpers.d.ts.map +1 -1
  24. package/dist/src/commands/sync.helpers.js +38 -0
  25. package/dist/src/commands/sync.helpers.js.map +1 -1
  26. package/dist/src/commands/sync.js +20 -1
  27. package/dist/src/commands/sync.js.map +1 -1
  28. package/dist/src/commands/telemetry.d.ts +38 -0
  29. package/dist/src/commands/telemetry.d.ts.map +1 -0
  30. package/dist/src/commands/telemetry.helpers.d.ts +58 -0
  31. package/dist/src/commands/telemetry.helpers.d.ts.map +1 -0
  32. package/dist/src/commands/telemetry.helpers.js +173 -0
  33. package/dist/src/commands/telemetry.helpers.js.map +1 -0
  34. package/dist/src/commands/telemetry.helpers.test.d.ts +15 -0
  35. package/dist/src/commands/telemetry.helpers.test.d.ts.map +1 -0
  36. package/dist/src/commands/telemetry.helpers.test.js +240 -0
  37. package/dist/src/commands/telemetry.helpers.test.js.map +1 -0
  38. package/dist/src/commands/telemetry.js +354 -0
  39. package/dist/src/commands/telemetry.js.map +1 -0
  40. package/dist/src/commands/telemetry.test.d.ts +18 -0
  41. package/dist/src/commands/telemetry.test.d.ts.map +1 -0
  42. package/dist/src/commands/telemetry.test.js +401 -0
  43. package/dist/src/commands/telemetry.test.js.map +1 -0
  44. package/dist/src/index.js +3 -1
  45. package/dist/src/index.js.map +1 -1
  46. package/dist/src/utils/manifest.d.ts +75 -0
  47. package/dist/src/utils/manifest.d.ts.map +1 -1
  48. package/dist/src/utils/manifest.js +78 -0
  49. package/dist/src/utils/manifest.js.map +1 -1
  50. package/dist/src/utils/manifest.test.d.ts +20 -0
  51. package/dist/src/utils/manifest.test.d.ts.map +1 -0
  52. package/dist/src/utils/manifest.test.js +344 -0
  53. package/dist/src/utils/manifest.test.js.map +1 -0
  54. package/dist/src/utils/open-database.d.ts.map +1 -1
  55. package/dist/src/utils/open-database.js +33 -4
  56. package/dist/src/utils/open-database.js.map +1 -1
  57. package/dist/tests/embedding-probe-stdio.test.d.ts +11 -0
  58. package/dist/tests/embedding-probe-stdio.test.d.ts.map +1 -0
  59. package/dist/tests/embedding-probe-stdio.test.js +58 -0
  60. package/dist/tests/embedding-probe-stdio.test.js.map +1 -0
  61. package/dist/tests/open-database.test.js +25 -0
  62. package/dist/tests/open-database.test.js.map +1 -1
  63. package/dist/tests/sync.auth-ux.test.d.ts +14 -0
  64. package/dist/tests/sync.auth-ux.test.d.ts.map +1 -0
  65. package/dist/tests/sync.auth-ux.test.js +148 -0
  66. package/dist/tests/sync.auth-ux.test.js.map +1 -0
  67. package/package.json +5 -4
  68. package/templates/skill-telemetry.cmd +16 -0
  69. package/templates/skill-telemetry.sh +199 -0
@@ -2,6 +2,7 @@
2
2
  * @fileoverview Thin manifest reader for the Skillsmith CLI
3
3
  * @module @skillsmith/cli/utils/manifest
4
4
  * @see SMI-skill-version-tracking Wave 2
5
+ * @see SMI-5012 Wave 3: telemetry block + annual anonymous_id rotation
5
6
  *
6
7
  * Reads (and optionally writes) the ~/.skillsmith/manifest.json file.
7
8
  * This mirrors the SkillManifest types defined in
@@ -10,7 +11,16 @@
10
11
  *
11
12
  * The CLI owns its own read path; write operations (pin/unpin) use
12
13
  * updateManifestEntry below which does an atomic temp-file rename.
14
+ *
15
+ * Concurrency note (v1): the atomic temp-file rename guarantees readers never
16
+ * see partial writes (POSIX rename(2) is atomic). A full file-lock (proper-lockfile)
17
+ * is NOT used here because proper-lockfile is not in the CLI's dependencies.
18
+ * v1 assumes single-writer-per-process; concurrent writers from the same machine
19
+ * (e.g., two concurrent `skillsmith telemetry` calls) may last-write-wins.
20
+ * This matches the shared-state matrix entry (plan line 715) which notes the
21
+ * proper-lockfile pattern but is aspirational for v2.
13
22
  */
23
+ import { createHash, randomUUID } from 'crypto';
14
24
  import { readFile, writeFile, mkdir, rename } from 'fs/promises';
15
25
  import { join, dirname } from 'path';
16
26
  import { homedir } from 'os';
@@ -52,4 +62,72 @@ export async function updateManifestEntry(updateFn) {
52
62
  const updated = updateFn(manifest);
53
63
  await saveManifest(updated);
54
64
  }
65
+ // ============================================================================
66
+ // Telemetry helpers (SMI-5012 Wave 3)
67
+ // ============================================================================
68
+ const ROTATION_DAYS = 365;
69
+ const OVERLAP_DAYS = 7;
70
+ const MS_PER_DAY = 86_400_000;
71
+ /**
72
+ * Generate a new anonymous telemetry id.
73
+ *
74
+ * Returns SHA-256(crypto.randomUUID()) as a 64-character lowercase hex string.
75
+ * The UUID is not stored; only the hash reaches the wire (plan line 719).
76
+ */
77
+ export function generateAnonymousId() {
78
+ return createHash('sha256').update(randomUUID()).digest('hex');
79
+ }
80
+ /**
81
+ * Returns true if the manifest's anonymousId should be rotated.
82
+ *
83
+ * Rotation is triggered when anonymousIdCreatedAt is older than 365 days.
84
+ * Returns false if the field is absent (id was never generated).
85
+ */
86
+ export function shouldRotateAnonymousId(manifest) {
87
+ const createdAt = manifest.telemetry?.anonymousIdCreatedAt;
88
+ if (!createdAt)
89
+ return false;
90
+ const ageMs = Date.now() - new Date(createdAt).getTime();
91
+ return ageMs > ROTATION_DAYS * MS_PER_DAY;
92
+ }
93
+ /**
94
+ * Rotate the anonymous id.
95
+ *
96
+ * Moves the current id to previousAnonymousId with a 7-day retirement window,
97
+ * generates a fresh id, and updates anonymousIdCreatedAt to now.
98
+ * Returns a new TelemetryManifest — does NOT write to disk.
99
+ * Callers must persist via saveManifest / updateManifestEntry.
100
+ */
101
+ export function rotateAnonymousId(manifest) {
102
+ const current = manifest.telemetry ?? { enabled: false };
103
+ const now = new Date();
104
+ const retiredAt = new Date(now.getTime() + OVERLAP_DAYS * MS_PER_DAY);
105
+ const next = {
106
+ ...current,
107
+ anonymousId: generateAnonymousId(),
108
+ anonymousIdCreatedAt: now.toISOString(),
109
+ previousAnonymousIdRetiredAt: retiredAt.toISOString(),
110
+ };
111
+ if (current.anonymousId !== undefined) {
112
+ next.previousAnonymousId = current.anonymousId;
113
+ }
114
+ return next;
115
+ }
116
+ /**
117
+ * Remove previousAnonymousId once its retirement window has passed.
118
+ *
119
+ * Should be called on every `skillsmith telemetry status` run and before
120
+ * each hook-script read. No-op if the window has not yet elapsed or if
121
+ * there is no previous id. Returns a new TelemetryManifest.
122
+ */
123
+ export function sweepExpiredPreviousId(manifest) {
124
+ const t = manifest.telemetry ?? { enabled: false };
125
+ if (!t.previousAnonymousIdRetiredAt)
126
+ return t;
127
+ const retiredAt = new Date(t.previousAnonymousIdRetiredAt).getTime();
128
+ if (Date.now() < retiredAt)
129
+ return t;
130
+ const { previousAnonymousId: _a, previousAnonymousIdRetiredAt: _b, ...rest } = t;
131
+ return rest;
132
+ }
55
133
  //# sourceMappingURL=manifest.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../src/utils/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAChE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAA;AA0B5B,+EAA+E;AAC/E,QAAQ;AACR,+EAA+E;AAE/E,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,aAAa,CAAC,CAAA;AACrD,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,CAAA;AAElE,+EAA+E;AAC/E,eAAe;AACf,+EAA+E;AAE/E;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC,CAAA;QACtD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAkB,CAAA;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE,CAAA;IAClD,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAuB;IACxD,MAAM,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACxD,MAAM,OAAO,GAAG,GAAG,aAAa,QAAQ,OAAO,CAAC,GAAG,EAAE,CAAA;IACrD,MAAM,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;IAC3D,MAAM,MAAM,CAAC,OAAO,EAAE,aAAa,CAAC,CAAA;AACtC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAAoD;IAEpD,MAAM,QAAQ,GAAG,MAAM,YAAY,EAAE,CAAA;IACrC,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAA;IAClC,MAAM,YAAY,CAAC,OAAO,CAAC,CAAA;AAC7B,CAAC"}
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../src/utils/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @fileoverview Tests for manifest utilities — telemetry block (SMI-5012 W3)
3
+ * @module @skillsmith/cli/utils/manifest.test
4
+ *
5
+ * Tests the TelemetryManifest schema additions and the four helper functions:
6
+ * generateAnonymousId, shouldRotateAnonymousId, rotateAnonymousId,
7
+ * sweepExpiredPreviousId
8
+ *
9
+ * Also covers manifest roundtrip (save → load preserves telemetry block) and
10
+ * the atomic temp-file-rename code path for the sequential-correctness
11
+ * concurrent-writer test.
12
+ *
13
+ * Concurrency note: vitest does not support true OS-level concurrency in a
14
+ * single process. The "concurrent writer" test exercises the temp-file rename
15
+ * code path sequentially — two writes to the same file where the second must
16
+ * fully overwrite the first without leaving partial state. True lock contention
17
+ * is a v2 concern tracked in the shared-state matrix (plan line 715).
18
+ */
19
+ export {};
20
+ //# sourceMappingURL=manifest.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.test.d.ts","sourceRoot":"","sources":["../../../src/utils/manifest.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG"}
@@ -0,0 +1,344 @@
1
+ /**
2
+ * @fileoverview Tests for manifest utilities — telemetry block (SMI-5012 W3)
3
+ * @module @skillsmith/cli/utils/manifest.test
4
+ *
5
+ * Tests the TelemetryManifest schema additions and the four helper functions:
6
+ * generateAnonymousId, shouldRotateAnonymousId, rotateAnonymousId,
7
+ * sweepExpiredPreviousId
8
+ *
9
+ * Also covers manifest roundtrip (save → load preserves telemetry block) and
10
+ * the atomic temp-file-rename code path for the sequential-correctness
11
+ * concurrent-writer test.
12
+ *
13
+ * Concurrency note: vitest does not support true OS-level concurrency in a
14
+ * single process. The "concurrent writer" test exercises the temp-file rename
15
+ * code path sequentially — two writes to the same file where the second must
16
+ * fully overwrite the first without leaving partial state. True lock contention
17
+ * is a v2 concern tracked in the shared-state matrix (plan line 715).
18
+ */
19
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
20
+ import { mkdir, writeFile, rename } from 'fs/promises';
21
+ // ============================================================================
22
+ // In-memory fs mock for isolation — covers save/load roundtrip tests
23
+ // ============================================================================
24
+ // We mock 'fs/promises' so tests never touch ~/.skillsmith on the developer's machine.
25
+ // The mock implements the subset used by manifest.ts: mkdir, writeFile, rename, readFile.
26
+ //
27
+ // memfs is declared as a module-scope object (not Map) so the vi.mock factory
28
+ // closure can reference it even after vi.mock hoisting reorders the declaration.
29
+ // Tests clear memfs keys in beforeEach/afterEach for isolation.
30
+ const memfs = {};
31
+ vi.mock('fs/promises', () => ({
32
+ mkdir: vi.fn(async () => undefined),
33
+ writeFile: vi.fn(async (path, content) => {
34
+ memfs[path] = content;
35
+ }),
36
+ rename: vi.fn(async (src, dst) => {
37
+ const content = memfs[src];
38
+ if (content === undefined)
39
+ throw new Error(`ENOENT: ${src}`);
40
+ memfs[dst] = content;
41
+ delete memfs[src];
42
+ }),
43
+ readFile: vi.fn(async (path) => {
44
+ const content = memfs[path];
45
+ if (content === undefined)
46
+ throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' });
47
+ return content;
48
+ }),
49
+ }));
50
+ // Import AFTER mocks are registered
51
+ import { loadManifest, saveManifest, generateAnonymousId, shouldRotateAnonymousId, rotateAnonymousId, sweepExpiredPreviousId, } from './manifest.js';
52
+ // ============================================================================
53
+ // Helpers
54
+ // ============================================================================
55
+ function makeManifest(telemetry) {
56
+ return {
57
+ version: '1.0.0',
58
+ installedSkills: {},
59
+ ...(telemetry !== undefined ? { telemetry } : {}),
60
+ };
61
+ }
62
+ /** Build an ISO string N days in the past */
63
+ function daysAgo(n) {
64
+ return new Date(Date.now() - n * 86_400_000).toISOString();
65
+ }
66
+ /** Build an ISO string N days in the future */
67
+ function daysFromNow(n) {
68
+ return new Date(Date.now() + n * 86_400_000).toISOString();
69
+ }
70
+ // ============================================================================
71
+ // Tests
72
+ // ============================================================================
73
+ describe('telemetry block', () => {
74
+ beforeEach(() => {
75
+ for (const k of Object.keys(memfs))
76
+ delete memfs[k];
77
+ vi.clearAllMocks();
78
+ });
79
+ afterEach(() => {
80
+ for (const k of Object.keys(memfs))
81
+ delete memfs[k];
82
+ });
83
+ // --------------------------------------------------------------------------
84
+ // Default: fresh config has no telemetry block
85
+ // --------------------------------------------------------------------------
86
+ describe('default — fresh config', () => {
87
+ it('loadManifest returns a manifest with no telemetry key when file is absent', async () => {
88
+ // memfs is empty → readFile throws ENOENT → loadManifest returns default
89
+ const m = await loadManifest();
90
+ expect(m.telemetry).toBeUndefined();
91
+ });
92
+ it('a manifest without telemetry block is treated as enabled: false by helpers', () => {
93
+ const m = makeManifest();
94
+ expect(shouldRotateAnonymousId(m)).toBe(false);
95
+ const swept = sweepExpiredPreviousId(m);
96
+ expect(swept.enabled).toBe(false);
97
+ });
98
+ });
99
+ // --------------------------------------------------------------------------
100
+ // Roundtrip: write then read preserves telemetry block
101
+ // --------------------------------------------------------------------------
102
+ describe('roundtrip', () => {
103
+ it('saves and reloads a full telemetry block with deep equality', async () => {
104
+ const telemetry = {
105
+ enabled: true,
106
+ anonymousId: 'a'.repeat(64),
107
+ anonymousIdCreatedAt: '2026-01-01T00:00:00.000Z',
108
+ scope: 'personal',
109
+ };
110
+ const m = makeManifest(telemetry);
111
+ await saveManifest(m);
112
+ const loaded = await loadManifest();
113
+ expect(loaded.telemetry).toEqual(telemetry);
114
+ });
115
+ it('older configs without a telemetry key continue to load without error', async () => {
116
+ // Write a manifest that has no telemetry key (legacy shape)
117
+ const legacy = { version: '1.0.0', installedSkills: {} };
118
+ const { MANIFEST_PATH } = await import('./manifest.js');
119
+ memfs[MANIFEST_PATH] = JSON.stringify(legacy, null, 2);
120
+ const m = await loadManifest();
121
+ expect(m.telemetry).toBeUndefined();
122
+ expect(m.version).toBe('1.0.0');
123
+ });
124
+ });
125
+ // --------------------------------------------------------------------------
126
+ // generateAnonymousId
127
+ // --------------------------------------------------------------------------
128
+ describe('generateAnonymousId', () => {
129
+ it('returns a 64-character lowercase hex string (SHA-256 length)', () => {
130
+ const id = generateAnonymousId();
131
+ expect(id).toMatch(/^[0-9a-f]{64}$/);
132
+ });
133
+ it('returns distinct values on successive calls', () => {
134
+ const ids = new Set(Array.from({ length: 5 }, () => generateAnonymousId()));
135
+ expect(ids.size).toBe(5);
136
+ });
137
+ it('never returns the raw UUID (only the hash is exposed)', () => {
138
+ // A UUID has 36 chars including hyphens; our output must be 64 hex chars
139
+ const id = generateAnonymousId();
140
+ expect(id).not.toContain('-');
141
+ expect(id.length).toBe(64);
142
+ });
143
+ });
144
+ // --------------------------------------------------------------------------
145
+ // shouldRotateAnonymousId
146
+ // --------------------------------------------------------------------------
147
+ describe('shouldRotateAnonymousId', () => {
148
+ it('returns true when anonymousIdCreatedAt is 366 days ago', () => {
149
+ const m = makeManifest({ enabled: true, anonymousIdCreatedAt: daysAgo(366) });
150
+ expect(shouldRotateAnonymousId(m)).toBe(true);
151
+ });
152
+ it('returns false when anonymousIdCreatedAt is 364 days ago', () => {
153
+ const m = makeManifest({ enabled: true, anonymousIdCreatedAt: daysAgo(364) });
154
+ expect(shouldRotateAnonymousId(m)).toBe(false);
155
+ });
156
+ it('returns false when anonymousIdCreatedAt is exactly today', () => {
157
+ const m = makeManifest({ enabled: true, anonymousIdCreatedAt: new Date().toISOString() });
158
+ expect(shouldRotateAnonymousId(m)).toBe(false);
159
+ });
160
+ it('returns false when anonymousIdCreatedAt is undefined (id never generated)', () => {
161
+ const m = makeManifest({ enabled: false });
162
+ expect(shouldRotateAnonymousId(m)).toBe(false);
163
+ });
164
+ it('returns false when telemetry block is absent', () => {
165
+ const m = makeManifest();
166
+ expect(shouldRotateAnonymousId(m)).toBe(false);
167
+ });
168
+ });
169
+ // --------------------------------------------------------------------------
170
+ // rotateAnonymousId
171
+ // --------------------------------------------------------------------------
172
+ describe('rotateAnonymousId', () => {
173
+ it('generates a new id distinct from the previous one', () => {
174
+ const oldId = generateAnonymousId();
175
+ const m = makeManifest({
176
+ enabled: true,
177
+ anonymousId: oldId,
178
+ anonymousIdCreatedAt: daysAgo(400),
179
+ });
180
+ const rotated = rotateAnonymousId(m);
181
+ expect(rotated.anonymousId).toBeDefined();
182
+ expect(rotated.anonymousId).not.toBe(oldId);
183
+ expect(rotated.anonymousId).toMatch(/^[0-9a-f]{64}$/);
184
+ });
185
+ it('moves current id to previousAnonymousId', () => {
186
+ const oldId = generateAnonymousId();
187
+ const m = makeManifest({
188
+ enabled: true,
189
+ anonymousId: oldId,
190
+ anonymousIdCreatedAt: daysAgo(400),
191
+ });
192
+ const rotated = rotateAnonymousId(m);
193
+ expect(rotated.previousAnonymousId).toBe(oldId);
194
+ });
195
+ it('sets anonymousIdCreatedAt to approximately now', () => {
196
+ const before = Date.now();
197
+ const m = makeManifest({
198
+ enabled: true,
199
+ anonymousId: 'a'.repeat(64),
200
+ anonymousIdCreatedAt: daysAgo(400),
201
+ });
202
+ const rotated = rotateAnonymousId(m);
203
+ const createdMs = new Date(rotated.anonymousIdCreatedAt).getTime();
204
+ expect(createdMs).toBeGreaterThanOrEqual(before);
205
+ expect(createdMs).toBeLessThanOrEqual(Date.now());
206
+ });
207
+ it('sets previousAnonymousIdRetiredAt to approximately 7 days from now', () => {
208
+ const before = Date.now();
209
+ const m = makeManifest({
210
+ enabled: true,
211
+ anonymousId: 'a'.repeat(64),
212
+ anonymousIdCreatedAt: daysAgo(400),
213
+ });
214
+ const rotated = rotateAnonymousId(m);
215
+ const retiredMs = new Date(rotated.previousAnonymousIdRetiredAt).getTime();
216
+ const sevenDaysMs = 7 * 86_400_000;
217
+ // Allow 1-second tolerance for test execution time
218
+ expect(retiredMs).toBeGreaterThanOrEqual(before + sevenDaysMs - 1000);
219
+ expect(retiredMs).toBeLessThanOrEqual(Date.now() + sevenDaysMs + 1000);
220
+ });
221
+ it('preserves existing telemetry fields (enabled, scope) across rotation', () => {
222
+ const m = makeManifest({
223
+ enabled: true,
224
+ anonymousId: 'b'.repeat(64),
225
+ anonymousIdCreatedAt: daysAgo(400),
226
+ scope: 'team',
227
+ teamId: 'team-123',
228
+ });
229
+ const rotated = rotateAnonymousId(m);
230
+ expect(rotated.enabled).toBe(true);
231
+ expect(rotated.scope).toBe('team');
232
+ expect(rotated.teamId).toBe('team-123');
233
+ });
234
+ it('handles telemetry block being absent (acts like enabled: false with no prior id)', () => {
235
+ const m = makeManifest(); // no telemetry block
236
+ const rotated = rotateAnonymousId(m);
237
+ expect(rotated.anonymousId).toMatch(/^[0-9a-f]{64}$/);
238
+ // previousAnonymousId should be undefined since there was no prior id
239
+ expect(rotated.previousAnonymousId).toBeUndefined();
240
+ });
241
+ });
242
+ // --------------------------------------------------------------------------
243
+ // sweepExpiredPreviousId
244
+ // --------------------------------------------------------------------------
245
+ describe('sweepExpiredPreviousId', () => {
246
+ it('clears previousAnonymousId and previousAnonymousIdRetiredAt when retiredAt is past', () => {
247
+ const m = makeManifest({
248
+ enabled: true,
249
+ anonymousId: 'a'.repeat(64),
250
+ anonymousIdCreatedAt: daysAgo(10),
251
+ previousAnonymousId: 'b'.repeat(64),
252
+ previousAnonymousIdRetiredAt: daysAgo(1), // already past
253
+ });
254
+ const swept = sweepExpiredPreviousId(m);
255
+ expect(swept.previousAnonymousId).toBeUndefined();
256
+ expect(swept.previousAnonymousIdRetiredAt).toBeUndefined();
257
+ });
258
+ it('preserves previousAnonymousId when the overlap window has not elapsed', () => {
259
+ const prevId = 'c'.repeat(64);
260
+ const m = makeManifest({
261
+ enabled: true,
262
+ anonymousId: 'a'.repeat(64),
263
+ anonymousIdCreatedAt: daysAgo(2),
264
+ previousAnonymousId: prevId,
265
+ previousAnonymousIdRetiredAt: daysFromNow(5), // still in window
266
+ });
267
+ const swept = sweepExpiredPreviousId(m);
268
+ expect(swept.previousAnonymousId).toBe(prevId);
269
+ expect(swept.previousAnonymousIdRetiredAt).toBeDefined();
270
+ });
271
+ it('is a no-op when there is no previousAnonymousId', () => {
272
+ const m = makeManifest({ enabled: true, anonymousId: 'a'.repeat(64) });
273
+ const swept = sweepExpiredPreviousId(m);
274
+ expect(swept).toEqual({ enabled: true, anonymousId: 'a'.repeat(64) });
275
+ });
276
+ it('is a no-op when the telemetry block is absent', () => {
277
+ const m = makeManifest();
278
+ const swept = sweepExpiredPreviousId(m);
279
+ expect(swept.enabled).toBe(false);
280
+ expect(swept.previousAnonymousId).toBeUndefined();
281
+ });
282
+ it('preserves all other telemetry fields when sweeping', () => {
283
+ const m = makeManifest({
284
+ enabled: true,
285
+ anonymousId: 'a'.repeat(64),
286
+ anonymousIdCreatedAt: daysAgo(10),
287
+ scope: 'personal',
288
+ endpoint: 'https://staging.example.com',
289
+ previousAnonymousId: 'b'.repeat(64),
290
+ previousAnonymousIdRetiredAt: daysAgo(1),
291
+ });
292
+ const swept = sweepExpiredPreviousId(m);
293
+ expect(swept.scope).toBe('personal');
294
+ expect(swept.endpoint).toBe('https://staging.example.com');
295
+ expect(swept.anonymousId).toBe('a'.repeat(64));
296
+ });
297
+ });
298
+ // --------------------------------------------------------------------------
299
+ // Concurrent-writer (sequential atomic correctness)
300
+ //
301
+ // Exercises the temp-file-rename code path: two sequential saves to the
302
+ // same MANIFEST_PATH must result in the second write fully replacing the
303
+ // first without partial state visible to readers.
304
+ //
305
+ // True OS-level concurrent write contention is a v2 concern (plan line 715).
306
+ // --------------------------------------------------------------------------
307
+ describe('atomic write — sequential correctness', () => {
308
+ it('second save fully replaces first; reader never sees partial state', async () => {
309
+ const first = makeManifest({ enabled: false });
310
+ const second = makeManifest({
311
+ enabled: true,
312
+ anonymousId: 'd'.repeat(64),
313
+ anonymousIdCreatedAt: new Date().toISOString(),
314
+ scope: 'personal',
315
+ });
316
+ await saveManifest(first);
317
+ await saveManifest(second);
318
+ const loaded = await loadManifest();
319
+ // Reader must see the second write in its entirety
320
+ expect(loaded.telemetry?.enabled).toBe(true);
321
+ expect(loaded.telemetry?.anonymousId).toBe('d'.repeat(64));
322
+ expect(loaded.telemetry?.scope).toBe('personal');
323
+ });
324
+ it('temp file is removed after rename (no stale .tmp artifacts)', async () => {
325
+ const { MANIFEST_PATH } = await import('./manifest.js');
326
+ const tmpPath = `${MANIFEST_PATH}.tmp.${process.pid}`;
327
+ await saveManifest(makeManifest({ enabled: false }));
328
+ // The tmp file should have been renamed away — not present in memfs
329
+ expect(memfs[tmpPath]).toBeUndefined();
330
+ // The final path should be present
331
+ expect(memfs[MANIFEST_PATH]).toBeDefined();
332
+ });
333
+ it('the write path exercises mkdir → writeFile(tmp) → rename(tmp → final)', async () => {
334
+ const { MANIFEST_PATH } = await import('./manifest.js');
335
+ await saveManifest(makeManifest({ enabled: false }));
336
+ // Verify the mock call sequence: mkdir was called, then writeFile with tmp path,
337
+ // then rename from tmp to final
338
+ expect(mkdir).toHaveBeenCalled();
339
+ expect(writeFile).toHaveBeenCalledWith(expect.stringContaining('.tmp.'), expect.any(String));
340
+ expect(rename).toHaveBeenCalledWith(expect.stringContaining('.tmp.'), MANIFEST_PATH);
341
+ });
342
+ });
343
+ });
344
+ //# sourceMappingURL=manifest.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.test.js","sourceRoot":"","sources":["../../../src/utils/manifest.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"open-database.d.ts","sourceRoot":"","sources":["../../../src/utils/open-database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAyC,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE3F;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAIzE"}
1
+ {"version":3,"file":"open-database.d.ts","sourceRoot":"","sources":["../../../src/utils/open-database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAA;AAGzB;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAgCzE"}
@@ -14,7 +14,8 @@
14
14
  * `openCliDatabase` makes the footgun structurally impossible: it is the one
15
15
  * unmissable way a CLI command opens a database.
16
16
  */
17
- import { createDatabaseAsync, initializeSchema } from '@skillsmith/core';
17
+ import { createDatabaseAsync, initializeSchema, isCorruptionError, backupCorruptDbFile, } from '@skillsmith/core';
18
+ import { existsSync } from 'node:fs';
18
19
  /**
19
20
  * Open a CLI database with the full schema initialized and all migrations
20
21
  * applied. Use this everywhere a CLI command needs a database — never call the
@@ -24,8 +25,36 @@ import { createDatabaseAsync, initializeSchema } from '@skillsmith/core';
24
25
  * @returns A connected, schema-initialized database. Caller owns `db.close()`.
25
26
  */
26
27
  export async function openCliDatabase(path) {
27
- const db = await createDatabaseAsync(path);
28
- initializeSchema(db);
29
- return db;
28
+ let db;
29
+ try {
30
+ db = await createDatabaseAsync(path);
31
+ initializeSchema(db);
32
+ return db;
33
+ }
34
+ catch (err) {
35
+ // SMI-4484: backstop for corruption that surfaces during schema init (e.g.
36
+ // a corrupt page only read once the schema queries run). The sql.js driver
37
+ // self-heals on open, but a corrupt file opened by the native driver can
38
+ // still fail here. Back up the bad file and retry once on a fresh DB.
39
+ if (!isCorruptionError(err) || path === ':memory:' || !existsSync(path)) {
40
+ throw err;
41
+ }
42
+ // Close the handle opened before initializeSchema failed so it is not
43
+ // leaked before we rebuild.
44
+ if (db) {
45
+ try {
46
+ db.close();
47
+ }
48
+ catch {
49
+ // handle already unusable — nothing more to free
50
+ }
51
+ }
52
+ const backupPath = backupCorruptDbFile(path);
53
+ console.warn(`[Skillsmith] The local database at ${path} was corrupt and could not be opened. ` +
54
+ `It has been backed up to ${backupPath} and will be rebuilt on the next sync.`);
55
+ const fresh = await createDatabaseAsync(path);
56
+ initializeSchema(fresh);
57
+ return fresh;
58
+ }
30
59
  }
31
60
  //# sourceMappingURL=open-database.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"open-database.js","sourceRoot":"","sources":["../../../src/utils/open-database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAqB,MAAM,kBAAkB,CAAA;AAE3F;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY;IAChD,MAAM,EAAE,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,CAAA;IAC1C,gBAAgB,CAAC,EAAE,CAAC,CAAA;IACpB,OAAO,EAAE,CAAA;AACX,CAAC"}
1
+ {"version":3,"file":"open-database.js","sourceRoot":"","sources":["../../../src/utils/open-database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,GAEpB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEpC;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY;IAChD,IAAI,EAA4B,CAAA;IAChC,IAAI,CAAC;QACH,EAAE,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,CAAA;QACpC,gBAAgB,CAAC,EAAE,CAAC,CAAA;QACpB,OAAO,EAAE,CAAA;IACX,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,2EAA2E;QAC3E,2EAA2E;QAC3E,yEAAyE;QACzE,sEAAsE;QACtE,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,IAAI,KAAK,UAAU,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACxE,MAAM,GAAG,CAAA;QACX,CAAC;QACD,sEAAsE;QACtE,4BAA4B;QAC5B,IAAI,EAAE,EAAE,CAAC;YACP,IAAI,CAAC;gBACH,EAAE,CAAC,KAAK,EAAE,CAAA;YACZ,CAAC;YAAC,MAAM,CAAC;gBACP,iDAAiD;YACnD,CAAC;QACH,CAAC;QACD,MAAM,UAAU,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAA;QAC5C,OAAO,CAAC,IAAI,CACV,sCAAsC,IAAI,wCAAwC;YAChF,4BAA4B,UAAU,wCAAwC,CACjF,CAAA;QACD,MAAM,KAAK,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,CAAA;QAC7C,gBAAgB,CAAC,KAAK,CAAC,CAAA;QACvB,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * SMI-5039: Stdout-pollution test for `@skillsmith/cli` — pins the same
3
+ * "probe never writes to stdout" invariant exercised by the mcp-server and
4
+ * doc-retrieval-mcp packages.
5
+ *
6
+ * CLI's `search` command emits structured table output to stdout for
7
+ * downstream piping. A regression that lets the embedding probe leak to
8
+ * stdout would garble that pipe — we catch it here before it lands in CI.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=embedding-probe-stdio.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding-probe-stdio.test.d.ts","sourceRoot":"","sources":["../../tests/embedding-probe-stdio.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * SMI-5039: Stdout-pollution test for `@skillsmith/cli` — pins the same
3
+ * "probe never writes to stdout" invariant exercised by the mcp-server and
4
+ * doc-retrieval-mcp packages.
5
+ *
6
+ * CLI's `search` command emits structured table output to stdout for
7
+ * downstream piping. A regression that lets the embedding probe leak to
8
+ * stdout would garble that pipe — we catch it here before it lands in CI.
9
+ */
10
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
11
+ import { EmbeddingService } from '@skillsmith/core/embeddings';
12
+ import { probeEmbeddingCapability } from '@skillsmith/core/embeddings/probe';
13
+ describe('SMI-5039 cli embedding probe — stdout-clean invariant', () => {
14
+ let stdoutWrite;
15
+ let checkSpy;
16
+ let getErrSpy;
17
+ beforeEach(() => {
18
+ stdoutWrite = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
19
+ checkSpy = vi.spyOn(EmbeddingService, 'checkAvailability');
20
+ getErrSpy = vi.spyOn(EmbeddingService, 'getTransformersLoadError');
21
+ delete process.env['SKILLSMITH_QUIET'];
22
+ });
23
+ afterEach(() => {
24
+ vi.restoreAllMocks();
25
+ delete process.env['SKILLSMITH_QUIET'];
26
+ });
27
+ it('never writes to stdout on mock fallback (CLI pipe-safety invariant)', async () => {
28
+ checkSpy.mockResolvedValue(false);
29
+ getErrSpy.mockReturnValue(new Error('transformers absent'));
30
+ const stderrChunks = [];
31
+ await probeEmbeddingCapability({ logger: (m) => stderrChunks.push(m) });
32
+ expect(stderrChunks).toHaveLength(1);
33
+ expect(stderrChunks[0]).toContain('[skillsmith] embeddings: mock');
34
+ expect(stdoutWrite).not.toHaveBeenCalled();
35
+ });
36
+ it('SKILLSMITH_QUIET=true suppresses the warning AND keeps stdout clean', async () => {
37
+ process.env['SKILLSMITH_QUIET'] = 'true';
38
+ checkSpy.mockResolvedValue(false);
39
+ getErrSpy.mockReturnValue(new Error('transformers absent'));
40
+ const stderrChunks = [];
41
+ await probeEmbeddingCapability({ logger: (m) => stderrChunks.push(m) });
42
+ expect(stderrChunks).toEqual([]);
43
+ expect(stdoutWrite).not.toHaveBeenCalled();
44
+ });
45
+ it('default logger (console.error) does not touch stdout', async () => {
46
+ checkSpy.mockResolvedValue(false);
47
+ getErrSpy.mockReturnValue(new Error('default-logger path'));
48
+ await probeEmbeddingCapability();
49
+ expect(stdoutWrite).not.toHaveBeenCalled();
50
+ });
51
+ it('never throws — probe failure must not abort the CLI command', async () => {
52
+ checkSpy.mockRejectedValue(new Error('catastrophic'));
53
+ getErrSpy.mockReturnValue(null);
54
+ await expect(probeEmbeddingCapability()).resolves.toBeUndefined();
55
+ expect(stdoutWrite).not.toHaveBeenCalled();
56
+ });
57
+ });
58
+ //# sourceMappingURL=embedding-probe-stdio.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding-probe-stdio.test.js","sourceRoot":"","sources":["../../tests/embedding-probe-stdio.test.ts"],"names":[],"mappings":"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"}