@rune-kit/rune 2.12.0 → 2.12.2
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/compiler/__tests__/hooks-antigravity.test.js +3 -1
- package/compiler/__tests__/hooks-cursor.test.js +3 -1
- package/compiler/__tests__/hooks-install.test.js +3 -1
- package/compiler/__tests__/hooks-tiers.test.js +102 -1
- package/compiler/__tests__/hooks-windsurf.test.js +3 -1
- package/compiler/commands/hooks/tiers.js +108 -6
- package/package.json +1 -1
- package/skills/onboard/scripts/onboard-invariants.js +3 -1
|
@@ -4,12 +4,14 @@ import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promis
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { afterEach, beforeEach, describe, test } from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import * as antigravity from '../adapters/hooks/antigravity.js';
|
|
8
9
|
import { installHooks } from '../commands/hooks/install.js';
|
|
9
10
|
import { hookStatus } from '../commands/hooks/status.js';
|
|
10
11
|
import { uninstallHooks } from '../commands/hooks/uninstall.js';
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const RUNE_ROOT = path.resolve(__dirname, '..', '..');
|
|
13
15
|
const RULES_DIR = '.antigravity/rules';
|
|
14
16
|
|
|
15
17
|
let tmpRoot;
|
|
@@ -4,12 +4,14 @@ import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promis
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { afterEach, beforeEach, describe, test } from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import * as cursor from '../adapters/hooks/cursor.js';
|
|
8
9
|
import { installHooks } from '../commands/hooks/install.js';
|
|
9
10
|
import { hookStatus } from '../commands/hooks/status.js';
|
|
10
11
|
import { uninstallHooks } from '../commands/hooks/uninstall.js';
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const RUNE_ROOT = path.resolve(__dirname, '..', '..');
|
|
13
15
|
const RULES_DIR = '.cursor/rules';
|
|
14
16
|
|
|
15
17
|
let tmpRoot;
|
|
@@ -4,12 +4,14 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { afterEach, beforeEach, describe, test } from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import { installHooks } from '../commands/hooks/install.js';
|
|
8
9
|
import { SETTINGS_REL_PATH } from '../commands/hooks/presets.js';
|
|
9
10
|
import { hookStatus } from '../commands/hooks/status.js';
|
|
10
11
|
import { uninstallHooks } from '../commands/hooks/uninstall.js';
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const RUNE_ROOT = path.resolve(__dirname, '..', '..');
|
|
13
15
|
|
|
14
16
|
let tmpRoot;
|
|
15
17
|
|
|
@@ -18,18 +18,25 @@ import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promis
|
|
|
18
18
|
import { tmpdir } from 'node:os';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { afterEach, beforeEach, describe, test } from 'node:test';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
21
22
|
import { installHooks } from '../commands/hooks/install.js';
|
|
22
23
|
import { SETTINGS_REL_PATH } from '../commands/hooks/presets.js';
|
|
23
24
|
import { hookStatus } from '../commands/hooks/status.js';
|
|
24
25
|
import {
|
|
26
|
+
_resetFreeVersionCache,
|
|
27
|
+
assertFreeVersionCompat,
|
|
25
28
|
checkManifestRequires,
|
|
29
|
+
compareSemver,
|
|
30
|
+
getFreeVersion,
|
|
26
31
|
loadTierManifest,
|
|
27
32
|
locateTierManifest,
|
|
33
|
+
parseSemver,
|
|
28
34
|
resolveTier,
|
|
29
35
|
validateManifest,
|
|
30
36
|
} from '../commands/hooks/tiers.js';
|
|
31
37
|
|
|
32
|
-
const
|
|
38
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const RUNE_ROOT = path.resolve(__dirname, '..', '..');
|
|
33
40
|
|
|
34
41
|
let tmpRoot;
|
|
35
42
|
let tierRoot;
|
|
@@ -494,6 +501,100 @@ describe('review fixes: M3 overrides consumption', () => {
|
|
|
494
501
|
});
|
|
495
502
|
});
|
|
496
503
|
|
|
504
|
+
describe('Gap 2: minFreeVersion version gate', () => {
|
|
505
|
+
test('parseSemver accepts x.y.z and ignores prerelease/build', () => {
|
|
506
|
+
assert.deepStrictEqual(parseSemver('2.12.0'), [2, 12, 0]);
|
|
507
|
+
assert.deepStrictEqual(parseSemver('2.12.0-rc.1'), [2, 12, 0]);
|
|
508
|
+
assert.deepStrictEqual(parseSemver('2.12.0+sha.abc'), [2, 12, 0]);
|
|
509
|
+
assert.strictEqual(parseSemver('not-a-version'), null);
|
|
510
|
+
assert.strictEqual(parseSemver(null), null);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test('compareSemver returns -1/0/1 ordering', () => {
|
|
514
|
+
assert.strictEqual(compareSemver('2.12.0', '2.12.0'), 0);
|
|
515
|
+
assert.strictEqual(compareSemver('2.11.9', '2.12.0'), -1);
|
|
516
|
+
assert.strictEqual(compareSemver('2.12.1', '2.12.0'), 1);
|
|
517
|
+
assert.strictEqual(compareSemver('3.0.0', '2.12.0'), 1);
|
|
518
|
+
assert.strictEqual(compareSemver('bogus', '2.12.0'), null);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test('validateManifest accepts optional minFreeVersion', () => {
|
|
522
|
+
const m = validateManifest({ ...PRO_MANIFEST_FIXTURE, minFreeVersion: '2.12.0' });
|
|
523
|
+
assert.strictEqual(m.minFreeVersion, '2.12.0');
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test('validateManifest rejects non-semver minFreeVersion', () => {
|
|
527
|
+
assert.throws(
|
|
528
|
+
() => validateManifest({ ...PRO_MANIFEST_FIXTURE, minFreeVersion: 'latest' }),
|
|
529
|
+
/minFreeVersion.*must be semver/,
|
|
530
|
+
);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('validateManifest rejects empty minFreeVersion string', () => {
|
|
534
|
+
assert.throws(() => validateManifest({ ...PRO_MANIFEST_FIXTURE, minFreeVersion: '' }), /minFreeVersion.*non-empty/);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test('assertFreeVersionCompat is a no-op when minFreeVersion unset', () => {
|
|
538
|
+
const m = validateManifest(PRO_MANIFEST_FIXTURE);
|
|
539
|
+
assert.doesNotThrow(() => assertFreeVersionCompat(m, '0.0.1'));
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test('assertFreeVersionCompat passes when current >= minFreeVersion', () => {
|
|
543
|
+
const m = validateManifest({ ...PRO_MANIFEST_FIXTURE, minFreeVersion: '2.12.0' });
|
|
544
|
+
assert.doesNotThrow(() => assertFreeVersionCompat(m, '2.12.0'));
|
|
545
|
+
assert.doesNotThrow(() => assertFreeVersionCompat(m, '2.12.1'));
|
|
546
|
+
assert.doesNotThrow(() => assertFreeVersionCompat(m, '3.0.0'));
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test('assertFreeVersionCompat throws helpful upgrade error when current < minFreeVersion', () => {
|
|
550
|
+
const m = validateManifest({ ...PRO_MANIFEST_FIXTURE, minFreeVersion: '2.12.0' });
|
|
551
|
+
assert.throws(
|
|
552
|
+
() => assertFreeVersionCompat(m, '2.11.0'),
|
|
553
|
+
(err) => {
|
|
554
|
+
assert.match(err.message, /requires Rune Free >= 2\.12\.0/);
|
|
555
|
+
assert.match(err.message, /installed compiler is 2\.11\.0/);
|
|
556
|
+
assert.match(err.message, /npm i -g @rune-kit\/rune@latest/);
|
|
557
|
+
assert.match(err.message, /--tier pro/);
|
|
558
|
+
return true;
|
|
559
|
+
},
|
|
560
|
+
);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test('resolveTier enforces minFreeVersion via assertFreeVersionCompat', async () => {
|
|
564
|
+
const futureManifest = { ...PRO_MANIFEST_FIXTURE, minFreeVersion: '999.0.0' };
|
|
565
|
+
await writeFile(path.join(tierRoot, 'hooks', 'manifest.json'), JSON.stringify(futureManifest, null, 2));
|
|
566
|
+
await assert.rejects(resolveTier('pro', tmpRoot), /requires Rune Free >= 999\.0\.0/);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test('getFreeVersion reads package.json semver', () => {
|
|
570
|
+
_resetFreeVersionCache();
|
|
571
|
+
const v = getFreeVersion();
|
|
572
|
+
assert.match(v, /^\d+\.\d+\.\d+/);
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe('Gap 3: actionable missing-manifest error', () => {
|
|
577
|
+
test('resolveTier error lists both env var and monorepo sibling path', async () => {
|
|
578
|
+
delete process.env.RUNE_PRO_ROOT;
|
|
579
|
+
await assert.rejects(resolveTier('pro', tmpRoot), (err) => {
|
|
580
|
+
assert.match(err.message, /RUNE_PRO_ROOT/);
|
|
581
|
+
assert.match(err.message, /monorepo sibling/);
|
|
582
|
+
assert.match(err.message, /Drop --tier pro/);
|
|
583
|
+
assert.match(err.message, /rune\.dev\/docs\/hooks/);
|
|
584
|
+
return true;
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test('resolveTier error for unknown tier (no env var) still includes sibling + docs hint', async () => {
|
|
589
|
+
await assert.rejects(resolveTier('unknowntier', tmpRoot), (err) => {
|
|
590
|
+
assert.match(err.message, /monorepo sibling fallback/);
|
|
591
|
+
assert.match(err.message, /Drop --tier unknowntier/);
|
|
592
|
+
assert.match(err.message, /rune\.dev\/docs\/hooks/);
|
|
593
|
+
return true;
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
497
598
|
describe('regression: Pro+Claude parity', () => {
|
|
498
599
|
test('Pro entries present BEFORE and AFTER migration produce equivalent settings.json shape', async () => {
|
|
499
600
|
await mkdir(path.join(tmpRoot, '.claude'), { recursive: true });
|
|
@@ -3,12 +3,14 @@ import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promis
|
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { afterEach, beforeEach, describe, test } from 'node:test';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import * as windsurf from '../adapters/hooks/windsurf.js';
|
|
7
8
|
import { installHooks } from '../commands/hooks/install.js';
|
|
8
9
|
import { hookStatus } from '../commands/hooks/status.js';
|
|
9
10
|
import { uninstallHooks } from '../commands/hooks/uninstall.js';
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const RUNE_ROOT = path.resolve(__dirname, '..', '..');
|
|
12
14
|
const WF_DIR = '.windsurf/workflows';
|
|
13
15
|
const RULES_DIR = '.windsurf/rules';
|
|
14
16
|
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
* third-party) plugs in by shipping a manifest at a known path.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { existsSync } from 'node:fs';
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
20
|
import { readFile } from 'node:fs/promises';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
22
23
|
|
|
23
24
|
/** Known tier env vars. Adding a new tier = add its env var here. */
|
|
24
25
|
export const TIER_ENV_VARS = Object.freeze({
|
|
@@ -26,6 +27,56 @@ export const TIER_ENV_VARS = Object.freeze({
|
|
|
26
27
|
business: 'RUNE_BUSINESS_ROOT',
|
|
27
28
|
});
|
|
28
29
|
|
|
30
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read Free compiler version from the bundled package.json. Cached after first read.
|
|
34
|
+
* @returns {string} semver string, e.g. "2.12.1"
|
|
35
|
+
*/
|
|
36
|
+
let _freeVersionCache = null;
|
|
37
|
+
export function getFreeVersion() {
|
|
38
|
+
if (_freeVersionCache) return _freeVersionCache;
|
|
39
|
+
const pkgPath = path.resolve(__dirname, '..', '..', '..', 'package.json');
|
|
40
|
+
try {
|
|
41
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
42
|
+
_freeVersionCache = typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
43
|
+
} catch {
|
|
44
|
+
_freeVersionCache = '0.0.0';
|
|
45
|
+
}
|
|
46
|
+
return _freeVersionCache;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** For test/override scenarios — resets the cache. */
|
|
50
|
+
export function _resetFreeVersionCache() {
|
|
51
|
+
_freeVersionCache = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse a semver `x.y.z` (ignores pre-release/build) into a tuple. Returns null on malformed input.
|
|
56
|
+
* @param {string} v
|
|
57
|
+
* @returns {[number,number,number]|null}
|
|
58
|
+
*/
|
|
59
|
+
export function parseSemver(v) {
|
|
60
|
+
if (typeof v !== 'string') return null;
|
|
61
|
+
const m = v.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
62
|
+
if (!m) return null;
|
|
63
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Compare two semver strings. Returns -1, 0, 1 or null if either is malformed.
|
|
68
|
+
*/
|
|
69
|
+
export function compareSemver(a, b) {
|
|
70
|
+
const pa = parseSemver(a);
|
|
71
|
+
const pb = parseSemver(b);
|
|
72
|
+
if (!pa || !pb) return null;
|
|
73
|
+
for (let i = 0; i < 3; i++) {
|
|
74
|
+
if (pa[i] < pb[i]) return -1;
|
|
75
|
+
if (pa[i] > pb[i]) return 1;
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
29
80
|
/** Valid event names a manifest entry may declare. */
|
|
30
81
|
const VALID_EVENTS = new Set(['UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop', 'statusLine']);
|
|
31
82
|
|
|
@@ -148,11 +199,23 @@ export function validateManifest(manifest, source = '<memory>') {
|
|
|
148
199
|
};
|
|
149
200
|
});
|
|
150
201
|
|
|
202
|
+
const minFreeVersion =
|
|
203
|
+
typeof manifest.minFreeVersion === 'string' && manifest.minFreeVersion.length > 0 ? manifest.minFreeVersion : null;
|
|
204
|
+
if (manifest.minFreeVersion !== undefined && minFreeVersion === null) {
|
|
205
|
+
throw new Error(`Manifest ${source}: 'minFreeVersion' must be a non-empty string if present`);
|
|
206
|
+
}
|
|
207
|
+
if (minFreeVersion && !parseSemver(minFreeVersion)) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Manifest ${source}: 'minFreeVersion' must be semver x.y.z (got ${JSON.stringify(minFreeVersion)})`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
151
213
|
return {
|
|
152
214
|
name: typeof manifest.name === 'string' ? manifest.name : `Rune ${manifest.tier} Hooks`,
|
|
153
215
|
description: typeof manifest.description === 'string' ? manifest.description : '',
|
|
154
216
|
tier: manifest.tier,
|
|
155
217
|
version: typeof manifest.version === 'string' ? manifest.version : '0.0.0',
|
|
218
|
+
minFreeVersion,
|
|
156
219
|
requires: Array.isArray(manifest.requires) ? [...manifest.requires] : [],
|
|
157
220
|
entries,
|
|
158
221
|
overrides: manifest.overrides && typeof manifest.overrides === 'object' ? { ...manifest.overrides } : {},
|
|
@@ -160,6 +223,29 @@ export function validateManifest(manifest, source = '<memory>') {
|
|
|
160
223
|
};
|
|
161
224
|
}
|
|
162
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Assert the current Free compiler satisfies a manifest's `minFreeVersion`.
|
|
228
|
+
* Throws with an actionable upgrade message when the local Free is too old.
|
|
229
|
+
*
|
|
230
|
+
* @param {import('./tiers.js').TierManifest} manifest
|
|
231
|
+
* @param {string} [currentFreeVersion] — defaults to `getFreeVersion()`. Override for tests.
|
|
232
|
+
*/
|
|
233
|
+
export function assertFreeVersionCompat(manifest, currentFreeVersion) {
|
|
234
|
+
if (!manifest || !manifest.minFreeVersion) return;
|
|
235
|
+
const current = currentFreeVersion ?? getFreeVersion();
|
|
236
|
+
const cmp = compareSemver(current, manifest.minFreeVersion);
|
|
237
|
+
if (cmp === null) {
|
|
238
|
+
// Malformed input — surface but don't block (defensive).
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (cmp < 0) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Tier '${manifest.tier}' requires Rune Free >= ${manifest.minFreeVersion} but the installed compiler is ${current}. ` +
|
|
244
|
+
`Upgrade Free first: \`npm i -g @rune-kit/rune@latest\` (or \`npx @rune-kit/rune@latest hooks install --tier ${manifest.tier}\`).`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
163
249
|
/**
|
|
164
250
|
* Check whether a manifest's `requires` list is satisfied by current env.
|
|
165
251
|
* Returns { ok, missing }.
|
|
@@ -180,16 +266,32 @@ export function checkManifestRequires(manifest) {
|
|
|
180
266
|
export async function resolveTier(tier, projectRoot) {
|
|
181
267
|
const manifestPath = locateTierManifest(tier, projectRoot);
|
|
182
268
|
if (!manifestPath) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
269
|
+
const envVar = TIER_ENV_VARS[tier];
|
|
270
|
+
const capitalized = tier.charAt(0).toUpperCase() + tier.slice(1);
|
|
271
|
+
const siblingPath = path.resolve(projectRoot, '..', capitalized, 'hooks', 'manifest.json');
|
|
272
|
+
const lines = [`Could not locate '${tier}' tier manifest. Rune looked in:`];
|
|
273
|
+
if (envVar) {
|
|
274
|
+
lines.push(` 1. $${envVar}/hooks/manifest.json (env var — set this if ${capitalized} is installed elsewhere)`);
|
|
275
|
+
lines.push(` 2. ${siblingPath} (monorepo sibling fallback)`);
|
|
276
|
+
} else {
|
|
277
|
+
lines.push(` • ${siblingPath} (monorepo sibling fallback)`);
|
|
278
|
+
}
|
|
279
|
+
lines.push('');
|
|
280
|
+
lines.push(`Fix one of:`);
|
|
281
|
+
if (envVar) {
|
|
282
|
+
lines.push(` • export ${envVar}=/path/to/${capitalized} # point at your ${tier} install`);
|
|
283
|
+
}
|
|
284
|
+
lines.push(` • Clone ${capitalized} next to Free so the sibling path resolves`);
|
|
285
|
+
lines.push(` • Drop --tier ${tier} to install Free-only hooks`);
|
|
286
|
+
lines.push('');
|
|
287
|
+
lines.push(`See https://rune.dev/docs/hooks#tiers for details.`);
|
|
288
|
+
throw new Error(lines.join('\n'));
|
|
188
289
|
}
|
|
189
290
|
const manifest = await loadTierManifest(manifestPath);
|
|
190
291
|
if (manifest.tier !== tier) {
|
|
191
292
|
throw new Error(`Manifest at ${manifestPath} declares tier='${manifest.tier}' but was requested as '${tier}'`);
|
|
192
293
|
}
|
|
294
|
+
assertFreeVersionCompat(manifest);
|
|
193
295
|
return manifest;
|
|
194
296
|
}
|
|
195
297
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rune-kit/rune",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.2",
|
|
4
4
|
"description": "62-skill mesh for AI coding assistants — runtime auto-discipline via native hooks (Claude/Cursor/Windsurf/Antigravity), 5-layer architecture, 215+ connections, multi-platform compiler.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,12 +19,14 @@
|
|
|
19
19
|
import { existsSync } from 'node:fs';
|
|
20
20
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
22
23
|
import { parseArgs } from 'node:util';
|
|
23
24
|
import { detectInvariants, renderInvariants } from './detect-invariants.js';
|
|
24
25
|
import { applyInvariantsPointer } from './inject-claude-md.js';
|
|
25
26
|
|
|
26
27
|
const AUTO_HEADER = '## Auto-detected (new)';
|
|
27
28
|
const TEMPLATE_REL_PATH = '../references/invariants-template.md';
|
|
29
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Locate the next `## `-level section boundary in `text`, ignoring any `##`
|
|
@@ -60,7 +62,7 @@ function findNextSectionOffset(text) {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export async function loadTemplate(templatePath) {
|
|
63
|
-
const resolved = templatePath ?? path.resolve(
|
|
65
|
+
const resolved = templatePath ?? path.resolve(__dirname, TEMPLATE_REL_PATH);
|
|
64
66
|
return readFile(resolved, 'utf8');
|
|
65
67
|
}
|
|
66
68
|
|