@zihanw/pi-forge 0.2.0 → 0.3.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/README.md +154 -104
- package/README.zh-CN.md +151 -103
- package/examples/default-prompt-stack.json +52 -93
- package/examples/reviewer-prompt-stack.json +115 -0
- package/examples/sillytavern-dm-writer-prompt-stack.json +190 -0
- package/package.json +3 -8
- package/src/compiler.ts +546 -175
- package/src/index.ts +320 -660
- package/src/loader.ts +141 -56
- package/src/payload-capture.ts +85 -0
- package/src/policy.ts +42 -0
- package/src/regex.ts +500 -0
- package/src/sillytavern-importer.ts +404 -19
- package/src/stack-migration.ts +159 -0
- package/src/storage.ts +28 -0
- package/src/types.ts +80 -38
- package/src/web-editor/index.ts +2 -0
- package/src/web-editor/page.ts +2844 -0
- package/src/web-editor/server.ts +289 -0
- package/src/web-editor/types.ts +84 -0
- package/src/web-host.ts +229 -0
- package/src/web-editor.ts +0 -1491
package/src/loader.ts
CHANGED
|
@@ -1,38 +1,51 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
|
+
import { validateRegexConfig } from "./regex.ts";
|
|
4
|
+
import { promptStackReadDirs } from "./storage.ts";
|
|
3
5
|
import type {
|
|
4
6
|
LoadedPromptStack,
|
|
7
|
+
PromptResourcePolicy,
|
|
5
8
|
PromptStack,
|
|
6
9
|
PromptStackDiagnostic,
|
|
7
10
|
PromptStackItem,
|
|
8
11
|
PromptStackRole,
|
|
9
|
-
PromptStateConfig,
|
|
10
|
-
PromptStateDefinition,
|
|
11
|
-
PromptStateScope,
|
|
12
|
-
PromptStateValue,
|
|
13
12
|
} from "./types.ts";
|
|
14
13
|
import { SUPPORTED_SLOTS } from "./types.ts";
|
|
15
14
|
|
|
16
15
|
const VALID_ROLES = new Set<PromptStackRole>(["system", "user", "assistant", "custom"]);
|
|
16
|
+
const VALID_CHAT_HISTORY_TOOL_MODES = new Set(["keep", "drop"]);
|
|
17
17
|
|
|
18
|
-
export
|
|
19
|
-
return join(cwd, ".pi", "prompt-stacks");
|
|
20
|
-
}
|
|
18
|
+
export { isInsidePromptStackStorage, legacyPromptStacksDir, promptStackPath, promptStackReadDirs, promptStacksDir } from "./storage.ts";
|
|
21
19
|
|
|
22
20
|
export function loadPromptStacks(cwd: string): LoadedPromptStack[] {
|
|
23
|
-
const
|
|
24
|
-
|
|
21
|
+
const files = promptStackFiles(cwd);
|
|
22
|
+
const loaded = files.map(loadPromptStackFile);
|
|
23
|
+
annotateDuplicateStackIds(loaded);
|
|
24
|
+
return loaded;
|
|
25
|
+
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
function promptStackFiles(cwd: string): string[] {
|
|
28
|
+
const files: string[] = [];
|
|
29
|
+
const shadowedNames = new Set<string>();
|
|
30
|
+
|
|
31
|
+
for (const dir of promptStackReadDirs(cwd)) {
|
|
32
|
+
if (!existsSync(dir)) continue;
|
|
33
|
+
|
|
34
|
+
let entries: string[];
|
|
35
|
+
try {
|
|
36
|
+
entries = readdirSync(dir).filter((name) => name.endsWith(".json"));
|
|
37
|
+
} catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const name of entries.sort()) {
|
|
42
|
+
if (shadowedNames.has(name)) continue;
|
|
43
|
+
shadowedNames.add(name);
|
|
44
|
+
files.push(join(dir, name));
|
|
45
|
+
}
|
|
31
46
|
}
|
|
32
47
|
|
|
33
|
-
|
|
34
|
-
annotateDuplicateStackIds(loaded);
|
|
35
|
-
return loaded;
|
|
48
|
+
return files;
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
function annotateDuplicateStackIds(stacks: LoadedPromptStack[]): void {
|
|
@@ -129,7 +142,7 @@ function normalizeStack(raw: unknown, filePath: string, diagnostics: PromptStack
|
|
|
129
142
|
|
|
130
143
|
const obj = raw as Record<string, unknown>;
|
|
131
144
|
const id = typeof obj.id === "string" && obj.id.trim() ? obj.id.trim() : basename(filePath, ".json");
|
|
132
|
-
const schemaVersion =
|
|
145
|
+
const schemaVersion = 1;
|
|
133
146
|
if (obj.schemaVersion !== 1) {
|
|
134
147
|
diagnostics.push({ level: "warning", message: "Missing or unsupported schemaVersion; assuming 1." });
|
|
135
148
|
}
|
|
@@ -139,6 +152,13 @@ function normalizeStack(raw: unknown, filePath: string, diagnostics: PromptStack
|
|
|
139
152
|
diagnostics.push({ level: "error", message: "Prompt stack must contain an items array." });
|
|
140
153
|
}
|
|
141
154
|
|
|
155
|
+
if (isPlainObject(obj.state) && Object.keys(obj.state).length > 0) {
|
|
156
|
+
diagnostics.push({
|
|
157
|
+
level: "info",
|
|
158
|
+
message: "state is no longer supported and was ignored; use stack.variables and template variable macros instead.",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
142
162
|
return {
|
|
143
163
|
schemaVersion,
|
|
144
164
|
type: obj.type === "pi-forge.prompt-stack" ? "pi-forge.prompt-stack" : undefined,
|
|
@@ -149,8 +169,10 @@ function normalizeStack(raw: unknown, filePath: string, diagnostics: PromptStack
|
|
|
149
169
|
mode: obj.mode === "append" || obj.mode === "prepend" || obj.mode === "replace" ? obj.mode : undefined,
|
|
150
170
|
defaults: isPlainObject(obj.defaults) ? (obj.defaults as PromptStack["defaults"]) : undefined,
|
|
151
171
|
context: isPlainObject(obj.context) ? (obj.context as PromptStack["context"]) : undefined,
|
|
172
|
+
tools: normalizeResourcePolicy(obj.tools, "tools", diagnostics),
|
|
173
|
+
skills: normalizeResourcePolicy(obj.skills, "skills", diagnostics),
|
|
152
174
|
variables: normalizeStringRecord(obj.variables),
|
|
153
|
-
|
|
175
|
+
regex: normalizeRegexConfig(obj.regex, diagnostics),
|
|
154
176
|
items,
|
|
155
177
|
import: isPlainObject(obj.import) ? (obj.import as Record<string, unknown>) : undefined,
|
|
156
178
|
};
|
|
@@ -213,6 +235,7 @@ export function validatePromptStack(stack: PromptStack): PromptStackDiagnostic[]
|
|
|
213
235
|
diagnostics.push({ level: "warning", message: `Unsupported slot: ${item.slot}`, itemId: item.id });
|
|
214
236
|
}
|
|
215
237
|
if (item.enabled !== false && item.slot === "chat-history") chatHistoryCount++;
|
|
238
|
+
if (item.slot === "chat-history") validateChatHistoryOptions(item, diagnostics);
|
|
216
239
|
}
|
|
217
240
|
|
|
218
241
|
if (item.kind === "block" && item.role !== "system" && item.enabled !== false && !item.role) {
|
|
@@ -232,15 +255,64 @@ export function validatePromptStack(stack: PromptStack): PromptStackDiagnostic[]
|
|
|
232
255
|
});
|
|
233
256
|
}
|
|
234
257
|
|
|
258
|
+
diagnostics.push(...validateResourcePolicy(stack.tools, "tools"));
|
|
259
|
+
diagnostics.push(...validateResourcePolicy(stack.skills, "skills"));
|
|
260
|
+
if ((stack.mode === "append" || stack.mode === "prepend") && hasPolicyEntries(stack.skills)) {
|
|
261
|
+
diagnostics.push({
|
|
262
|
+
level: "warning",
|
|
263
|
+
message: "skills policy only filters pi-forge skills slots. Use mode \"replace\" if you need the base Pi prompt to omit filtered skills.",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
diagnostics.push(...validateRegexConfig(stack.regex));
|
|
268
|
+
|
|
235
269
|
return diagnostics;
|
|
236
270
|
}
|
|
237
271
|
|
|
272
|
+
function validateChatHistoryOptions(
|
|
273
|
+
item: Extract<PromptStackItem, { kind: "slot" }>,
|
|
274
|
+
diagnostics: PromptStackDiagnostic[],
|
|
275
|
+
): void {
|
|
276
|
+
const options = item.options;
|
|
277
|
+
if (!options) return;
|
|
278
|
+
|
|
279
|
+
for (const key of ["includeLastUserMessage", "stripAssistantThinking", "includeSummaries"]) {
|
|
280
|
+
const value = options[key];
|
|
281
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
282
|
+
diagnostics.push({ level: "warning", message: `chat-history option ${key} should be a boolean.`, itemId: item.id });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (options.roles !== undefined && !isStringArray(options.roles)) {
|
|
287
|
+
diagnostics.push({ level: "error", message: "chat-history option roles must be an array of strings.", itemId: item.id });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (options.toolMode !== undefined && (typeof options.toolMode !== "string" || !VALID_CHAT_HISTORY_TOOL_MODES.has(options.toolMode))) {
|
|
291
|
+
diagnostics.push({ level: "error", message: 'chat-history option toolMode must be "keep" or "drop".', itemId: item.id });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const key of ["maxMessages", "maxChars"]) {
|
|
295
|
+
const value = options[key];
|
|
296
|
+
if (value !== undefined && !isPositiveInteger(value)) {
|
|
297
|
+
diagnostics.push({ level: "error", message: `chat-history option ${key} must be a positive integer.`, itemId: item.id });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
238
302
|
function normalizeId(value: unknown, fallback: string): string {
|
|
239
303
|
if (typeof value === "string" && value.trim()) return value.trim();
|
|
240
304
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
241
305
|
return fallback;
|
|
242
306
|
}
|
|
243
307
|
|
|
308
|
+
function isStringArray(value: unknown): value is string[] {
|
|
309
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function isPositiveInteger(value: unknown): value is number {
|
|
313
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
244
316
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
245
317
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
246
318
|
}
|
|
@@ -254,56 +326,69 @@ function normalizeStringRecord(value: unknown): Record<string, string> | undefin
|
|
|
254
326
|
return result;
|
|
255
327
|
}
|
|
256
328
|
|
|
257
|
-
function
|
|
329
|
+
function normalizeResourcePolicy(value: unknown, label: string, diagnostics: PromptStackDiagnostic[]): PromptResourcePolicy | undefined {
|
|
258
330
|
if (value === undefined) return undefined;
|
|
259
331
|
if (!isPlainObject(value)) {
|
|
260
|
-
diagnostics.push({ level: "
|
|
332
|
+
diagnostics.push({ level: "error", message: `${label} policy must be an object when provided.` });
|
|
261
333
|
return undefined;
|
|
262
334
|
}
|
|
335
|
+
const allow = normalizePolicyPatterns(value.allow, `${label}.allow`, diagnostics);
|
|
336
|
+
const deny = normalizePolicyPatterns(value.deny, `${label}.deny`, diagnostics);
|
|
337
|
+
if (allow && deny) {
|
|
338
|
+
diagnostics.push({ level: "error", message: `${label} policy must use either allow or deny, not both.` });
|
|
339
|
+
return { allow };
|
|
340
|
+
}
|
|
341
|
+
if (allow) return { allow };
|
|
342
|
+
if (deny) return { deny };
|
|
343
|
+
return {};
|
|
344
|
+
}
|
|
263
345
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
346
|
+
function normalizePolicyPatterns(value: unknown, label: string, diagnostics: PromptStackDiagnostic[]): string[] | undefined {
|
|
347
|
+
if (value === undefined) return undefined;
|
|
348
|
+
if (!Array.isArray(value)) {
|
|
349
|
+
diagnostics.push({ level: "error", message: `${label} must be an array of strings when provided.` });
|
|
350
|
+
return undefined;
|
|
269
351
|
}
|
|
270
352
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
353
|
+
const patterns: string[] = [];
|
|
354
|
+
for (const [index, item] of value.entries()) {
|
|
355
|
+
if (typeof item !== "string" || !item.trim()) {
|
|
356
|
+
diagnostics.push({ level: "error", message: `${label}[${index}] must be a non-empty string.` });
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
patterns.push(item.trim());
|
|
360
|
+
}
|
|
361
|
+
return patterns.length > 0 ? patterns : undefined;
|
|
362
|
+
}
|
|
277
363
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
364
|
+
function validateResourcePolicy(policy: PromptResourcePolicy | undefined, label: string): PromptStackDiagnostic[] {
|
|
365
|
+
const diagnostics: PromptStackDiagnostic[] = [];
|
|
366
|
+
if ((policy?.allow?.length ?? 0) > 0 && (policy?.deny?.length ?? 0) > 0) {
|
|
367
|
+
diagnostics.push({ level: "error", message: `${label} policy must use either allow or deny, not both.` });
|
|
368
|
+
}
|
|
369
|
+
for (const key of ["allow", "deny"] as const) {
|
|
370
|
+
const values = policy?.[key];
|
|
371
|
+
if (!values) continue;
|
|
372
|
+
const seen = new Set<string>();
|
|
373
|
+
for (const value of values) {
|
|
374
|
+
if (seen.has(value)) {
|
|
375
|
+
diagnostics.push({ level: "warning", message: `Duplicate ${label}.${key} pattern: ${value}.` });
|
|
287
376
|
}
|
|
288
|
-
|
|
377
|
+
seen.add(value);
|
|
289
378
|
}
|
|
290
379
|
}
|
|
291
|
-
|
|
292
|
-
return {
|
|
293
|
-
schemaVersion: value.schemaVersion === 1 ? 1 : undefined,
|
|
294
|
-
definitions: Object.keys(definitions).length > 0 ? definitions : undefined,
|
|
295
|
-
};
|
|
380
|
+
return diagnostics;
|
|
296
381
|
}
|
|
297
382
|
|
|
298
|
-
function
|
|
299
|
-
return
|
|
383
|
+
function hasPolicyEntries(policy: PromptResourcePolicy | undefined): boolean {
|
|
384
|
+
return !!policy && ((policy.allow?.length ?? 0) > 0 || (policy.deny?.length ?? 0) > 0);
|
|
300
385
|
}
|
|
301
386
|
|
|
302
|
-
function
|
|
303
|
-
if (value ===
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return
|
|
387
|
+
function normalizeRegexConfig(value: unknown, diagnostics: PromptStackDiagnostic[]): PromptStack["regex"] | undefined {
|
|
388
|
+
if (value === undefined) return undefined;
|
|
389
|
+
if (!isPlainObject(value)) {
|
|
390
|
+
diagnostics.push({ level: "warning", message: "regex must be an object when provided." });
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
return value as PromptStack["regex"];
|
|
309
394
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { WebEditorPayloadCapture } from "./web-editor/index.ts";
|
|
2
|
+
|
|
3
|
+
export function createProviderPayloadCapture(value: unknown, options: { stackId?: string; savePath?: string } = {}): WebEditorPayloadCapture {
|
|
4
|
+
const formatted = formatProviderPayload(value);
|
|
5
|
+
return {
|
|
6
|
+
capturedAt: new Date().toISOString(),
|
|
7
|
+
stackId: options.stackId,
|
|
8
|
+
savePath: options.savePath,
|
|
9
|
+
payload: formatted.payload,
|
|
10
|
+
text: formatted.text,
|
|
11
|
+
chars: formatted.chars,
|
|
12
|
+
approxTokens: formatted.approxTokens,
|
|
13
|
+
truncated: formatted.truncated,
|
|
14
|
+
error: formatted.error,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatProviderPayload(value: unknown): { payload?: unknown; text: string; chars: number; approxTokens: number; truncated: boolean; error?: string } {
|
|
19
|
+
try {
|
|
20
|
+
const payload = redactPayload(value);
|
|
21
|
+
const renderedJson = JSON.stringify(payload, null, 2);
|
|
22
|
+
const text = renderedJson === undefined ? String(payload) : renderedJson;
|
|
23
|
+
const maxChars = 200_000;
|
|
24
|
+
const truncated = text.length > maxChars;
|
|
25
|
+
const rendered = truncated ? `${text.slice(0, maxChars)}\n\n[pi-forge: payload truncated after ${maxChars} chars]` : text;
|
|
26
|
+
return {
|
|
27
|
+
payload: truncated ? undefined : payload,
|
|
28
|
+
text: rendered,
|
|
29
|
+
chars: rendered.length,
|
|
30
|
+
approxTokens: estimatePayloadTokens(rendered),
|
|
31
|
+
truncated,
|
|
32
|
+
};
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const text = `Failed to stringify provider payload: ${error instanceof Error ? error.message : String(error)}`;
|
|
35
|
+
return {
|
|
36
|
+
text,
|
|
37
|
+
chars: text.length,
|
|
38
|
+
approxTokens: estimatePayloadTokens(text),
|
|
39
|
+
truncated: false,
|
|
40
|
+
error: text,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function estimatePayloadTokens(payload: string): number {
|
|
46
|
+
return Math.max(1, Math.ceil(payload.length / 4));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function redactPayload(value: unknown, depth = 0): unknown {
|
|
50
|
+
if (depth > 8) return "[pi-forge: max depth reached]";
|
|
51
|
+
if (typeof value === "string") return redactLongString(value);
|
|
52
|
+
if (value === null || typeof value !== "object") return value;
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
const maxItems = 80;
|
|
55
|
+
const items = value.slice(0, maxItems).map((item) => redactPayload(item, depth + 1));
|
|
56
|
+
if (value.length > maxItems) items.push(`[pi-forge: ${value.length - maxItems} more items omitted]`);
|
|
57
|
+
return items;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result: Record<string, unknown> = {};
|
|
61
|
+
let count = 0;
|
|
62
|
+
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
63
|
+
if (++count > 120) {
|
|
64
|
+
result["[pi-forge: omitted]"] = "object has more than 120 keys";
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
if (isSecretKey(key)) {
|
|
68
|
+
result[key] = "[redacted]";
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
result[key] = redactPayload(raw, depth + 1);
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isSecretKey(key: string): boolean {
|
|
77
|
+
return /(api[-_]?key|authorization|bearer|token|secret|password|cookie|credential)/i.test(key);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function redactLongString(value: string): string {
|
|
81
|
+
if (/^data:image\//.test(value)) return "[image data omitted]";
|
|
82
|
+
if (value.length > 8_000 && /^[A-Za-z0-9+/=\r\n]+$/.test(value)) return `[base64-like data omitted: ${value.length} chars]`;
|
|
83
|
+
if (value.length > 12_000) return `${value.slice(0, 12_000)}\n[pi-forge: string truncated from ${value.length} chars]`;
|
|
84
|
+
return value;
|
|
85
|
+
}
|
package/src/policy.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PromptResourcePolicy } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export function hasResourcePolicy(policy: PromptResourcePolicy | undefined): boolean {
|
|
4
|
+
return !!policy && (hasEffectiveAllowPolicy(policy.allow) || hasPatterns(policy.deny));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function applyResourcePolicy(names: string[], policy: PromptResourcePolicy | undefined): string[] {
|
|
8
|
+
if (!hasResourcePolicy(policy)) return names;
|
|
9
|
+
if (hasEffectiveAllowPolicy(policy?.allow)) {
|
|
10
|
+
return names.filter((name) => matchesAnyPattern(name, policy.allow!));
|
|
11
|
+
}
|
|
12
|
+
if (hasPatterns(policy?.deny)) {
|
|
13
|
+
return names.filter((name) => !matchesAnyPattern(name, policy.deny!));
|
|
14
|
+
}
|
|
15
|
+
return names;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function matchesAnyPattern(name: string, patterns: string[]): boolean {
|
|
19
|
+
return patterns.some((pattern) => resourcePatternMatches(name, pattern));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resourcePatternMatches(name: string, pattern: string): boolean {
|
|
23
|
+
if (pattern === "*") return true;
|
|
24
|
+
if (!pattern.includes("*")) return name === pattern;
|
|
25
|
+
const escaped = pattern
|
|
26
|
+
.split("*")
|
|
27
|
+
.map(escapeRegExp)
|
|
28
|
+
.join(".*");
|
|
29
|
+
return new RegExp(`^${escaped}$`).test(name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasPatterns(value: string[] | undefined): value is string[] {
|
|
33
|
+
return Array.isArray(value) && value.length > 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasEffectiveAllowPolicy(value: string[] | undefined): value is string[] {
|
|
37
|
+
return hasPatterns(value) && value.some((pattern) => pattern !== "*");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function escapeRegExp(value: string): string {
|
|
41
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
42
|
+
}
|