@skillsmith/cli 0.6.2 → 0.6.4

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 (210) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/src/commands/__meta__/telemetry-coverage.test.d.ts +20 -0
  4. package/dist/src/commands/__meta__/telemetry-coverage.test.d.ts.map +1 -0
  5. package/dist/src/commands/__meta__/telemetry-coverage.test.js +118 -0
  6. package/dist/src/commands/__meta__/telemetry-coverage.test.js.map +1 -0
  7. package/dist/src/commands/ab-test.d.ts +1 -0
  8. package/dist/src/commands/ab-test.d.ts.map +1 -1
  9. package/dist/src/commands/ab-test.js +17 -8
  10. package/dist/src/commands/ab-test.js.map +1 -1
  11. package/dist/src/commands/analyze.d.ts +1 -0
  12. package/dist/src/commands/analyze.d.ts.map +1 -1
  13. package/dist/src/commands/analyze.js +32 -23
  14. package/dist/src/commands/analyze.js.map +1 -1
  15. package/dist/src/commands/audit-collisions.d.ts +1 -4
  16. package/dist/src/commands/audit-collisions.d.ts.map +1 -1
  17. package/dist/src/commands/audit-collisions.js +32 -23
  18. package/dist/src/commands/audit-collisions.js.map +1 -1
  19. package/dist/src/commands/audit-sources.action.d.ts +37 -0
  20. package/dist/src/commands/audit-sources.action.d.ts.map +1 -0
  21. package/dist/src/commands/audit-sources.action.js +300 -0
  22. package/dist/src/commands/audit-sources.action.js.map +1 -0
  23. package/dist/src/commands/audit-sources.d.ts +37 -0
  24. package/dist/src/commands/audit-sources.d.ts.map +1 -0
  25. package/dist/src/commands/audit-sources.js +55 -0
  26. package/dist/src/commands/audit-sources.js.map +1 -0
  27. package/dist/src/commands/audit-sources.test.d.ts +9 -0
  28. package/dist/src/commands/audit-sources.test.d.ts.map +1 -0
  29. package/dist/src/commands/audit-sources.test.js +228 -0
  30. package/dist/src/commands/audit-sources.test.js.map +1 -0
  31. package/dist/src/commands/audit.d.ts +2 -6
  32. package/dist/src/commands/audit.d.ts.map +1 -1
  33. package/dist/src/commands/audit.js +52 -31
  34. package/dist/src/commands/audit.js.map +1 -1
  35. package/dist/src/commands/author/index.d.ts +2 -1
  36. package/dist/src/commands/author/index.d.ts.map +1 -1
  37. package/dist/src/commands/author/index.js +4 -1
  38. package/dist/src/commands/author/index.js.map +1 -1
  39. package/dist/src/commands/author/init.action.d.ts +29 -0
  40. package/dist/src/commands/author/init.action.d.ts.map +1 -0
  41. package/dist/src/commands/author/init.action.js +131 -0
  42. package/dist/src/commands/author/init.action.js.map +1 -0
  43. package/dist/src/commands/author/init.d.ts +0 -14
  44. package/dist/src/commands/author/init.d.ts.map +1 -1
  45. package/dist/src/commands/author/init.js +3 -87
  46. package/dist/src/commands/author/init.js.map +1 -1
  47. package/dist/src/commands/author/mcp-init.d.ts +1 -0
  48. package/dist/src/commands/author/mcp-init.d.ts.map +1 -1
  49. package/dist/src/commands/author/mcp-init.js +21 -13
  50. package/dist/src/commands/author/mcp-init.js.map +1 -1
  51. package/dist/src/commands/author/subagent.d.ts +1 -0
  52. package/dist/src/commands/author/subagent.d.ts.map +1 -1
  53. package/dist/src/commands/author/subagent.js +23 -15
  54. package/dist/src/commands/author/subagent.js.map +1 -1
  55. package/dist/src/commands/author/transform.d.ts +1 -0
  56. package/dist/src/commands/author/transform.d.ts.map +1 -1
  57. package/dist/src/commands/author/transform.js +23 -15
  58. package/dist/src/commands/author/transform.js.map +1 -1
  59. package/dist/src/commands/config.d.ts +2 -0
  60. package/dist/src/commands/config.d.ts.map +1 -1
  61. package/dist/src/commands/config.js +43 -28
  62. package/dist/src/commands/config.js.map +1 -1
  63. package/dist/src/commands/create.d.ts +1 -0
  64. package/dist/src/commands/create.d.ts.map +1 -1
  65. package/dist/src/commands/create.js +28 -19
  66. package/dist/src/commands/create.js.map +1 -1
  67. package/dist/src/commands/diff.d.ts +4 -0
  68. package/dist/src/commands/diff.d.ts.map +1 -1
  69. package/dist/src/commands/diff.js +57 -42
  70. package/dist/src/commands/diff.js.map +1 -1
  71. package/dist/src/commands/diff.test.js +30 -0
  72. package/dist/src/commands/diff.test.js.map +1 -1
  73. package/dist/src/commands/import-local.d.ts +7 -0
  74. package/dist/src/commands/import-local.d.ts.map +1 -1
  75. package/dist/src/commands/import-local.js +43 -34
  76. package/dist/src/commands/import-local.js.map +1 -1
  77. package/dist/src/commands/import.d.ts +6 -0
  78. package/dist/src/commands/import.d.ts.map +1 -1
  79. package/dist/src/commands/import.js +23 -14
  80. package/dist/src/commands/import.js.map +1 -1
  81. package/dist/src/commands/index.d.ts +1 -0
  82. package/dist/src/commands/index.d.ts.map +1 -1
  83. package/dist/src/commands/index.js +2 -0
  84. package/dist/src/commands/index.js.map +1 -1
  85. package/dist/src/commands/info.d.ts +7 -0
  86. package/dist/src/commands/info.d.ts.map +1 -1
  87. package/dist/src/commands/info.js +77 -68
  88. package/dist/src/commands/info.js.map +1 -1
  89. package/dist/src/commands/install-skill.d.ts +3 -0
  90. package/dist/src/commands/install-skill.d.ts.map +1 -1
  91. package/dist/src/commands/install-skill.js +23 -15
  92. package/dist/src/commands/install-skill.js.map +1 -1
  93. package/dist/src/commands/install.d.ts +21 -0
  94. package/dist/src/commands/install.d.ts.map +1 -1
  95. package/dist/src/commands/install.js +154 -139
  96. package/dist/src/commands/install.js.map +1 -1
  97. package/dist/src/commands/login.d.ts +4 -0
  98. package/dist/src/commands/login.d.ts.map +1 -1
  99. package/dist/src/commands/login.js +56 -47
  100. package/dist/src/commands/login.js.map +1 -1
  101. package/dist/src/commands/logout.d.ts +1 -0
  102. package/dist/src/commands/logout.d.ts.map +1 -1
  103. package/dist/src/commands/logout.js +36 -27
  104. package/dist/src/commands/logout.js.map +1 -1
  105. package/dist/src/commands/manage.d.ts +3 -9
  106. package/dist/src/commands/manage.d.ts.map +1 -1
  107. package/dist/src/commands/manage.js +69 -45
  108. package/dist/src/commands/manage.js.map +1 -1
  109. package/dist/src/commands/merge.d.ts +7 -0
  110. package/dist/src/commands/merge.d.ts.map +1 -1
  111. package/dist/src/commands/merge.js +117 -108
  112. package/dist/src/commands/merge.js.map +1 -1
  113. package/dist/src/commands/pin.d.ts +2 -0
  114. package/dist/src/commands/pin.d.ts.map +1 -1
  115. package/dist/src/commands/pin.js +88 -73
  116. package/dist/src/commands/pin.js.map +1 -1
  117. package/dist/src/commands/recommend.d.ts +1 -0
  118. package/dist/src/commands/recommend.d.ts.map +1 -1
  119. package/dist/src/commands/recommend.js +35 -27
  120. package/dist/src/commands/recommend.js.map +1 -1
  121. package/dist/src/commands/search-formatters.d.ts +1 -0
  122. package/dist/src/commands/search-formatters.d.ts.map +1 -1
  123. package/dist/src/commands/search-formatters.js +3 -0
  124. package/dist/src/commands/search-formatters.js.map +1 -1
  125. package/dist/src/commands/search.action.d.ts +16 -0
  126. package/dist/src/commands/search.action.d.ts.map +1 -0
  127. package/dist/src/commands/search.action.js +384 -0
  128. package/dist/src/commands/search.action.js.map +1 -0
  129. package/dist/src/commands/search.d.ts +3 -0
  130. package/dist/src/commands/search.d.ts.map +1 -1
  131. package/dist/src/commands/search.js +5 -343
  132. package/dist/src/commands/search.js.map +1 -1
  133. package/dist/src/commands/sync.action.d.ts +39 -0
  134. package/dist/src/commands/sync.action.d.ts.map +1 -0
  135. package/dist/src/commands/sync.action.js +353 -0
  136. package/dist/src/commands/sync.action.js.map +1 -0
  137. package/dist/src/commands/sync.d.ts +3 -0
  138. package/dist/src/commands/sync.d.ts.map +1 -1
  139. package/dist/src/commands/sync.js +8 -310
  140. package/dist/src/commands/sync.js.map +1 -1
  141. package/dist/src/commands/telemetry.action.d.ts +38 -0
  142. package/dist/src/commands/telemetry.action.d.ts.map +1 -0
  143. package/dist/src/commands/telemetry.action.js +350 -0
  144. package/dist/src/commands/telemetry.action.js.map +1 -0
  145. package/dist/src/commands/telemetry.d.ts +30 -0
  146. package/dist/src/commands/telemetry.d.ts.map +1 -0
  147. package/dist/src/commands/telemetry.helpers.d.ts +58 -0
  148. package/dist/src/commands/telemetry.helpers.d.ts.map +1 -0
  149. package/dist/src/commands/telemetry.helpers.js +173 -0
  150. package/dist/src/commands/telemetry.helpers.js.map +1 -0
  151. package/dist/src/commands/telemetry.helpers.test.d.ts +15 -0
  152. package/dist/src/commands/telemetry.helpers.test.d.ts.map +1 -0
  153. package/dist/src/commands/telemetry.helpers.test.js +240 -0
  154. package/dist/src/commands/telemetry.helpers.test.js.map +1 -0
  155. package/dist/src/commands/telemetry.js +65 -0
  156. package/dist/src/commands/telemetry.js.map +1 -0
  157. package/dist/src/commands/telemetry.test.d.ts +18 -0
  158. package/dist/src/commands/telemetry.test.d.ts.map +1 -0
  159. package/dist/src/commands/telemetry.test.js +401 -0
  160. package/dist/src/commands/telemetry.test.js.map +1 -0
  161. package/dist/src/commands/whoami.d.ts +1 -0
  162. package/dist/src/commands/whoami.d.ts.map +1 -1
  163. package/dist/src/commands/whoami.js +29 -21
  164. package/dist/src/commands/whoami.js.map +1 -1
  165. package/dist/src/index.js +3 -1
  166. package/dist/src/index.js.map +1 -1
  167. package/dist/src/utils/manifest.d.ts +75 -0
  168. package/dist/src/utils/manifest.d.ts.map +1 -1
  169. package/dist/src/utils/manifest.js +78 -0
  170. package/dist/src/utils/manifest.js.map +1 -1
  171. package/dist/src/utils/manifest.test.d.ts +20 -0
  172. package/dist/src/utils/manifest.test.d.ts.map +1 -0
  173. package/dist/src/utils/manifest.test.js +344 -0
  174. package/dist/src/utils/manifest.test.js.map +1 -0
  175. package/dist/src/utils/open-database.d.ts +10 -2
  176. package/dist/src/utils/open-database.d.ts.map +1 -1
  177. package/dist/src/utils/open-database.js +15 -2
  178. package/dist/src/utils/open-database.js.map +1 -1
  179. package/dist/src/utils/skills-directory.d.ts.map +1 -1
  180. package/dist/src/utils/skills-directory.js +5 -1
  181. package/dist/src/utils/skills-directory.js.map +1 -1
  182. package/dist/tests/e2e/audit-sources-apply.test.d.ts +13 -0
  183. package/dist/tests/e2e/audit-sources-apply.test.d.ts.map +1 -0
  184. package/dist/tests/e2e/audit-sources-apply.test.js +166 -0
  185. package/dist/tests/e2e/audit-sources-apply.test.js.map +1 -0
  186. package/dist/tests/e2e/audit-sources-recovery.test.d.ts +15 -0
  187. package/dist/tests/e2e/audit-sources-recovery.test.d.ts.map +1 -0
  188. package/dist/tests/e2e/audit-sources-recovery.test.js +179 -0
  189. package/dist/tests/e2e/audit-sources-recovery.test.js.map +1 -0
  190. package/dist/tests/e2e/audit-sources-roundtrip.test.d.ts +23 -0
  191. package/dist/tests/e2e/audit-sources-roundtrip.test.d.ts.map +1 -0
  192. package/dist/tests/e2e/audit-sources-roundtrip.test.js +107 -0
  193. package/dist/tests/e2e/audit-sources-roundtrip.test.js.map +1 -0
  194. package/dist/tests/e2e/utils/source-recovery-fixture.d.ts +49 -0
  195. package/dist/tests/e2e/utils/source-recovery-fixture.d.ts.map +1 -0
  196. package/dist/tests/e2e/utils/source-recovery-fixture.js +106 -0
  197. package/dist/tests/e2e/utils/source-recovery-fixture.js.map +1 -0
  198. package/dist/tests/embedding-probe-stdio.test.d.ts +11 -0
  199. package/dist/tests/embedding-probe-stdio.test.d.ts.map +1 -0
  200. package/dist/tests/embedding-probe-stdio.test.js +58 -0
  201. package/dist/tests/embedding-probe-stdio.test.js.map +1 -0
  202. package/dist/tests/init.test.js +2 -1
  203. package/dist/tests/init.test.js.map +1 -1
  204. package/dist/tests/install-quarantine.test.d.ts +10 -0
  205. package/dist/tests/install-quarantine.test.d.ts.map +1 -0
  206. package/dist/tests/install-quarantine.test.js +86 -0
  207. package/dist/tests/install-quarantine.test.js.map +1 -0
  208. package/package.json +5 -5
  209. package/templates/skill-telemetry.cmd +16 -0
  210. 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,6 +11,14 @@
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
  */
