pi-ultracode 0.1.2 → 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 +47 -42
- package/examples/workflows/loop-until-dry-bugs.workflow.js +16 -6
- package/extensions/ultracode.ts +0 -1
- package/package.json +7 -7
- package/src/commands.ts +19 -23
- package/src/index.ts +1 -1
- package/src/mode.ts +6 -40
- package/src/prompts.ts +18 -28
- package/src/workflow/admission.ts +318 -0
- package/src/workflow/agent-runner.ts +12 -0
- package/src/workflow/display.ts +17 -13
- package/src/workflow/journal.ts +0 -0
- package/src/workflow/json-schema.ts +39 -17
- package/src/workflow/leases.ts +60 -0
- package/src/workflow/parser.ts +809 -5
- package/src/workflow/registry.ts +39 -22
- package/src/workflow/run-artifacts.ts +231 -0
- package/src/workflow/run-details.ts +106 -28
- package/src/workflow/runtime.ts +640 -303
- package/src/workflow/script-executor.ts +317 -0
- package/src/workflow/script-worker.mjs +678 -0
- package/src/workflow/structured-output.ts +2 -0
- package/src/workflow/tool.ts +207 -74
- package/src/workflow/value-limits.ts +363 -0
- package/src/workflow/worktree.ts +5 -3
- package/types/workflow.d.ts +60 -22
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { AgentTelemetryEvent, AgentUsage, ThinkingLevel } from "./agent-runner.ts";
|
|
4
5
|
import { safeDisplayText, safeTranscriptText } from "./display-text.ts";
|
|
5
6
|
import { preview, recompute, type WorkflowAgentStatus, type WorkflowSnapshot } from "./display.ts";
|
|
7
|
+
import {
|
|
8
|
+
appendArtifactFile,
|
|
9
|
+
ensurePrivateArtifactDirectory,
|
|
10
|
+
readArtifactFile,
|
|
11
|
+
writeArtifactFile,
|
|
12
|
+
} from "./run-artifacts.ts";
|
|
6
13
|
|
|
7
14
|
export const RUN_DETAILS_VERSION = 1;
|
|
8
15
|
export const MAX_LIVE_TASK_BYTES = 1024 * 1024;
|
|
@@ -66,6 +73,7 @@ export interface WorkflowTimelineEvent {
|
|
|
66
73
|
|
|
67
74
|
export interface WorkflowTaskSummary {
|
|
68
75
|
id: number;
|
|
76
|
+
callPath?: string;
|
|
69
77
|
label: string;
|
|
70
78
|
phase?: string;
|
|
71
79
|
workflowPath: string[];
|
|
@@ -98,6 +106,7 @@ export interface WorkflowTaskDetail extends WorkflowTaskSummary {
|
|
|
98
106
|
|
|
99
107
|
export interface StartWorkflowTaskInput {
|
|
100
108
|
id: number;
|
|
109
|
+
callPath?: string;
|
|
101
110
|
label: string;
|
|
102
111
|
phase?: string;
|
|
103
112
|
workflowPath?: string[];
|
|
@@ -261,6 +270,7 @@ function cloneEvent(event: WorkflowTimelineEvent): WorkflowTimelineEvent {
|
|
|
261
270
|
function cloneSummary(task: InternalTask): WorkflowTaskSummary {
|
|
262
271
|
return {
|
|
263
272
|
id: task.id,
|
|
273
|
+
callPath: task.callPath,
|
|
264
274
|
label: task.label,
|
|
265
275
|
phase: task.phase,
|
|
266
276
|
workflowPath: [...task.workflowPath],
|
|
@@ -299,6 +309,7 @@ export class WorkflowRunDetails {
|
|
|
299
309
|
readonly manifestPath: string;
|
|
300
310
|
|
|
301
311
|
private readonly tasks = new Map<number, InternalTask>();
|
|
312
|
+
private readonly previousGenerationTasks = new Map<string, InternalTask>();
|
|
302
313
|
private readonly listeners = new Set<() => void>();
|
|
303
314
|
private readonly streamingLineColumns = new Map<string, number>();
|
|
304
315
|
private readonly artifactBudget: RunArtifactBudget;
|
|
@@ -311,24 +322,23 @@ export class WorkflowRunDetails {
|
|
|
311
322
|
this.runsDir = input.runsDir;
|
|
312
323
|
this.manifestPath = path.join(input.runsDir, `${input.runId}.details.json`);
|
|
313
324
|
const taskDir = this.taskDir();
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
} catch {
|
|
317
|
-
// Artifact persistence is best-effort; live details remain available.
|
|
318
|
-
}
|
|
325
|
+
ensurePrivateArtifactDirectory(this.runsDir);
|
|
326
|
+
ensurePrivateArtifactDirectory(taskDir);
|
|
319
327
|
this.artifactBudget = new RunArtifactBudget(taskDir, MAX_RUN_TRANSCRIPT_PAYLOAD_BYTES);
|
|
320
328
|
}
|
|
321
329
|
|
|
322
330
|
static restore(manifestPath: string): { details: WorkflowRunDetails; snapshot: WorkflowSnapshot } | undefined {
|
|
323
331
|
let manifest: WorkflowRunDetailsManifest;
|
|
324
332
|
try {
|
|
325
|
-
manifest = JSON.parse(
|
|
333
|
+
manifest = JSON.parse(readArtifactFile(manifestPath, "workflow details manifest", MAX_DETAILS_MANIFEST_BYTES)) as WorkflowRunDetailsManifest;
|
|
326
334
|
} catch {
|
|
327
335
|
return undefined;
|
|
328
336
|
}
|
|
337
|
+
const expectedFileName = `${manifest.runId}.details.json`;
|
|
329
338
|
if (
|
|
330
339
|
manifest.version !== RUN_DETAILS_VERSION
|
|
331
340
|
|| !/^wf_[a-z0-9_-]{1,120}$/i.test(manifest.runId ?? "")
|
|
341
|
+
|| path.basename(manifestPath) !== expectedFileName
|
|
332
342
|
|| typeof manifest.name !== "string"
|
|
333
343
|
|| !manifest.snapshot
|
|
334
344
|
) return undefined;
|
|
@@ -381,20 +391,46 @@ export class WorkflowRunDetails {
|
|
|
381
391
|
this.viewedTaskId = id;
|
|
382
392
|
}
|
|
383
393
|
|
|
394
|
+
/** Start a resume generation while retaining prior transcripts only for cache hits. */
|
|
395
|
+
beginGeneration(): void {
|
|
396
|
+
this.previousGenerationTasks.clear();
|
|
397
|
+
for (const task of this.tasks.values()) {
|
|
398
|
+
this.previousGenerationTasks.set(task.callPath ?? `id:${task.id}`, task);
|
|
399
|
+
}
|
|
400
|
+
this.tasks.clear();
|
|
401
|
+
this.streamingLineColumns.clear();
|
|
402
|
+
this.sequence = 0;
|
|
403
|
+
this.viewedTaskId = undefined;
|
|
404
|
+
this.notify();
|
|
405
|
+
}
|
|
406
|
+
|
|
384
407
|
startTask(input: StartWorkflowTaskInput): WorkflowTaskSummary {
|
|
385
|
-
const
|
|
408
|
+
const callPath = input.callPath ?? input.cachedRecord?.callPath ?? `id:${input.id}`;
|
|
409
|
+
const restored = this.tasks.get(input.id) ?? this.previousGenerationTasks.get(callPath);
|
|
386
410
|
if (input.cached && restored) {
|
|
411
|
+
restored.id = input.id;
|
|
412
|
+
restored.callPath = callPath;
|
|
413
|
+
restored.label = safeDisplayText(input.label, 120) || `agent ${input.id}`;
|
|
414
|
+
restored.phase = input.phase ? safeDisplayText(input.phase, 120) : undefined;
|
|
415
|
+
restored.workflowPath = (input.workflowPath?.length ? input.workflowPath : [this.name])
|
|
416
|
+
.map((part) => safeDisplayText(part, 120))
|
|
417
|
+
.filter(Boolean);
|
|
387
418
|
restored.status = "cached";
|
|
388
419
|
restored.cached = true;
|
|
389
420
|
restored.currentTurn = undefined;
|
|
421
|
+
this.previousGenerationTasks.delete(callPath);
|
|
422
|
+
this.tasks.set(input.id, restored);
|
|
423
|
+
this.sequence = Math.max(this.sequence, input.id);
|
|
390
424
|
this.notify();
|
|
391
425
|
return cloneSummary(restored);
|
|
392
426
|
}
|
|
427
|
+
this.previousGenerationTasks.delete(callPath);
|
|
393
428
|
|
|
394
429
|
const prompt = safeTranscriptText(input.prompt, MAX_TASK_TRANSCRIPT_BYTES);
|
|
395
430
|
const cached = input.cachedRecord;
|
|
396
431
|
const task: InternalTask = {
|
|
397
432
|
id: input.id,
|
|
433
|
+
callPath,
|
|
398
434
|
label: safeDisplayText(input.label, 120) || `agent ${input.id}`,
|
|
399
435
|
phase: input.phase ? safeDisplayText(input.phase, 120) : undefined,
|
|
400
436
|
workflowPath: (input.workflowPath?.length ? input.workflowPath : [this.name])
|
|
@@ -433,11 +469,14 @@ export class WorkflowRunDetails {
|
|
|
433
469
|
if (!input.cached) {
|
|
434
470
|
const writer = new BoundedTaskTranscriptWriter({
|
|
435
471
|
dir: this.taskDir(),
|
|
472
|
+
artifactKey: transcriptArtifactKey(callPath),
|
|
473
|
+
runId: this.runId,
|
|
474
|
+
callPath,
|
|
436
475
|
taskId: input.id,
|
|
437
476
|
budget: this.artifactBudget,
|
|
438
477
|
});
|
|
439
478
|
writer.reset();
|
|
440
|
-
this.persistPrompt(writer, input.id, prompt);
|
|
479
|
+
this.persistPrompt(writer, input.id, callPath, prompt);
|
|
441
480
|
task.writer = writer;
|
|
442
481
|
task.transcriptPath = writer.finalPath;
|
|
443
482
|
}
|
|
@@ -709,17 +748,21 @@ export class WorkflowRunDetails {
|
|
|
709
748
|
}
|
|
710
749
|
|
|
711
750
|
persist(snapshot: WorkflowSnapshot): void {
|
|
751
|
+
const persistedTasks = this.tasks.size === 0 && this.previousGenerationTasks.size > 0
|
|
752
|
+
? [...this.previousGenerationTasks.values()]
|
|
753
|
+
: [...this.tasks.values()];
|
|
754
|
+
persistedTasks.sort((a, b) => a.id - b.id);
|
|
712
755
|
const manifest: WorkflowRunDetailsManifest = {
|
|
713
756
|
version: RUN_DETAILS_VERSION,
|
|
714
757
|
runId: this.runId,
|
|
715
758
|
name: this.name,
|
|
716
759
|
updatedAt: Date.now(),
|
|
717
760
|
snapshot: snapshotForManifest(snapshot),
|
|
718
|
-
tasks:
|
|
761
|
+
tasks: persistedTasks.map(taskForManifest),
|
|
719
762
|
transcriptBytes: this.artifactBudget.usedBytes,
|
|
720
763
|
};
|
|
721
764
|
try {
|
|
722
|
-
|
|
765
|
+
ensurePrivateArtifactDirectory(this.runsDir);
|
|
723
766
|
let serialized = JSON.stringify(manifest, null, 2);
|
|
724
767
|
if (Buffer.byteLength(serialized, "utf8") > MAX_DETAILS_MANIFEST_BYTES) {
|
|
725
768
|
manifest.snapshot.logs = [];
|
|
@@ -738,9 +781,7 @@ export class WorkflowRunDetails {
|
|
|
738
781
|
serialized = JSON.stringify(manifest);
|
|
739
782
|
}
|
|
740
783
|
if (Buffer.byteLength(serialized, "utf8") > MAX_DETAILS_MANIFEST_BYTES) return;
|
|
741
|
-
|
|
742
|
-
fs.writeFileSync(temp, serialized, "utf8");
|
|
743
|
-
fs.renameSync(temp, this.manifestPath);
|
|
784
|
+
writeArtifactFile(this.manifestPath, serialized, { trustedRoot: this.runsDir, overwrite: true });
|
|
744
785
|
} catch {
|
|
745
786
|
// Persistence failure does not affect the workflow result.
|
|
746
787
|
}
|
|
@@ -782,7 +823,7 @@ export class WorkflowRunDetails {
|
|
|
782
823
|
if (persist) this.persistTimelineEvent(task, event);
|
|
783
824
|
}
|
|
784
825
|
|
|
785
|
-
private persistPrompt(writer: BoundedTaskTranscriptWriter, taskId: number, prompt: string): void {
|
|
826
|
+
private persistPrompt(writer: BoundedTaskTranscriptWriter, taskId: number, callPath: string, prompt: string): void {
|
|
786
827
|
const chunks = chunkText(safeMultiline(prompt), TRANSCRIPT_TEXT_CHUNK_CHARS);
|
|
787
828
|
for (const [chunkIndex, text] of chunks.entries()) {
|
|
788
829
|
writer.append({
|
|
@@ -790,6 +831,7 @@ export class WorkflowRunDetails {
|
|
|
790
831
|
recordType: "prompt",
|
|
791
832
|
runId: this.runId,
|
|
792
833
|
taskId,
|
|
834
|
+
callPath,
|
|
793
835
|
ts: Date.now(),
|
|
794
836
|
chunkIndex,
|
|
795
837
|
chunkCount: chunks.length,
|
|
@@ -814,6 +856,7 @@ export class WorkflowRunDetails {
|
|
|
814
856
|
recordType: "event",
|
|
815
857
|
runId: this.runId,
|
|
816
858
|
taskId: task.id,
|
|
859
|
+
callPath: task.callPath,
|
|
817
860
|
ts: Date.now(),
|
|
818
861
|
baseSeq: safeEvent.seq,
|
|
819
862
|
chunkIndex,
|
|
@@ -944,7 +987,7 @@ export class WorkflowRunDetails {
|
|
|
944
987
|
const resolved = path.resolve(task.transcriptPath);
|
|
945
988
|
const root = path.resolve(this.taskDir()) + path.sep;
|
|
946
989
|
if (!resolved.startsWith(root)) return;
|
|
947
|
-
content =
|
|
990
|
+
content = readArtifactFile(resolved, "workflow transcript", MAX_TASK_TRANSCRIPT_BYTES);
|
|
948
991
|
} catch {
|
|
949
992
|
return;
|
|
950
993
|
}
|
|
@@ -957,6 +1000,11 @@ export class WorkflowRunDetails {
|
|
|
957
1000
|
if (!line.trim()) continue;
|
|
958
1001
|
try {
|
|
959
1002
|
const record = JSON.parse(line) as any;
|
|
1003
|
+
if (
|
|
1004
|
+
record.version !== RUN_DETAILS_VERSION
|
|
1005
|
+
|| record.runId !== this.runId
|
|
1006
|
+
|| record.callPath !== task.callPath
|
|
1007
|
+
) continue;
|
|
960
1008
|
if (record.recordType === "prompt" && typeof record.text === "string") {
|
|
961
1009
|
const index = Number.isFinite(record.chunkIndex) ? record.chunkIndex : 0;
|
|
962
1010
|
promptChunks.set(index, record.text);
|
|
@@ -1013,7 +1061,7 @@ function restoreTaskSummary(value: unknown, fallbackName: string): WorkflowTaskS
|
|
|
1013
1061
|
if (!value || typeof value !== "object") return undefined;
|
|
1014
1062
|
const raw = value as Record<string, unknown>;
|
|
1015
1063
|
const id = Number.isInteger(raw.id) ? Number(raw.id) : 0;
|
|
1016
|
-
if (id < 1 || id >
|
|
1064
|
+
if (id < 1 || id > 1024) return undefined;
|
|
1017
1065
|
const statuses: WorkflowAgentStatus[] = ["running", "done", "error", "cancelled", "skipped", "cached"];
|
|
1018
1066
|
const status = statuses.includes(raw.status as WorkflowAgentStatus) ? raw.status as WorkflowAgentStatus : "cancelled";
|
|
1019
1067
|
const requestedEffort = normalizeThinkingLevel(raw.requestedEffort);
|
|
@@ -1023,6 +1071,7 @@ function restoreTaskSummary(value: unknown, fallbackName: string): WorkflowTaskS
|
|
|
1023
1071
|
: [fallbackName];
|
|
1024
1072
|
return taskForManifest({
|
|
1025
1073
|
id,
|
|
1074
|
+
callPath: typeof raw.callPath === "string" ? raw.callPath : undefined,
|
|
1026
1075
|
label: typeof raw.label === "string" ? raw.label : `agent ${id}`,
|
|
1027
1076
|
phase: typeof raw.phase === "string" ? raw.phase : undefined,
|
|
1028
1077
|
workflowPath,
|
|
@@ -1072,7 +1121,7 @@ function taskForManifest(task: WorkflowTaskSummary): WorkflowTaskSummary {
|
|
|
1072
1121
|
}
|
|
1073
1122
|
|
|
1074
1123
|
function snapshotForManifest(snapshot: WorkflowSnapshot): WorkflowSnapshot {
|
|
1075
|
-
|
|
1124
|
+
const projected: WorkflowSnapshot & { budgetTotal?: unknown } = {
|
|
1076
1125
|
...snapshot,
|
|
1077
1126
|
name: safeDisplayText(snapshot.name, 120),
|
|
1078
1127
|
description: snapshot.description ? safeDisplayText(snapshot.description, 240) : undefined,
|
|
@@ -1098,6 +1147,10 @@ function snapshotForManifest(snapshot: WorkflowSnapshot): WorkflowSnapshot {
|
|
|
1098
1147
|
// potentially huge or hostile serialization in the UI manifest.
|
|
1099
1148
|
result: undefined,
|
|
1100
1149
|
};
|
|
1150
|
+
// Drop the removed run-level token-budget field when restoring or persisting
|
|
1151
|
+
// legacy manifests without broad unknown-field rewriting.
|
|
1152
|
+
delete projected.budgetTotal;
|
|
1153
|
+
return projected;
|
|
1101
1154
|
}
|
|
1102
1155
|
|
|
1103
1156
|
class RunArtifactBudget {
|
|
@@ -1128,6 +1181,10 @@ class RunArtifactBudget {
|
|
|
1128
1181
|
}
|
|
1129
1182
|
}
|
|
1130
1183
|
|
|
1184
|
+
function transcriptArtifactKey(callPath: string): string {
|
|
1185
|
+
return crypto.createHash("sha256").update(callPath).digest("hex").slice(0, 32);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1131
1188
|
class BoundedTaskTranscriptWriter {
|
|
1132
1189
|
readonly finalPath: string;
|
|
1133
1190
|
readonly headPath: string;
|
|
@@ -1136,14 +1193,27 @@ class BoundedTaskTranscriptWriter {
|
|
|
1136
1193
|
private headBytes = 0;
|
|
1137
1194
|
private tailBytes = 0;
|
|
1138
1195
|
|
|
1139
|
-
constructor(input: {
|
|
1196
|
+
constructor(input: {
|
|
1197
|
+
dir: string;
|
|
1198
|
+
artifactKey: string;
|
|
1199
|
+
runId: string;
|
|
1200
|
+
callPath: string;
|
|
1201
|
+
taskId: number;
|
|
1202
|
+
budget: RunArtifactBudget;
|
|
1203
|
+
}) {
|
|
1140
1204
|
this.budget = input.budget;
|
|
1141
|
-
this.
|
|
1205
|
+
this.runId = input.runId;
|
|
1206
|
+
this.callPath = input.callPath;
|
|
1207
|
+
this.taskId = input.taskId;
|
|
1208
|
+
this.finalPath = path.join(input.dir, `${input.artifactKey}.transcript.jsonl`);
|
|
1142
1209
|
this.headPath = `${this.finalPath}.head`;
|
|
1143
1210
|
this.tailPath = `${this.finalPath}.tail`;
|
|
1144
1211
|
}
|
|
1145
1212
|
|
|
1146
1213
|
private readonly budget: RunArtifactBudget;
|
|
1214
|
+
private readonly runId: string;
|
|
1215
|
+
private readonly callPath: string;
|
|
1216
|
+
private readonly taskId: number;
|
|
1147
1217
|
|
|
1148
1218
|
reset(): void {
|
|
1149
1219
|
for (const target of [this.finalPath, this.headPath, this.tailPath]) {
|
|
@@ -1169,10 +1239,10 @@ class BoundedTaskTranscriptWriter {
|
|
|
1169
1239
|
}
|
|
1170
1240
|
try {
|
|
1171
1241
|
if (this.headBytes + bytes <= TRANSCRIPT_HEAD_BYTES) {
|
|
1172
|
-
|
|
1242
|
+
appendArtifactFile(this.headPath, line, { trustedRoot: path.dirname(this.finalPath) });
|
|
1173
1243
|
this.headBytes += bytes;
|
|
1174
1244
|
} else {
|
|
1175
|
-
|
|
1245
|
+
appendArtifactFile(this.tailPath, line, { trustedRoot: path.dirname(this.finalPath) });
|
|
1176
1246
|
this.tailBytes += bytes;
|
|
1177
1247
|
if (this.tailBytes > TRANSCRIPT_TAIL_BYTES) this.compactTail();
|
|
1178
1248
|
}
|
|
@@ -1190,6 +1260,9 @@ class BoundedTaskTranscriptWriter {
|
|
|
1190
1260
|
? `${JSON.stringify({
|
|
1191
1261
|
version: RUN_DETAILS_VERSION,
|
|
1192
1262
|
recordType: "omitted",
|
|
1263
|
+
runId: this.runId,
|
|
1264
|
+
taskId: this.taskId,
|
|
1265
|
+
callPath: this.callPath,
|
|
1193
1266
|
ts: Date.now(),
|
|
1194
1267
|
omittedBytes: this.omittedBytes,
|
|
1195
1268
|
message: `… ${formatBytes(this.omittedBytes)} of middle transcript omitted …`,
|
|
@@ -1201,10 +1274,11 @@ class BoundedTaskTranscriptWriter {
|
|
|
1201
1274
|
: "";
|
|
1202
1275
|
const combined = `${head}${marker}${tail}`;
|
|
1203
1276
|
try {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1277
|
+
ensurePrivateArtifactDirectory(path.dirname(this.finalPath));
|
|
1278
|
+
writeArtifactFile(this.finalPath, combined, {
|
|
1279
|
+
trustedRoot: path.dirname(this.finalPath),
|
|
1280
|
+
overwrite: true,
|
|
1281
|
+
});
|
|
1208
1282
|
fs.rmSync(this.headPath, { force: true });
|
|
1209
1283
|
fs.rmSync(this.tailPath, { force: true });
|
|
1210
1284
|
this.headBytes = 0;
|
|
@@ -1232,7 +1306,10 @@ class BoundedTaskTranscriptWriter {
|
|
|
1232
1306
|
}
|
|
1233
1307
|
const removed = Math.max(0, this.tailBytes - keptBytes);
|
|
1234
1308
|
try {
|
|
1235
|
-
|
|
1309
|
+
writeArtifactFile(this.tailPath, kept.join(""), {
|
|
1310
|
+
trustedRoot: path.dirname(this.finalPath),
|
|
1311
|
+
overwrite: true,
|
|
1312
|
+
});
|
|
1236
1313
|
this.tailBytes = keptBytes;
|
|
1237
1314
|
this.omittedBytes += removed;
|
|
1238
1315
|
this.budget.release(removed);
|
|
@@ -1255,7 +1332,7 @@ function isPathInside(candidate: string | undefined, rootDir: string): boolean {
|
|
|
1255
1332
|
|
|
1256
1333
|
function readText(filePath: string): string {
|
|
1257
1334
|
try {
|
|
1258
|
-
return
|
|
1335
|
+
return readArtifactFile(filePath, "workflow transcript", MAX_TASK_TRANSCRIPT_BYTES);
|
|
1259
1336
|
} catch {
|
|
1260
1337
|
return "";
|
|
1261
1338
|
}
|
|
@@ -1263,7 +1340,8 @@ function readText(filePath: string): string {
|
|
|
1263
1340
|
|
|
1264
1341
|
function fileSize(filePath: string): number {
|
|
1265
1342
|
try {
|
|
1266
|
-
|
|
1343
|
+
const stat = fs.lstatSync(filePath);
|
|
1344
|
+
return stat.isFile() && !stat.isSymbolicLink() ? stat.size : 0;
|
|
1267
1345
|
} catch {
|
|
1268
1346
|
return 0;
|
|
1269
1347
|
}
|