@fixy/core 0.0.3 → 0.0.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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "lastSentAt": "2026-04-12T12:41:43.519Z"
3
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "tool_name": "Bash",
3
+ "tool_input_preview": "{\"command\":\"pnpm test 2>&1\",\"timeout\":120000,\"description\":\"Run tests\"}",
4
+ "error": "Exit code 1\n[rerun: b7]",
5
+ "timestamp": "2026-04-12T12:18:27.333Z",
6
+ "retry_count": 2
7
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAS3D,qBAAa,gBAAgB;IAC3B;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ3B;;;OAGG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IA0C5D;;;OAGG;IACG,aAAa,CACjB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC;IAWtB;;;OAGG;IACG,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAa3E;;;OAGG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAwB7D;;;OAGG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAe/E,kFAAkF;YACpE,YAAY;CAK3B"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAS3D,qBAAa,gBAAgB;IAC3B;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ3B;;;OAGG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IA0C5D;;;OAGG;IACG,aAAa,CACjB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC;IAWtB;;;OAGG;IACG,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAa3E;;;OAGG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAwB7D;;;OAGG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAe/E,kFAAkF;YACpE,YAAY;CAK3B"}
package/dist/store.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // packages/core/src/store.ts
2
2
  import { mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises';
3
- import { randomUUID } from 'node:crypto';
4
3
  import { join } from 'node:path';
4
+ import { v7 as uuidv7 } from 'uuid';
5
5
  import { computeProjectId, getFixyHome, getProjectFile, getThreadFile, getThreadsDir, } from './paths.js';
6
6
  export class LocalThreadStore {
7
7
  /**
@@ -20,7 +20,7 @@ export class LocalThreadStore {
20
20
  * Writes project.json if it does not already exist.
21
21
  */
22
22
  async createThread(projectRoot) {
23
- const id = randomUUID();
23
+ const id = uuidv7();
24
24
  const projectId = computeProjectId(projectRoot);
25
25
  const now = new Date().toISOString();
26
26
  // Ensure project and threads directories exist.
package/dist/store.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fixy/core",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -11,9 +11,12 @@
11
11
  }
12
12
  },
13
13
  "devDependencies": {
14
- "@types/node": "^20.0.0"
14
+ "@types/node": "^25.6.0"
15
15
  },
16
16
  "license": "MIT",
17
+ "dependencies": {
18
+ "uuid": "^13.0.0"
19
+ },
17
20
  "scripts": {
18
21
  "build": "tsc",
19
22
  "typecheck": "tsc --noEmit"
@@ -0,0 +1,290 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { slugify } from '../slugify.js';
3
+
4
+ describe('slugify — basic ASCII', () => {
5
+ it('lowercases the input', () => {
6
+ expect(slugify('Hello World')).toBe('hello-world');
7
+ });
8
+
9
+ it('removes punctuation', () => {
10
+ expect(slugify('Hello, World!')).toBe('hello-world');
11
+ });
12
+
13
+ it('collapses multiple whitespace characters into a single hyphen', () => {
14
+ expect(slugify('foo bar')).toBe('foo-bar');
15
+ });
16
+
17
+ it('preserves digits', () => {
18
+ expect(slugify('foo 123 bar')).toBe('foo-123-bar');
19
+ });
20
+
21
+ it('collapses repeated non-alphanumeric chars into one hyphen', () => {
22
+ expect(slugify('a---b')).toBe('a-b');
23
+ });
24
+
25
+ it('trims leading hyphens', () => {
26
+ expect(slugify('---hello')).toBe('hello');
27
+ });
28
+
29
+ it('trims trailing hyphens', () => {
30
+ expect(slugify('hello---')).toBe('hello');
31
+ });
32
+
33
+ it('trims both leading and trailing hyphens', () => {
34
+ expect(slugify('---hello---')).toBe('hello');
35
+ });
36
+
37
+ it('returns empty string when only punctuation is given', () => {
38
+ expect(slugify('!!!')).toBe('');
39
+ });
40
+
41
+ it('returns empty string for an empty input', () => {
42
+ expect(slugify('')).toBe('');
43
+ });
44
+
45
+ it('removes apostrophes so contractions collapse without a separator', () => {
46
+ expect(slugify("don't stop")).toBe('dont-stop');
47
+ });
48
+
49
+ it('handles a slug that is already clean', () => {
50
+ expect(slugify('hello-world')).toBe('hello-world');
51
+ });
52
+
53
+ it('handles input that is purely digits', () => {
54
+ expect(slugify('123')).toBe('123');
55
+ });
56
+
57
+ it('output contains only lowercase letters, digits, and hyphens', () => {
58
+ const result = slugify('Hello, World! 123 -- foo@bar.baz');
59
+ expect(result).toMatch(/^[a-z0-9-]+$/);
60
+ });
61
+ });
62
+
63
+ describe('slugify — diacritics and transliteration map', () => {
64
+ it('strips diacritics via NFKD decomposition', () => {
65
+ expect(slugify('naïve café')).toBe('naive-cafe');
66
+ });
67
+
68
+ it('handles "Crème brûlée"', () => {
69
+ expect(slugify('Crème brûlée')).toBe('creme-brulee');
70
+ });
71
+
72
+ it('transliterates ß → ss ("Straße")', () => {
73
+ expect(slugify('Straße')).toBe('strasse');
74
+ });
75
+
76
+ it('transliterates Æ → ae and Œ → oe ("Æther Œuvre")', () => {
77
+ expect(slugify('Æther Œuvre')).toBe('aether-oeuvre');
78
+ });
79
+
80
+ it('transliterates æ → ae', () => {
81
+ expect(slugify('æon')).toBe('aeon');
82
+ });
83
+
84
+ it('transliterates œ → oe', () => {
85
+ expect(slugify('œuvre')).toBe('oeuvre');
86
+ });
87
+
88
+ it('transliterates ø / Ø → o', () => {
89
+ expect(slugify('ø')).toBe('o');
90
+ expect(slugify('Ø')).toBe('o');
91
+ });
92
+
93
+ it('transliterates đ / Đ → d', () => {
94
+ expect(slugify('đuro')).toBe('duro');
95
+ expect(slugify('Đuro')).toBe('duro');
96
+ });
97
+
98
+ it('transliterates ð / Ð → d', () => {
99
+ expect(slugify('ðór')).toBe('dor');
100
+ });
101
+
102
+ it('transliterates þ / Þ → th', () => {
103
+ expect(slugify('þorn')).toBe('thorn');
104
+ expect(slugify('Þorn')).toBe('thorn');
105
+ });
106
+
107
+ it('transliterates ł / Ł → l', () => {
108
+ expect(slugify('Łódź')).toBe('lodz');
109
+ });
110
+
111
+ it('returns empty string when only non-slug-safe unicode chars remain', () => {
112
+ // Characters with no transliteration and no ASCII base (e.g. CJK)
113
+ expect(slugify('日本語')).toBe('');
114
+ });
115
+
116
+ it('output is strictly ASCII for transliterated input', () => {
117
+ const result = slugify('Crème brûlée Straße Æther Œuvre');
118
+ expect(result).toMatch(/^[a-z0-9-]+$/);
119
+ });
120
+ });
121
+
122
+ describe('slugify — separators, underscores, slashes', () => {
123
+ it('converts underscores to hyphens', () => {
124
+ expect(slugify('foo_bar')).toBe('foo-bar');
125
+ });
126
+
127
+ it('converts multiple underscores to a single hyphen', () => {
128
+ expect(slugify('foo___bar')).toBe('foo-bar');
129
+ });
130
+
131
+ it('converts forward slashes to hyphens', () => {
132
+ expect(slugify('foo/bar')).toBe('foo-bar');
133
+ });
134
+
135
+ it('converts backslashes to hyphens', () => {
136
+ expect(slugify('foo\\bar')).toBe('foo-bar');
137
+ });
138
+
139
+ it('converts dots used as separators to hyphens', () => {
140
+ expect(slugify('foo.bar.baz')).toBe('foo-bar-baz');
141
+ });
142
+
143
+ it('converts pipes to hyphens', () => {
144
+ expect(slugify('foo|bar')).toBe('foo-bar');
145
+ });
146
+
147
+ it('converts colons to hyphens', () => {
148
+ expect(slugify('foo:bar')).toBe('foo-bar');
149
+ });
150
+
151
+ it('collapses mixed separators (slash + underscore + space) into one hyphen', () => {
152
+ expect(slugify('foo / _bar')).toBe('foo-bar');
153
+ });
154
+
155
+ it('preserves an already-clean hyphenated slug unchanged', () => {
156
+ expect(slugify('already-clean-slug')).toBe('already-clean-slug');
157
+ });
158
+
159
+ it('preserves hyphenated slug with numbers', () => {
160
+ expect(slugify('step-1-of-3')).toBe('step-1-of-3');
161
+ });
162
+ });
163
+
164
+ describe('slugify — apostrophes', () => {
165
+ it('removes straight apostrophe in contraction without inserting a hyphen', () => {
166
+ expect(slugify("it's")).toBe('its');
167
+ });
168
+
169
+ it('removes Unicode left single quotation mark (U+2018)', () => {
170
+ expect(slugify('\u2018hello\u2019')).toBe('hello');
171
+ });
172
+
173
+ it('removes Unicode right single quotation mark (U+2019)', () => {
174
+ expect(slugify("won\u2019t")).toBe('wont');
175
+ });
176
+
177
+ it('removes modifier letter apostrophe (U+02BC)', () => {
178
+ expect(slugify('o\u02BCclock')).toBe('oclock');
179
+ });
180
+
181
+ it('handles multiple apostrophes in a row without producing hyphens', () => {
182
+ expect(slugify("''hello''")).toBe('hello');
183
+ });
184
+ });
185
+
186
+ describe('slugify — emojis', () => {
187
+ it('strips a leading emoji', () => {
188
+ expect(slugify('🚀 launch')).toBe('launch');
189
+ });
190
+
191
+ it('strips a trailing emoji', () => {
192
+ expect(slugify('launch 🚀')).toBe('launch');
193
+ });
194
+
195
+ it('strips an emoji between words without doubling hyphens', () => {
196
+ expect(slugify('foo 🔥 bar')).toBe('foo-bar');
197
+ });
198
+
199
+ it('strips an emoji adjacent to a word without inserting a hyphen', () => {
200
+ expect(slugify('foo🔥bar')).toBe('foobar');
201
+ });
202
+
203
+ it('returns empty string when input is only emojis', () => {
204
+ expect(slugify('🚀🔥💡')).toBe('');
205
+ });
206
+ });
207
+
208
+ describe('slugify — edge cases', () => {
209
+ it('returns empty string for an empty input ""', () => {
210
+ expect(slugify('')).toBe('');
211
+ });
212
+
213
+ it('returns empty string for whitespace-only input', () => {
214
+ expect(slugify(' ')).toBe('');
215
+ });
216
+
217
+ it('returns empty string for tab and newline input', () => {
218
+ expect(slugify('\t\n\r')).toBe('');
219
+ });
220
+
221
+ it('returns empty string for all-symbol input (mixed punctuation)', () => {
222
+ expect(slugify('!@#$%^&*()')).toBe('');
223
+ });
224
+
225
+ it('returns empty string for Cyrillic input with no transliteration', () => {
226
+ expect(slugify('Привет')).toBe('');
227
+ });
228
+
229
+ it('returns empty string for Arabic input', () => {
230
+ expect(slugify('مرحبا')).toBe('');
231
+ });
232
+
233
+ it('returns empty string for Hebrew input', () => {
234
+ expect(slugify('שלום')).toBe('');
235
+ });
236
+
237
+ it('returns empty string for CJK input', () => {
238
+ expect(slugify('日本語')).toBe('');
239
+ });
240
+
241
+ it('handles a single ASCII letter', () => {
242
+ expect(slugify('a')).toBe('a');
243
+ });
244
+
245
+ it('handles a single digit', () => {
246
+ expect(slugify('9')).toBe('9');
247
+ });
248
+ });
249
+
250
+ describe('slugify — output invariants', () => {
251
+ const cases = [
252
+ 'Hello, World!',
253
+ ' leading and trailing ',
254
+ '---hyphens---',
255
+ 'a---b---c',
256
+ 'foo / bar \\ baz',
257
+ 'foo_bar_baz',
258
+ 'Crème brûlée Straße',
259
+ '🚀 rockets 🔥 fire',
260
+ 'it\u2019s a test',
261
+ '!!!',
262
+ '',
263
+ '日本語',
264
+ 'UPPER CASE INPUT',
265
+ 'mixed123Numbers',
266
+ ];
267
+
268
+ for (const input of cases) {
269
+ it(`output for "${input}" never has repeated, leading, or trailing hyphens`, () => {
270
+ const result = slugify(input);
271
+ if (result.length > 0) {
272
+ expect(result).not.toMatch(/^-/);
273
+ expect(result).not.toMatch(/-$/);
274
+ expect(result).not.toMatch(/--/);
275
+ expect(result).toMatch(/^[a-z0-9-]+$/);
276
+ } else {
277
+ expect(result).toBe('');
278
+ }
279
+ });
280
+ }
281
+
282
+ it('valid slug input is idempotent (slugify(slugify(x)) === slugify(x))', () => {
283
+ const inputs = ['Hello World', 'foo_bar', 'Crème brûlée', '🚀 launch', "don't stop"];
284
+ for (const input of inputs) {
285
+ const once = slugify(input);
286
+ const twice = slugify(once);
287
+ expect(twice).toBe(once);
288
+ }
289
+ });
290
+ });
package/src/index.ts CHANGED
@@ -38,3 +38,5 @@ export { parseUnifiedDiff } from './diff-parser.js';
38
38
 
39
39
  export { FixyCommandRunner } from './fixy-commands.js';
40
40
  export type { FixyCommandContext } from './fixy-commands.js';
41
+
42
+ export { slugify } from './slugify.js';
package/src/slugify.ts ADDED
@@ -0,0 +1,34 @@
1
+ const TRANSLITERATION_MAP: Record<string, string> = {
2
+ ß: 'ss',
3
+ æ: 'ae',
4
+ Æ: 'ae',
5
+ œ: 'oe',
6
+ Œ: 'oe',
7
+ ø: 'o',
8
+ Ø: 'o',
9
+ đ: 'd',
10
+ Đ: 'd',
11
+ ð: 'd',
12
+ Ð: 'd',
13
+ þ: 'th',
14
+ Þ: 'th',
15
+ ł: 'l',
16
+ Ł: 'l',
17
+ };
18
+
19
+ export function slugify(input: string): string {
20
+ return input
21
+ .normalize('NFKD')
22
+ .replace(/[^\u0000-\u007F]/g, (ch) => TRANSLITERATION_MAP[ch] ?? '')
23
+ // 1. Convert to lowercase
24
+ .toLowerCase()
25
+ // 2. Remove Unicode combining marks produced by normalization
26
+ .replace(/\p{M}/gu, '')
27
+ // 3. Remove apostrophes so contractions collapse without a separator
28
+ .replace(/['\u2018\u2019\u02bc]/g, '')
29
+ // 4. Replace each contiguous run of non-ASCII-alphanumeric characters with a single hyphen
30
+ .replace(/[^a-z0-9]+/g, '-')
31
+ // 5. Collapse repeated hyphens into one hyphen
32
+ .replace(/-{2,}/g, '-')
33
+ .replace(/^-+|-+$/g, '');
34
+ }
package/src/store.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import { mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { join } from 'node:path';
6
+ import { v7 as uuidv7 } from 'uuid';
6
7
 
7
8
  import type { FixyMessage, FixyThread } from './thread.js';
8
9
  import {
@@ -31,7 +32,7 @@ export class LocalThreadStore {
31
32
  * Writes project.json if it does not already exist.
32
33
  */
33
34
  async createThread(projectRoot: string): Promise<FixyThread> {
34
- const id = randomUUID();
35
+ const id = uuidv7();
35
36
  const projectId = computeProjectId(projectRoot);
36
37
  const now = new Date().toISOString();
37
38