@forge-ts/core 0.7.2 → 0.13.0
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/dist/index.d.ts +501 -1
- package/dist/index.js +535 -28
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1,7 +1,168 @@
|
|
|
1
|
+
// src/audit.ts
|
|
2
|
+
import { appendFileSync, existsSync, readFileSync } from "fs";
|
|
3
|
+
import { userInfo } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
var AUDIT_FILENAME = ".forge-audit.jsonl";
|
|
6
|
+
function getCurrentUser() {
|
|
7
|
+
try {
|
|
8
|
+
return userInfo().username;
|
|
9
|
+
} catch {
|
|
10
|
+
return "unknown";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function appendAuditEvent(rootDir, event) {
|
|
14
|
+
const filePath = join(rootDir, AUDIT_FILENAME);
|
|
15
|
+
appendFileSync(filePath, `${JSON.stringify(event)}
|
|
16
|
+
`, "utf-8");
|
|
17
|
+
}
|
|
18
|
+
function readAuditLog(rootDir, options) {
|
|
19
|
+
const filePath = join(rootDir, AUDIT_FILENAME);
|
|
20
|
+
if (!existsSync(filePath)) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
24
|
+
const lines = raw.split("\n").filter((line) => line.trim().length > 0);
|
|
25
|
+
let events = lines.map((line) => JSON.parse(line));
|
|
26
|
+
if (options?.eventType) {
|
|
27
|
+
events = events.filter((e) => e.event === options.eventType);
|
|
28
|
+
}
|
|
29
|
+
events.reverse();
|
|
30
|
+
if (options?.limit !== void 0 && options.limit >= 0) {
|
|
31
|
+
events = events.slice(0, options.limit);
|
|
32
|
+
}
|
|
33
|
+
return events;
|
|
34
|
+
}
|
|
35
|
+
function formatAuditEvent(event) {
|
|
36
|
+
const reasonPart = event.reason ? ` \u2014 ${event.reason}` : "";
|
|
37
|
+
const detailKeys = Object.keys(event.details);
|
|
38
|
+
const detailPart = detailKeys.length > 0 ? ` ${JSON.stringify(event.details)}` : "";
|
|
39
|
+
return `[${event.timestamp}] ${event.event} by ${event.user}${reasonPart}${detailPart}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/bypass.ts
|
|
43
|
+
import { randomUUID } from "crypto";
|
|
44
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
45
|
+
import { userInfo as userInfo2 } from "os";
|
|
46
|
+
import { join as join2 } from "path";
|
|
47
|
+
var BYPASS_FILENAME = ".forge-bypass.json";
|
|
48
|
+
var DEFAULT_BYPASS_CONFIG = {
|
|
49
|
+
dailyBudget: 3,
|
|
50
|
+
durationHours: 24
|
|
51
|
+
};
|
|
52
|
+
function getCurrentUser2() {
|
|
53
|
+
try {
|
|
54
|
+
return userInfo2().username;
|
|
55
|
+
} catch {
|
|
56
|
+
return "unknown";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function readBypassFile(rootDir) {
|
|
60
|
+
const filePath = join2(rootDir, BYPASS_FILENAME);
|
|
61
|
+
if (!existsSync2(filePath)) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const raw = readFileSync2(filePath, "utf-8");
|
|
66
|
+
return JSON.parse(raw);
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function writeBypassFile(rootDir, records) {
|
|
72
|
+
const filePath = join2(rootDir, BYPASS_FILENAME);
|
|
73
|
+
writeFileSync(filePath, `${JSON.stringify(records, null, 2)}
|
|
74
|
+
`, "utf-8");
|
|
75
|
+
}
|
|
76
|
+
function resolveConfig(config) {
|
|
77
|
+
return {
|
|
78
|
+
...DEFAULT_BYPASS_CONFIG,
|
|
79
|
+
...config
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function startOfToday() {
|
|
83
|
+
const now = /* @__PURE__ */ new Date();
|
|
84
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
85
|
+
}
|
|
86
|
+
function createBypass(rootDir, reason, rule, config) {
|
|
87
|
+
const resolved = resolveConfig(config);
|
|
88
|
+
const remaining = getRemainingBudget(rootDir, config);
|
|
89
|
+
if (remaining <= 0) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Bypass budget exhausted: ${resolved.dailyBudget}/${resolved.dailyBudget} bypasses used today. Wait until tomorrow or increase bypass.dailyBudget in your forge-ts config.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const now = /* @__PURE__ */ new Date();
|
|
95
|
+
const expiresAt = new Date(now.getTime() + resolved.durationHours * 60 * 60 * 1e3);
|
|
96
|
+
const record = {
|
|
97
|
+
id: randomUUID(),
|
|
98
|
+
createdAt: now.toISOString(),
|
|
99
|
+
expiresAt: expiresAt.toISOString(),
|
|
100
|
+
reason,
|
|
101
|
+
rule: rule ?? "all",
|
|
102
|
+
user: getCurrentUser2()
|
|
103
|
+
};
|
|
104
|
+
const records = readBypassFile(rootDir);
|
|
105
|
+
records.push(record);
|
|
106
|
+
writeBypassFile(rootDir, records);
|
|
107
|
+
appendAuditEvent(rootDir, {
|
|
108
|
+
timestamp: record.createdAt,
|
|
109
|
+
event: "bypass.create",
|
|
110
|
+
user: record.user,
|
|
111
|
+
reason,
|
|
112
|
+
details: {
|
|
113
|
+
bypassId: record.id,
|
|
114
|
+
rule: record.rule,
|
|
115
|
+
expiresAt: record.expiresAt,
|
|
116
|
+
durationHours: resolved.durationHours
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return record;
|
|
120
|
+
}
|
|
121
|
+
function getActiveBypasses(rootDir) {
|
|
122
|
+
const records = readBypassFile(rootDir);
|
|
123
|
+
const now = /* @__PURE__ */ new Date();
|
|
124
|
+
return records.filter((r) => new Date(r.expiresAt) > now);
|
|
125
|
+
}
|
|
126
|
+
function isRuleBypassed(rootDir, ruleCode) {
|
|
127
|
+
const active = getActiveBypasses(rootDir);
|
|
128
|
+
return active.some((r) => r.rule === ruleCode || r.rule === "all");
|
|
129
|
+
}
|
|
130
|
+
function getRemainingBudget(rootDir, config) {
|
|
131
|
+
const resolved = resolveConfig(config);
|
|
132
|
+
const records = readBypassFile(rootDir);
|
|
133
|
+
const todayStart = startOfToday();
|
|
134
|
+
const todayCount = records.filter((r) => new Date(r.createdAt) >= todayStart).length;
|
|
135
|
+
return Math.max(0, resolved.dailyBudget - todayCount);
|
|
136
|
+
}
|
|
137
|
+
function expireOldBypasses(rootDir) {
|
|
138
|
+
const records = readBypassFile(rootDir);
|
|
139
|
+
const now = /* @__PURE__ */ new Date();
|
|
140
|
+
const active = records.filter((r) => new Date(r.expiresAt) > now);
|
|
141
|
+
const expired = records.filter((r) => new Date(r.expiresAt) <= now);
|
|
142
|
+
if (expired.length === 0) {
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
writeBypassFile(rootDir, active);
|
|
146
|
+
for (const record of expired) {
|
|
147
|
+
appendAuditEvent(rootDir, {
|
|
148
|
+
timestamp: now.toISOString(),
|
|
149
|
+
event: "bypass.expire",
|
|
150
|
+
user: record.user,
|
|
151
|
+
details: {
|
|
152
|
+
bypassId: record.id,
|
|
153
|
+
rule: record.rule,
|
|
154
|
+
createdAt: record.createdAt,
|
|
155
|
+
expiresAt: record.expiresAt
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return expired.length;
|
|
160
|
+
}
|
|
161
|
+
|
|
1
162
|
// src/config.ts
|
|
2
|
-
import { existsSync } from "fs";
|
|
163
|
+
import { existsSync as existsSync3 } from "fs";
|
|
3
164
|
import { readFile } from "fs/promises";
|
|
4
|
-
import { join, resolve } from "path";
|
|
165
|
+
import { join as join3, resolve } from "path";
|
|
5
166
|
import { pathToFileURL } from "url";
|
|
6
167
|
|
|
7
168
|
// src/types.ts
|
|
@@ -17,8 +178,8 @@ var Visibility = /* @__PURE__ */ ((Visibility2) => {
|
|
|
17
178
|
function defaultConfig(rootDir) {
|
|
18
179
|
return {
|
|
19
180
|
rootDir,
|
|
20
|
-
tsconfig:
|
|
21
|
-
outDir:
|
|
181
|
+
tsconfig: join3(rootDir, "tsconfig.json"),
|
|
182
|
+
outDir: join3(rootDir, "docs"),
|
|
22
183
|
enforce: {
|
|
23
184
|
enabled: true,
|
|
24
185
|
minVisibility: "public" /* Public */,
|
|
@@ -30,17 +191,25 @@ function defaultConfig(rootDir) {
|
|
|
30
191
|
"require-example": "error",
|
|
31
192
|
"require-package-doc": "warn",
|
|
32
193
|
"require-class-member-doc": "error",
|
|
33
|
-
"require-interface-member-doc": "error"
|
|
194
|
+
"require-interface-member-doc": "error",
|
|
195
|
+
"require-tsdoc-syntax": "warn",
|
|
196
|
+
"require-remarks": "error",
|
|
197
|
+
"require-default-value": "warn",
|
|
198
|
+
"require-type-param": "error",
|
|
199
|
+
"require-see": "warn",
|
|
200
|
+
"require-release-tag": "error",
|
|
201
|
+
"require-fresh-guides": "warn",
|
|
202
|
+
"require-guide-coverage": "warn"
|
|
34
203
|
}
|
|
35
204
|
},
|
|
36
205
|
doctest: {
|
|
37
206
|
enabled: true,
|
|
38
|
-
cacheDir:
|
|
207
|
+
cacheDir: join3(rootDir, ".cache", "doctest")
|
|
39
208
|
},
|
|
40
209
|
api: {
|
|
41
210
|
enabled: false,
|
|
42
211
|
openapi: false,
|
|
43
|
-
openapiPath:
|
|
212
|
+
openapiPath: join3(rootDir, "docs", "openapi.json")
|
|
44
213
|
},
|
|
45
214
|
gen: {
|
|
46
215
|
enabled: true,
|
|
@@ -49,6 +218,39 @@ function defaultConfig(rootDir) {
|
|
|
49
218
|
readmeSync: false
|
|
50
219
|
},
|
|
51
220
|
skill: {},
|
|
221
|
+
bypass: {
|
|
222
|
+
dailyBudget: 3,
|
|
223
|
+
durationHours: 24
|
|
224
|
+
},
|
|
225
|
+
tsdoc: {
|
|
226
|
+
writeConfig: true,
|
|
227
|
+
customTags: [],
|
|
228
|
+
enforce: {
|
|
229
|
+
core: "error",
|
|
230
|
+
extended: "warn",
|
|
231
|
+
discretionary: "off"
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
guides: {
|
|
235
|
+
enabled: true,
|
|
236
|
+
autoDiscover: true,
|
|
237
|
+
custom: []
|
|
238
|
+
},
|
|
239
|
+
guards: {
|
|
240
|
+
tsconfig: {
|
|
241
|
+
enabled: true,
|
|
242
|
+
requiredFlags: ["strict", "strictNullChecks", "noImplicitAny"]
|
|
243
|
+
},
|
|
244
|
+
biome: {
|
|
245
|
+
enabled: false,
|
|
246
|
+
lockedRules: []
|
|
247
|
+
},
|
|
248
|
+
packageJson: {
|
|
249
|
+
enabled: true,
|
|
250
|
+
minNodeVersion: "22.0.0",
|
|
251
|
+
requiredFields: ["type", "engines"]
|
|
252
|
+
}
|
|
253
|
+
},
|
|
52
254
|
project: {}
|
|
53
255
|
};
|
|
54
256
|
}
|
|
@@ -61,6 +263,10 @@ var KNOWN_TOP_KEYS = /* @__PURE__ */ new Set([
|
|
|
61
263
|
"api",
|
|
62
264
|
"gen",
|
|
63
265
|
"skill",
|
|
266
|
+
"bypass",
|
|
267
|
+
"tsdoc",
|
|
268
|
+
"guides",
|
|
269
|
+
"guards",
|
|
64
270
|
"project"
|
|
65
271
|
]);
|
|
66
272
|
var KNOWN_RULE_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -70,8 +276,30 @@ var KNOWN_RULE_KEYS = /* @__PURE__ */ new Set([
|
|
|
70
276
|
"require-example",
|
|
71
277
|
"require-package-doc",
|
|
72
278
|
"require-class-member-doc",
|
|
73
|
-
"require-interface-member-doc"
|
|
279
|
+
"require-interface-member-doc",
|
|
280
|
+
"require-tsdoc-syntax",
|
|
281
|
+
"require-remarks",
|
|
282
|
+
"require-default-value",
|
|
283
|
+
"require-type-param",
|
|
284
|
+
"require-see",
|
|
285
|
+
"require-release-tag",
|
|
286
|
+
"require-fresh-guides",
|
|
287
|
+
"require-guide-coverage"
|
|
74
288
|
]);
|
|
289
|
+
var KNOWN_TSDOC_KEYS = /* @__PURE__ */ new Set(["writeConfig", "customTags", "enforce"]);
|
|
290
|
+
var KNOWN_TSDOC_ENFORCE_KEYS = /* @__PURE__ */ new Set(["core", "extended", "discretionary"]);
|
|
291
|
+
var KNOWN_GUIDES_KEYS = /* @__PURE__ */ new Set(["enabled", "autoDiscover", "custom"]);
|
|
292
|
+
var KNOWN_GUARDS_KEYS = /* @__PURE__ */ new Set(["tsconfig", "biome", "packageJson"]);
|
|
293
|
+
var KNOWN_GUARDS_TSCONFIG_KEYS = /* @__PURE__ */ new Set(["enabled", "requiredFlags"]);
|
|
294
|
+
var KNOWN_GUARDS_BIOME_KEYS = /* @__PURE__ */ new Set(["enabled", "lockedRules"]);
|
|
295
|
+
var KNOWN_GUARDS_PACKAGE_JSON_KEYS = /* @__PURE__ */ new Set(["enabled", "minNodeVersion", "requiredFields"]);
|
|
296
|
+
function validateKnownKeys(obj, knownKeys, section, warnings) {
|
|
297
|
+
for (const key of Object.keys(obj)) {
|
|
298
|
+
if (!knownKeys.has(key)) {
|
|
299
|
+
warnings.push(`Unknown key "${key}" in ${section} \u2014 ignored.`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
75
303
|
function collectUnknownKeyWarnings(partial) {
|
|
76
304
|
const warnings = [];
|
|
77
305
|
for (const key of Object.keys(partial)) {
|
|
@@ -88,6 +316,62 @@ function collectUnknownKeyWarnings(partial) {
|
|
|
88
316
|
}
|
|
89
317
|
}
|
|
90
318
|
}
|
|
319
|
+
if (partial.tsdoc) {
|
|
320
|
+
validateKnownKeys(
|
|
321
|
+
partial.tsdoc,
|
|
322
|
+
KNOWN_TSDOC_KEYS,
|
|
323
|
+
"tsdoc",
|
|
324
|
+
warnings
|
|
325
|
+
);
|
|
326
|
+
if (partial.tsdoc.enforce) {
|
|
327
|
+
validateKnownKeys(
|
|
328
|
+
partial.tsdoc.enforce,
|
|
329
|
+
KNOWN_TSDOC_ENFORCE_KEYS,
|
|
330
|
+
"tsdoc.enforce",
|
|
331
|
+
warnings
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (partial.guides) {
|
|
336
|
+
validateKnownKeys(
|
|
337
|
+
partial.guides,
|
|
338
|
+
KNOWN_GUIDES_KEYS,
|
|
339
|
+
"guides",
|
|
340
|
+
warnings
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
if (partial.guards) {
|
|
344
|
+
validateKnownKeys(
|
|
345
|
+
partial.guards,
|
|
346
|
+
KNOWN_GUARDS_KEYS,
|
|
347
|
+
"guards",
|
|
348
|
+
warnings
|
|
349
|
+
);
|
|
350
|
+
if (partial.guards.tsconfig) {
|
|
351
|
+
validateKnownKeys(
|
|
352
|
+
partial.guards.tsconfig,
|
|
353
|
+
KNOWN_GUARDS_TSCONFIG_KEYS,
|
|
354
|
+
"guards.tsconfig",
|
|
355
|
+
warnings
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (partial.guards.biome) {
|
|
359
|
+
validateKnownKeys(
|
|
360
|
+
partial.guards.biome,
|
|
361
|
+
KNOWN_GUARDS_BIOME_KEYS,
|
|
362
|
+
"guards.biome",
|
|
363
|
+
warnings
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (partial.guards.packageJson) {
|
|
367
|
+
validateKnownKeys(
|
|
368
|
+
partial.guards.packageJson,
|
|
369
|
+
KNOWN_GUARDS_PACKAGE_JSON_KEYS,
|
|
370
|
+
"guards.packageJson",
|
|
371
|
+
warnings
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
91
375
|
for (const w of warnings) {
|
|
92
376
|
console.error(`[forge-ts] warning: ${w}`);
|
|
93
377
|
}
|
|
@@ -108,6 +392,20 @@ function mergeWithDefaults(rootDir, partial) {
|
|
|
108
392
|
api: { ...defaults.api, ...partial.api },
|
|
109
393
|
gen: { ...defaults.gen, ...partial.gen },
|
|
110
394
|
skill: { ...defaults.skill, ...partial.skill },
|
|
395
|
+
bypass: { ...defaults.bypass, ...partial.bypass },
|
|
396
|
+
guides: { ...defaults.guides, ...partial.guides },
|
|
397
|
+
tsdoc: {
|
|
398
|
+
...defaults.tsdoc,
|
|
399
|
+
...partial.tsdoc,
|
|
400
|
+
enforce: { ...defaults.tsdoc.enforce, ...partial.tsdoc?.enforce }
|
|
401
|
+
},
|
|
402
|
+
guards: {
|
|
403
|
+
...defaults.guards,
|
|
404
|
+
...partial.guards,
|
|
405
|
+
tsconfig: { ...defaults.guards.tsconfig, ...partial.guards?.tsconfig },
|
|
406
|
+
biome: { ...defaults.guards.biome, ...partial.guards?.biome },
|
|
407
|
+
packageJson: { ...defaults.guards.packageJson, ...partial.guards?.packageJson }
|
|
408
|
+
},
|
|
111
409
|
project: { ...defaults.project, ...partial.project }
|
|
112
410
|
};
|
|
113
411
|
if (warnings.length > 0) {
|
|
@@ -150,11 +448,11 @@ async function loadPackageJsonConfig(pkgPath) {
|
|
|
150
448
|
async function loadConfig(rootDir) {
|
|
151
449
|
const root = resolve(rootDir ?? process.cwd());
|
|
152
450
|
let config;
|
|
153
|
-
const candidates = [
|
|
451
|
+
const candidates = [join3(root, "forge-ts.config.ts"), join3(root, "forge-ts.config.js")];
|
|
154
452
|
let found = false;
|
|
155
453
|
const loadWarnings = [];
|
|
156
454
|
for (const candidate of candidates) {
|
|
157
|
-
if (
|
|
455
|
+
if (existsSync3(candidate)) {
|
|
158
456
|
const partial = await loadModuleConfig(candidate);
|
|
159
457
|
if (partial) {
|
|
160
458
|
config = mergeWithDefaults(root, partial);
|
|
@@ -167,8 +465,8 @@ async function loadConfig(rootDir) {
|
|
|
167
465
|
}
|
|
168
466
|
}
|
|
169
467
|
if (!found) {
|
|
170
|
-
const pkgPath2 =
|
|
171
|
-
if (
|
|
468
|
+
const pkgPath2 = join3(root, "package.json");
|
|
469
|
+
if (existsSync3(pkgPath2)) {
|
|
172
470
|
const partial = await loadPackageJsonConfig(pkgPath2);
|
|
173
471
|
if (partial) {
|
|
174
472
|
config = mergeWithDefaults(root, partial);
|
|
@@ -181,8 +479,8 @@ async function loadConfig(rootDir) {
|
|
|
181
479
|
} else {
|
|
182
480
|
config = config;
|
|
183
481
|
}
|
|
184
|
-
const pkgPath =
|
|
185
|
-
if (
|
|
482
|
+
const pkgPath = join3(root, "package.json");
|
|
483
|
+
if (existsSync3(pkgPath)) {
|
|
186
484
|
try {
|
|
187
485
|
const raw = await readFile(pkgPath, "utf8");
|
|
188
486
|
const pkg = JSON.parse(raw);
|
|
@@ -224,6 +522,135 @@ async function loadConfig(rootDir) {
|
|
|
224
522
|
return config;
|
|
225
523
|
}
|
|
226
524
|
|
|
525
|
+
// src/lock.ts
|
|
526
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
527
|
+
import { join as join4 } from "path";
|
|
528
|
+
var LOCK_FILE_NAME = ".forge-lock.json";
|
|
529
|
+
function readLockFile(rootDir) {
|
|
530
|
+
const lockPath = join4(rootDir, LOCK_FILE_NAME);
|
|
531
|
+
if (!existsSync4(lockPath)) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
const raw = readFileSync3(lockPath, "utf8");
|
|
536
|
+
return JSON.parse(raw);
|
|
537
|
+
} catch {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function writeLockFile(rootDir, manifest) {
|
|
542
|
+
const lockPath = join4(rootDir, LOCK_FILE_NAME);
|
|
543
|
+
writeFileSync2(lockPath, `${JSON.stringify(manifest, null, 2)}
|
|
544
|
+
`, "utf8");
|
|
545
|
+
}
|
|
546
|
+
function removeLockFile(rootDir) {
|
|
547
|
+
const lockPath = join4(rootDir, LOCK_FILE_NAME);
|
|
548
|
+
if (!existsSync4(lockPath)) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
unlinkSync(lockPath);
|
|
553
|
+
return true;
|
|
554
|
+
} catch {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function createLockManifest(config, lockedBy = "forge-ts lock") {
|
|
559
|
+
const rules = {};
|
|
560
|
+
for (const [key, value] of Object.entries(config.enforce.rules)) {
|
|
561
|
+
rules[key] = value;
|
|
562
|
+
}
|
|
563
|
+
const lockConfig = { rules };
|
|
564
|
+
if (config.guards.tsconfig.enabled) {
|
|
565
|
+
lockConfig.tsconfig = {
|
|
566
|
+
enabled: config.guards.tsconfig.enabled,
|
|
567
|
+
requiredFlags: config.guards.tsconfig.requiredFlags
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (config.guards.biome.enabled) {
|
|
571
|
+
lockConfig.biome = {
|
|
572
|
+
enabled: config.guards.biome.enabled,
|
|
573
|
+
lockedRules: config.guards.biome.lockedRules
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
version: "1.0.0",
|
|
578
|
+
lockedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
579
|
+
lockedBy,
|
|
580
|
+
config: lockConfig
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function validateAgainstLock(config, lock) {
|
|
584
|
+
const violations = [];
|
|
585
|
+
const severityRank = {
|
|
586
|
+
off: 0,
|
|
587
|
+
warn: 1,
|
|
588
|
+
error: 2
|
|
589
|
+
};
|
|
590
|
+
for (const [ruleName, lockedSeverity] of Object.entries(lock.config.rules)) {
|
|
591
|
+
const currentSeverity = config.enforce.rules[ruleName] ?? "off";
|
|
592
|
+
const lockedRank = severityRank[lockedSeverity] ?? 0;
|
|
593
|
+
const currentRank = severityRank[currentSeverity] ?? 0;
|
|
594
|
+
if (currentRank < lockedRank) {
|
|
595
|
+
violations.push({
|
|
596
|
+
field: `rules.${ruleName}`,
|
|
597
|
+
locked: lockedSeverity,
|
|
598
|
+
current: currentSeverity,
|
|
599
|
+
message: `Rule "${ruleName}" was weakened from "${lockedSeverity}" to "${currentSeverity}". Locked settings cannot be weakened without running "forge-ts unlock --reason=...".`
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (lock.config.tsconfig) {
|
|
604
|
+
const lockedTsconfig = lock.config.tsconfig;
|
|
605
|
+
if (lockedTsconfig.enabled && !config.guards.tsconfig.enabled) {
|
|
606
|
+
violations.push({
|
|
607
|
+
field: "guards.tsconfig.enabled",
|
|
608
|
+
locked: "true",
|
|
609
|
+
current: "false",
|
|
610
|
+
message: 'tsconfig guard was disabled. Locked settings cannot be weakened without running "forge-ts unlock --reason=...".'
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
if (lockedTsconfig.requiredFlags && config.guards.tsconfig.enabled) {
|
|
614
|
+
const currentFlags = new Set(config.guards.tsconfig.requiredFlags);
|
|
615
|
+
for (const flag of lockedTsconfig.requiredFlags) {
|
|
616
|
+
if (!currentFlags.has(flag)) {
|
|
617
|
+
violations.push({
|
|
618
|
+
field: `guards.tsconfig.requiredFlags.${flag}`,
|
|
619
|
+
locked: flag,
|
|
620
|
+
current: "(removed)",
|
|
621
|
+
message: `tsconfig required flag "${flag}" was removed. Locked settings cannot be weakened without running "forge-ts unlock --reason=...".`
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (lock.config.biome) {
|
|
628
|
+
const lockedBiome = lock.config.biome;
|
|
629
|
+
if (lockedBiome.enabled && !config.guards.biome.enabled) {
|
|
630
|
+
violations.push({
|
|
631
|
+
field: "guards.biome.enabled",
|
|
632
|
+
locked: "true",
|
|
633
|
+
current: "false",
|
|
634
|
+
message: 'Biome guard was disabled. Locked settings cannot be weakened without running "forge-ts unlock --reason=...".'
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
if (lockedBiome.lockedRules && config.guards.biome.enabled) {
|
|
638
|
+
const currentRules = new Set(config.guards.biome.lockedRules);
|
|
639
|
+
for (const rule of lockedBiome.lockedRules) {
|
|
640
|
+
if (!currentRules.has(rule)) {
|
|
641
|
+
violations.push({
|
|
642
|
+
field: `guards.biome.lockedRules.${rule}`,
|
|
643
|
+
locked: rule,
|
|
644
|
+
current: "(removed)",
|
|
645
|
+
message: `Biome locked rule "${rule}" was removed. Locked settings cannot be weakened without running "forge-ts unlock --reason=...".`
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return violations;
|
|
652
|
+
}
|
|
653
|
+
|
|
227
654
|
// src/visibility.ts
|
|
228
655
|
function resolveVisibility(tags) {
|
|
229
656
|
if (!tags) return "public" /* Public */;
|
|
@@ -246,15 +673,34 @@ function filterByVisibility(symbols, minVisibility) {
|
|
|
246
673
|
}
|
|
247
674
|
|
|
248
675
|
// src/walker.ts
|
|
249
|
-
import { readFileSync } from "fs";
|
|
250
|
-
import { resolve as resolve2 } from "path";
|
|
676
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
677
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
251
678
|
import {
|
|
252
679
|
DocNodeKind,
|
|
253
680
|
StandardTags,
|
|
254
681
|
TSDocConfiguration,
|
|
255
682
|
TSDocParser
|
|
256
683
|
} from "@microsoft/tsdoc";
|
|
684
|
+
import { TSDocConfigFile } from "@microsoft/tsdoc-config";
|
|
257
685
|
import ts from "typescript";
|
|
686
|
+
var tsdocConfigCache = /* @__PURE__ */ new Map();
|
|
687
|
+
function clearTSDocConfigCache() {
|
|
688
|
+
tsdocConfigCache.clear();
|
|
689
|
+
}
|
|
690
|
+
function loadTSDocConfiguration(folderPath) {
|
|
691
|
+
const cached = tsdocConfigCache.get(folderPath);
|
|
692
|
+
if (cached) return cached;
|
|
693
|
+
const configuration = new TSDocConfiguration();
|
|
694
|
+
try {
|
|
695
|
+
const configFile = TSDocConfigFile.loadForFolder(folderPath);
|
|
696
|
+
if (!configFile.fileNotFound && !configFile.hasErrors) {
|
|
697
|
+
configFile.configureParser(configuration);
|
|
698
|
+
}
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
701
|
+
tsdocConfigCache.set(folderPath, configuration);
|
|
702
|
+
return configuration;
|
|
703
|
+
}
|
|
258
704
|
function renderInlineNodes(nodes) {
|
|
259
705
|
const parts = [];
|
|
260
706
|
for (const node of nodes) {
|
|
@@ -309,8 +755,8 @@ function extractExamples(comment, startLine) {
|
|
|
309
755
|
}
|
|
310
756
|
return examples;
|
|
311
757
|
}
|
|
312
|
-
function parseTSDoc(rawComment, startLine) {
|
|
313
|
-
const configuration = new TSDocConfiguration();
|
|
758
|
+
function parseTSDoc(rawComment, startLine, folderPath) {
|
|
759
|
+
const configuration = folderPath !== void 0 ? loadTSDocConfiguration(folderPath) : new TSDocConfiguration();
|
|
314
760
|
const parser = new TSDocParser(configuration);
|
|
315
761
|
const result = parser.parseString(rawComment);
|
|
316
762
|
const comment = result.docComment;
|
|
@@ -362,6 +808,43 @@ function parseTSDoc(rawComment, startLine) {
|
|
|
362
808
|
}
|
|
363
809
|
}
|
|
364
810
|
}
|
|
811
|
+
if (comment.remarksBlock) {
|
|
812
|
+
const remarksText = renderBlock(comment.remarksBlock).trim();
|
|
813
|
+
tags.remarks = remarksText ? [remarksText] : [];
|
|
814
|
+
}
|
|
815
|
+
if (comment.seeBlocks.length > 0) {
|
|
816
|
+
tags.see = comment.seeBlocks.map((block) => renderBlock(block).trim()).filter(Boolean);
|
|
817
|
+
}
|
|
818
|
+
if (comment.typeParams.count > 0) {
|
|
819
|
+
tags.typeParam = comment.typeParams.blocks.map(
|
|
820
|
+
(block) => `${block.parameterName} - ${renderBlock(block).trim()}`
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
for (const block of comment.customBlocks) {
|
|
824
|
+
if (block.blockTag.tagName.toLowerCase() === "@defaultvalue") {
|
|
825
|
+
const dvText = renderBlock(block).trim();
|
|
826
|
+
if (!tags.defaultValue) tags.defaultValue = [];
|
|
827
|
+
tags.defaultValue.push(dvText);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
for (const block of comment.customBlocks) {
|
|
831
|
+
if (block.blockTag.tagName.toLowerCase() === "@concept") {
|
|
832
|
+
const conceptText = renderBlock(block).trim();
|
|
833
|
+
if (conceptText) {
|
|
834
|
+
if (!tags.concept) tags.concept = [];
|
|
835
|
+
tags.concept.push(conceptText);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
for (const block of comment.customBlocks) {
|
|
840
|
+
if (block.blockTag.tagName.toLowerCase() === "@guide") {
|
|
841
|
+
const guideText = renderBlock(block).trim();
|
|
842
|
+
if (guideText) {
|
|
843
|
+
if (!tags.guide) tags.guide = [];
|
|
844
|
+
tags.guide.push(guideText);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
365
848
|
const examples = extractExamples(comment, startLine);
|
|
366
849
|
const links = [];
|
|
367
850
|
function walkForLinks(node) {
|
|
@@ -379,6 +862,14 @@ function parseTSDoc(rawComment, startLine) {
|
|
|
379
862
|
}
|
|
380
863
|
}
|
|
381
864
|
walkForLinks(comment);
|
|
865
|
+
const parseMessages = [];
|
|
866
|
+
for (const msg of result.log.messages) {
|
|
867
|
+
parseMessages.push({
|
|
868
|
+
messageId: msg.messageId,
|
|
869
|
+
text: msg.unformattedText,
|
|
870
|
+
line: startLine
|
|
871
|
+
});
|
|
872
|
+
}
|
|
382
873
|
const summary = renderDocSection(comment.summarySection);
|
|
383
874
|
return {
|
|
384
875
|
summary: summary || void 0,
|
|
@@ -388,7 +879,8 @@ function parseTSDoc(rawComment, startLine) {
|
|
|
388
879
|
examples: examples.length > 0 ? examples : void 0,
|
|
389
880
|
tags: Object.keys(tags).length > 0 ? tags : void 0,
|
|
390
881
|
deprecated,
|
|
391
|
-
links: links.length > 0 ? links : void 0
|
|
882
|
+
links: links.length > 0 ? links : void 0,
|
|
883
|
+
parseMessages: parseMessages.length > 0 ? parseMessages : void 0
|
|
392
884
|
};
|
|
393
885
|
}
|
|
394
886
|
function getLeadingComment(node, sourceFile) {
|
|
@@ -439,9 +931,10 @@ function buildSignature(node, checker) {
|
|
|
439
931
|
return void 0;
|
|
440
932
|
}
|
|
441
933
|
}
|
|
442
|
-
function extractSymbolsFromFile(sourceFile, checker
|
|
934
|
+
function extractSymbolsFromFile(sourceFile, checker) {
|
|
443
935
|
const symbols = [];
|
|
444
936
|
const filePath = sourceFile.fileName;
|
|
937
|
+
const fileDir = dirname(filePath);
|
|
445
938
|
function visit(node, parentExported) {
|
|
446
939
|
const isExported = parentExported || (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0;
|
|
447
940
|
if (ts.isExportDeclaration(node)) {
|
|
@@ -458,7 +951,7 @@ function extractSymbolsFromFile(sourceFile, checker, _tsdocParser) {
|
|
|
458
951
|
const name2 = decl.name.getText(sourceFile);
|
|
459
952
|
const pos2 = sourceFile.getLineAndCharacterOfPosition(decl.getStart());
|
|
460
953
|
const rawComment2 = getLeadingComment(node, sourceFile);
|
|
461
|
-
const documentation2 = rawComment2 ? parseTSDoc(rawComment2, pos2.line + 1) : void 0;
|
|
954
|
+
const documentation2 = rawComment2 ? parseTSDoc(rawComment2, pos2.line + 1, fileDir) : void 0;
|
|
462
955
|
const tags2 = documentation2?.tags;
|
|
463
956
|
const visibility2 = resolveVisibility(tags2);
|
|
464
957
|
symbols.push({
|
|
@@ -488,7 +981,7 @@ function extractSymbolsFromFile(sourceFile, checker, _tsdocParser) {
|
|
|
488
981
|
const name = nameNode.getText(sourceFile);
|
|
489
982
|
const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
490
983
|
const rawComment = getLeadingComment(node, sourceFile);
|
|
491
|
-
const documentation = rawComment ? parseTSDoc(rawComment, pos.line + 1) : void 0;
|
|
984
|
+
const documentation = rawComment ? parseTSDoc(rawComment, pos.line + 1, fileDir) : void 0;
|
|
492
985
|
const tags = documentation?.tags;
|
|
493
986
|
const visibility = resolveVisibility(tags);
|
|
494
987
|
const children = [];
|
|
@@ -499,7 +992,7 @@ function extractSymbolsFromFile(sourceFile, checker, _tsdocParser) {
|
|
|
499
992
|
const memberName = member.name?.getText(sourceFile) ?? "";
|
|
500
993
|
const memberPos = sourceFile.getLineAndCharacterOfPosition(member.getStart());
|
|
501
994
|
const memberComment = getLeadingComment(member, sourceFile);
|
|
502
|
-
const memberDoc = memberComment ? parseTSDoc(memberComment, memberPos.line + 1) : void 0;
|
|
995
|
+
const memberDoc = memberComment ? parseTSDoc(memberComment, memberPos.line + 1, fileDir) : void 0;
|
|
503
996
|
const memberTags = memberDoc?.tags;
|
|
504
997
|
const memberVisibility = resolveVisibility(memberTags);
|
|
505
998
|
children.push({
|
|
@@ -535,7 +1028,7 @@ function createWalker(config) {
|
|
|
535
1028
|
return {
|
|
536
1029
|
walk() {
|
|
537
1030
|
const tsconfigPath = resolve2(config.tsconfig);
|
|
538
|
-
const configFile = ts.readConfigFile(tsconfigPath, (path) =>
|
|
1031
|
+
const configFile = ts.readConfigFile(tsconfigPath, (path) => readFileSync4(path, "utf8"));
|
|
539
1032
|
if (configFile.error) {
|
|
540
1033
|
throw new Error(
|
|
541
1034
|
`Failed to read tsconfig at ${tsconfigPath}: ${ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n")}`
|
|
@@ -551,14 +1044,12 @@ function createWalker(config) {
|
|
|
551
1044
|
options: parsedCommandLine.options
|
|
552
1045
|
});
|
|
553
1046
|
const checker = program.getTypeChecker();
|
|
554
|
-
const tsdocConfiguration = new TSDocConfiguration();
|
|
555
|
-
const tsdocParser = new TSDocParser(tsdocConfiguration);
|
|
556
1047
|
const allSymbols = [];
|
|
557
1048
|
for (const sourceFile of program.getSourceFiles()) {
|
|
558
1049
|
if (sourceFile.isDeclarationFile || sourceFile.fileName.includes("node_modules")) {
|
|
559
1050
|
continue;
|
|
560
1051
|
}
|
|
561
|
-
const fileSymbols = extractSymbolsFromFile(sourceFile, checker
|
|
1052
|
+
const fileSymbols = extractSymbolsFromFile(sourceFile, checker);
|
|
562
1053
|
allSymbols.push(...fileSymbols);
|
|
563
1054
|
}
|
|
564
1055
|
return allSymbols;
|
|
@@ -567,11 +1058,27 @@ function createWalker(config) {
|
|
|
567
1058
|
}
|
|
568
1059
|
export {
|
|
569
1060
|
Visibility,
|
|
1061
|
+
appendAuditEvent,
|
|
1062
|
+
clearTSDocConfigCache,
|
|
1063
|
+
createBypass,
|
|
1064
|
+
createLockManifest,
|
|
570
1065
|
createWalker,
|
|
571
1066
|
defaultConfig,
|
|
1067
|
+
expireOldBypasses,
|
|
572
1068
|
filterByVisibility,
|
|
1069
|
+
formatAuditEvent,
|
|
1070
|
+
getActiveBypasses,
|
|
1071
|
+
getCurrentUser,
|
|
1072
|
+
getRemainingBudget,
|
|
1073
|
+
isRuleBypassed,
|
|
573
1074
|
loadConfig,
|
|
1075
|
+
loadTSDocConfiguration,
|
|
574
1076
|
meetsVisibility,
|
|
575
|
-
|
|
1077
|
+
readAuditLog,
|
|
1078
|
+
readLockFile,
|
|
1079
|
+
removeLockFile,
|
|
1080
|
+
resolveVisibility,
|
|
1081
|
+
validateAgainstLock,
|
|
1082
|
+
writeLockFile
|
|
576
1083
|
};
|
|
577
1084
|
//# sourceMappingURL=index.js.map
|