14
23
  export interface SkillManifestEntry {
15
24
  id: string;
@@ -28,6 +37,41 @@ export interface SkillManifestEntry {
28
37
  export interface SkillManifest {
29
38
  version: string;
30
39
  installedSkills: Record<string, SkillManifestEntry>;
40
+ /** SMI-5012 W3: telemetry opt-in block. Absent on older configs; treated as { enabled: false }. */
41
+ telemetry?: TelemetryManifest;
42
+ }
43
+ /**
44
+ * Telemetry configuration stored in ~/.skillsmith/manifest.json.
45
+ *
46
+ * Schema is additive: older configs without this block continue to load
47
+ * (missing block is treated as { enabled: false } everywhere it is read).
48
+ *
49
+ * anonymous_id rotation policy (U6 / M7):
50
+ * - Generated via SHA-256(crypto.randomUUID()) on first opt-in
51
+ * - Auto-rotates after 365 days (checked on `skillsmith telemetry status`)
52
+ * - One-week overlap window: previous id is retained in previousAnonymousId
53
+ * until previousAnonymousIdRetiredAt so cross-rotation events can be joined
54
+ * - Manual rotation: `skillsmith telemetry reset-id` (SMI-5021)
55
+ */
56
+ export interface TelemetryManifest {
57
+ /** Whether telemetry is enabled. Always opt-in; default false. */
58
+ enabled: boolean;
59
+ /** SHA-256 hex of a random UUID; 64 hex chars. Present only when enabled. */
60
+ anonymousId?: string;
61
+ /** ISO-8601 date when anonymousId was generated. Enables annual rotation check. */
62
+ anonymousIdCreatedAt?: string;
63
+ /** Previous anonymousId retained for one-week overlap window after rotation. */
64
+ previousAnonymousId?: string;
65
+ /** ISO-8601 date after which previousAnonymousId can be swept. */
66
+ previousAnonymousIdRetiredAt?: string;
67
+ /** Telemetry scope. Default 'personal'. */
68
+ scope?: 'personal' | 'team';
69
+ /** Present when scope === 'team'. */
70
+ teamId?: string;
71
+ /** Override for staging. Default: prod events endpoint. */
72
+ endpoint?: string;
73
+ /** ISO-8601 date when the Claude Code hook was last installed. */
74
+ installedAt?: string;
31
75
  }
32
76
  export declare const MANIFEST_PATH: string;
33
77
  /**
@@ -43,4 +87,35 @@ export declare function saveManifest(manifest: SkillManifest): Promise<void>;
43
87
  * Load the manifest, apply an update function, and save atomically.
44
88
  */
45
89
  export declare function updateManifestEntry(updateFn: (manifest: SkillManifest) => SkillManifest): Promise<void>;
90
+ /**
91
+ * Generate a new anonymous telemetry id.
92
+ *
93
+ * Returns SHA-256(crypto.randomUUID()) as a 64-character lowercase hex string.
94
+ * The UUID is not stored; only the hash reaches the wire (plan line 719).
95
+ */
96
+ export declare function generateAnonymousId(): string;
97
+ /**
98
+ * Returns true if the manifest's anonymousId should be rotated.
99
+ *
100
+ * Rotation is triggered when anonymousIdCreatedAt is older than 365 days.
101
+ * Returns false if the field is absent (id was never generated).
102
+ */
103
+ export declare function shouldRotateAnonymousId(manifest: SkillManifest): boolean;
104
+ /**
105
+ * Rotate the anonymous id.
106
+ *
107
+ * Moves the current id to previousAnonymousId with a 7-day retirement window,
108
+ * generates a fresh id, and updates anonymousIdCreatedAt to now.
109
+ * Returns a new TelemetryManifest — does NOT write to disk.
110
+ * Callers must persist via saveManifest / updateManifestEntry.
111
+ */
112
+ export declare function rotateAnonymousId(manifest: SkillManifest): TelemetryManifest;
113
+ /**
114
+ * Remove previousAnonymousId once its retirement window has passed.
115
+ *
116
+ * Should be called on every `skillsmith telemetry status` run and before
117
+ * each hook-script read. No-op if the window has not yet elapsed or if
118
+ * there is no previous id. Returns a new TelemetryManifest.
119
+ */
120
+ export declare function sweepExpiredPreviousId(manifest: SkillManifest): TelemetryManifest;
46
121
  //# sourceMappingURL=manifest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/utils/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAUH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;CACpD;AAOD,eAAO,MAAM,aAAa,QAAwC,CAAA;AAMlE;;;GAGG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,aAAa,CAAC,CAO3D;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAKzE;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,aAAa,GACnD,OAAO,CAAC,IAAI,CAAC,CAIf"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/utils/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAWH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;IACnD,mGAAmG;IACnG,SAAS,CAAC,EAAE,iBAAiB,CAAA;CAC9B;AAMD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAA;IAChB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mFAAmF;IACnF,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,gFAAgF;IAChF,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,kEAAkE;IAClE,4BAA4B,CAAC,EAAE,MAAM,CAAA;IACrC,2CAA2C;IAC3C,KAAK,CAAC,EAAE,UAAU,GAAG,MAAM,CAAA;IAC3B,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAOD,eAAO,MAAM,aAAa,QAAwC,CAAA;AAMlE;;;GAGG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,aAAa,CAAC,CAO3D;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAKzE;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,aAAa,GACnD,OAAO,CAAC,IAAI,CAAC,CAIf;AAUD;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAKxE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,aAAa,GAAG,iBAAiB,CAc5E;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,aAAa,GAAG,iBAAiB,CAOjF"}
@@ -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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"}
@@ -21,7 +21,15 @@ import { type DatabaseType } from '@skillsmith/core';
21
21
  * bare `createDatabaseAsync` directly.
22
22
  *
23
23
  * @param path - Filesystem path to the SQLite database (e.g. `DEFAULT_DB_PATH`).
24
- * @returns A connected, schema-initialized database. Caller owns `db.close()`.
24
+ * @param options.readonly - SMI-5139: open read-only for pure-read consumers
25
+ * (e.g. version lookups). A read-only handle CANNOT run schema DDL, so
26
+ * `initializeSchema` is skipped — the caller must tolerate an absent schema
27
+ * (queries on a missing table throw, which read consumers already catch).
28
+ * This also prevents the WASM `SqlJsDatabaseAdapter` from persisting (writing)
29
+ * on `close()`, which would throw `EROFS` on an unwritable/absent db path.
30
+ * @returns A connected database. Caller owns `db.close()`.
25
31
  */
26
- export declare function openCliDatabase(path: string): Promise<DatabaseType>;
32
+ export declare function openCliDatabase(path: string, options?: {
33
+ readonly?: boolean;
34
+ }): Promise<DatabaseType>;
27
35
  //# sourceMappingURL=open-database.d.ts.map
@@ -1 +1 @@
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"}
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;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAC/B,OAAO,CAAC,YAAY,CAAC,CAwCvB"}