@skillsmith/cli 0.6.2 → 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.
- package/CHANGELOG.md +13 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/commands/index.d.ts +1 -0
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/search.d.ts.map +1 -1
- package/dist/src/commands/search.js +11 -0
- package/dist/src/commands/search.js.map +1 -1
- package/dist/src/commands/telemetry.d.ts +38 -0
- package/dist/src/commands/telemetry.d.ts.map +1 -0
- package/dist/src/commands/telemetry.helpers.d.ts +58 -0
- package/dist/src/commands/telemetry.helpers.d.ts.map +1 -0
- package/dist/src/commands/telemetry.helpers.js +173 -0
- package/dist/src/commands/telemetry.helpers.js.map +1 -0
- package/dist/src/commands/telemetry.helpers.test.d.ts +15 -0
- package/dist/src/commands/telemetry.helpers.test.d.ts.map +1 -0
- package/dist/src/commands/telemetry.helpers.test.js +240 -0
- package/dist/src/commands/telemetry.helpers.test.js.map +1 -0
- package/dist/src/commands/telemetry.js +354 -0
- package/dist/src/commands/telemetry.js.map +1 -0
- package/dist/src/commands/telemetry.test.d.ts +18 -0
- package/dist/src/commands/telemetry.test.d.ts.map +1 -0
- package/dist/src/commands/telemetry.test.js +401 -0
- package/dist/src/commands/telemetry.test.js.map +1 -0
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/utils/manifest.d.ts +75 -0
- package/dist/src/utils/manifest.d.ts.map +1 -1
- package/dist/src/utils/manifest.js +78 -0
- package/dist/src/utils/manifest.js.map +1 -1
- package/dist/src/utils/manifest.test.d.ts +20 -0
- package/dist/src/utils/manifest.test.d.ts.map +1 -0
- package/dist/src/utils/manifest.test.js +344 -0
- package/dist/src/utils/manifest.test.js.map +1 -0
- package/dist/tests/embedding-probe-stdio.test.d.ts +11 -0
- package/dist/tests/embedding-probe-stdio.test.d.ts.map +1 -0
- package/dist/tests/embedding-probe-stdio.test.js +58 -0
- package/dist/tests/embedding-probe-stdio.test.js.map +1 -0
- package/package.json +4 -3
- package/templates/skill-telemetry.cmd +16 -0
- 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
|
|
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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"}
|
|
@@ -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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillsmith/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "CLI tools for Skillsmith skill discovery and authentication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@inquirer/prompts": "7.10.1",
|
|
20
|
-
"@skillsmith/core": "^0.
|
|
21
|
-
"@skillsmith/mcp-server": "^0.5.
|
|
20
|
+
"@skillsmith/core": "^0.8.0",
|
|
21
|
+
"@skillsmith/mcp-server": "^0.5.3",
|
|
22
22
|
"chalk": "5.6.2",
|
|
23
23
|
"chokidar": "4.0.3",
|
|
24
24
|
"cli-table3": "0.6.5",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"files": [
|
|
47
47
|
"dist",
|
|
48
48
|
"assets",
|
|
49
|
+
"templates",
|
|
49
50
|
"CHANGELOG.md"
|
|
50
51
|
],
|
|
51
52
|
"license": "Elastic-2.0",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM Skillsmith skill-invocation telemetry hook — Windows Git-Bash shim.
|
|
3
|
+
REM
|
|
4
|
+
REM SMI-5012 / SMI-5020 W3.S1. Claude Code on Windows requires Git Bash,
|
|
5
|
+
REM so we delegate to the POSIX-sh script via `bash`. The shim itself
|
|
6
|
+
REM does no parsing — all logic (including the privacy-critical
|
|
7
|
+
REM never-read-args invariant) lives in skill-telemetry.sh.
|
|
8
|
+
REM
|
|
9
|
+
REM Stdin is piped through unchanged. Exit code is forwarded; the .sh
|
|
10
|
+
REM always exits 0 so this shim always exits 0 too.
|
|
11
|
+
REM
|
|
12
|
+
REM Installed at: %USERPROFILE%\.skillsmith\hooks\skill-telemetry.cmd
|
|
13
|
+
REM Delegates to: %USERPROFILE%\.skillsmith\hooks\skill-telemetry.sh
|
|
14
|
+
|
|
15
|
+
bash "%USERPROFILE%\.skillsmith\hooks\skill-telemetry.sh" %*
|
|
16
|
+
exit /b 0
|