mainflow 0.39.0 → 0.40.0-beta.1
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/cli.js +3 -3
- package/dist/hooks.d.ts +18 -0
- package/dist/hooks.js +89 -5
- package/dist/{host-CIOd2WSh.js → host-erQ_Sr8U.js} +1 -1
- package/dist/host.js +1 -1
- package/dist/{launch-DJ944-bI.js → launch-I_JDTAtU.js} +2 -2
- package/dist/launch.js +1 -1
- package/dist/native/mainflow-swap-darwin-arm64 +0 -0
- package/dist/native/mainflow-swap-darwin-x64 +0 -0
- package/dist/{operation-DJARDCM0.js → operation-Bf6DtnfD.js} +1 -1
- package/dist/{ui-BhwCZPJR.js → ui-CFtibt8x.js} +3 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { $ as stampMs, G as parseUntil, K as refExample, M as UNTIL_FORMS, W as parseRef, n as CLOSED_STATUS } from "./dist-zSabglPq.js";
|
|
3
|
-
import { $ as unavailableHubs, C as loadSessions,
|
|
3
|
+
import { $ as unavailableHubs, C as loadSessions, Et as WriteError, G as loadHub, H as checkHubName, K as sectionText, Q as remoteMembers, St as findHubRoot, Tt as DATA_DIR, U as HubError, V as applyOperation, X as memberOfGroup, b as duplicateClaimLines, bt as requireSlug, ct as MAX_FILE_BYTES, d as migrateHub, f as retroReport, h as RETRY_RULES, it as nodeStorage, m as receiverFromEnv, p as retroReportFromReceiver, tt as nowMinute, u as groupPlan, v as initHub, vt as MAINLINE_FILE, w as readSession, x as sessionBriefing, y as untilState, yt as TASK_FILE } from "./host-erQ_Sr8U.js";
|
|
4
4
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
5
|
-
import { C as ServerError, D as syncSettings, E as hubName, O as SETTINGS_FILE, S as ServerApi, T as earlierToken, _ as readDaemon, a as remoteMode, b as readRemote, c as planAnnounce, d as toolCallOf, f as startDaemon, g as probeDaemon, h as callDaemon, i as queuedCount, j as syncPaths, l as planFetch, m as DaemonUnavailable, n as hubWriter, o as fetchDenial, p as DaemonRunning, r as writtenLines, s as hookAnswer, t as CONFIRM_MS, u as planFlush, v as silentHint, w as ENV, x as oversizedFiles } from "./operation-
|
|
5
|
+
import { C as ServerError, D as syncSettings, E as hubName, O as SETTINGS_FILE, S as ServerApi, T as earlierToken, _ as readDaemon, a as remoteMode, b as readRemote, c as planAnnounce, d as toolCallOf, f as startDaemon, g as probeDaemon, h as callDaemon, i as queuedCount, j as syncPaths, l as planFetch, m as DaemonUnavailable, n as hubWriter, o as fetchDenial, p as DaemonRunning, r as writtenLines, s as hookAnswer, t as CONFIRM_MS, u as planFlush, v as silentHint, w as ENV, x as oversizedFiles } from "./operation-Bf6DtnfD.js";
|
|
6
6
|
import { COMMAND_SPECS, OPTIONS } from "./facts.js";
|
|
7
7
|
import { hookFallback, parsePayload, runHook } from "./hooks.js";
|
|
8
|
-
import { f as CLI_VERSION, i as launchUi, l as startUiInBackground, r as exitCodeFor, u as watchHubGone } from "./launch-
|
|
8
|
+
import { f as CLI_VERSION, i as launchUi, l as startUiInBackground, r as exitCodeFor, u as watchHubGone } from "./launch-I_JDTAtU.js";
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
10
|
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
11
11
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
package/dist/hooks.d.ts
CHANGED
|
@@ -574,6 +574,7 @@ export interface ClaudeCodePayload {
|
|
|
574
574
|
tool_name?: string;
|
|
575
575
|
tool_input?: {
|
|
576
576
|
file_path?: string;
|
|
577
|
+
notebook_path?: string;
|
|
577
578
|
command?: string;
|
|
578
579
|
};
|
|
579
580
|
}
|
|
@@ -617,6 +618,23 @@ export type HookFailure = {
|
|
|
617
618
|
} | {
|
|
618
619
|
kind: 'unreadable-input';
|
|
619
620
|
};
|
|
621
|
+
/** The first day the start gate applies; a task created earlier began before the lines below existed. */
|
|
622
|
+
export declare const START_GATE_FROM = "2026-10-19";
|
|
623
|
+
/** The kinds of change that need no acceptance scenarios, as a skip line names them. */
|
|
624
|
+
export declare const SCENARIO_SKIP_KINDS: readonly ['措辭或格式', '照抄或同步版號', '單一設定值', '一句話說得完'];
|
|
625
|
+
/** The two lines under a task's 紀錄 that let writing start: the scenarios were confirmed, or the change needs none. */
|
|
626
|
+
export declare const SCENARIO_LINES: {
|
|
627
|
+
readonly confirmed: {
|
|
628
|
+
readonly form: '- 驗收情境確認:{使用者怎麼回覆}';
|
|
629
|
+
readonly pattern: RegExp;
|
|
630
|
+
};
|
|
631
|
+
readonly skipped: {
|
|
632
|
+
readonly form: `- 驗收情境略過({${string} 之一}):{理由}`;
|
|
633
|
+
readonly pattern: RegExp;
|
|
634
|
+
};
|
|
635
|
+
};
|
|
636
|
+
/** Whether a task's 紀錄 holds a confirmation or a skip line as a top-level item; its lines are read untrimmed, so an indented one does not count. */
|
|
637
|
+
export declare function hasScenarioLine(body: string): boolean;
|
|
620
638
|
export declare const HOOK_PLATFORMS: Record<string, Record<string, Handler>>;
|
|
621
639
|
/** What an event prints when it cannot run its handler; null for the events that stay silent. */
|
|
622
640
|
export declare function hookFallback(platform: string, event: string, ctx: HookContext, failure: HookFailure): string | null;
|
package/dist/hooks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { P as configSchema, rt as describeRequest } from "./dist-zSabglPq.js";
|
|
2
|
-
import { A as headerLine, C as loadSessions, Ct as
|
|
3
|
-
import { n as hubWriter, r as writtenLines } from "./operation-
|
|
1
|
+
import { P as configSchema, W as parseRef, Y as taskSchema, rt as describeRequest } from "./dist-zSabglPq.js";
|
|
2
|
+
import { A as headerLine, C as loadSessions, Ct as worktreesOf, D as matchGrants, E as grantScope, G as loadHub, O as decorateReply, S as turnReminder, St as findHubRoot, Tt as DATA_DIR, U as HubError, _t as hubFormatVersion, et as taskFile, gt as formatMismatch, i as hostName, it as nodeStorage, j as mayDecorate, k as endsWithMarker, nt as readFrontMatter, rt as readYaml, w as readSession, x as sessionBriefing, xt as currentBranch } from "./host-erQ_Sr8U.js";
|
|
3
|
+
import { n as hubWriter, r as writtenLines } from "./operation-Bf6DtnfD.js";
|
|
4
4
|
import { resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
|
|
5
5
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
@@ -269,7 +269,7 @@ const CLAUDE_CODE_EVENTS = {
|
|
|
269
269
|
});
|
|
270
270
|
return null;
|
|
271
271
|
},
|
|
272
|
-
"pre-tool-use": decideByGrants,
|
|
272
|
+
"pre-tool-use": (root, context) => typeof context.payload.tool_name === "string" && WRITE_TOOLS.has(context.payload.tool_name) ? decideStartGate(root, context) : decideByGrants(root, context),
|
|
273
273
|
"post-tool-use": checkAfterWrite,
|
|
274
274
|
"message-display": displayReply
|
|
275
275
|
};
|
|
@@ -355,6 +355,90 @@ async function decideByGrants(root, { payload, warn, write }) {
|
|
|
355
355
|
await logGranted(write, scoped.scope, scan.call, resolved.request, grant, warn);
|
|
356
356
|
return permission("allow", `授權 ${grant.ref} 涵蓋 ${describeRequest(resolved.request)}`);
|
|
357
357
|
}
|
|
358
|
+
/** The first day the start gate applies; a task created earlier began before the lines below existed. */
|
|
359
|
+
const START_GATE_FROM = "2026-10-19";
|
|
360
|
+
/** The kinds of change that need no acceptance scenarios, as a skip line names them. */
|
|
361
|
+
const SCENARIO_SKIP_KINDS = [
|
|
362
|
+
"措辭或格式",
|
|
363
|
+
"照抄或同步版號",
|
|
364
|
+
"單一設定值",
|
|
365
|
+
"一句話說得完"
|
|
366
|
+
];
|
|
367
|
+
/** The two lines under a task's 紀錄 that let writing start: the scenarios were confirmed, or the change needs none. */
|
|
368
|
+
const SCENARIO_LINES = {
|
|
369
|
+
confirmed: {
|
|
370
|
+
form: "- 驗收情境確認:{使用者怎麼回覆}",
|
|
371
|
+
pattern: /^- 驗收情境確認:\S/
|
|
372
|
+
},
|
|
373
|
+
skipped: {
|
|
374
|
+
form: `- 驗收情境略過({${SCENARIO_SKIP_KINDS.join("、")} 之一}):{理由}`,
|
|
375
|
+
pattern: new RegExp(`^- 驗收情境略過((?:${SCENARIO_SKIP_KINDS.join("|")})):\\S`)
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
/** Whether a task's 紀錄 holds a confirmation or a skip line as a top-level item; its lines are read untrimmed, so an indented one does not count. */
|
|
379
|
+
function hasScenarioLine(body) {
|
|
380
|
+
const lines = body.split("\n");
|
|
381
|
+
const start = lines.findIndex((l) => l.trim() === "## 紀錄");
|
|
382
|
+
if (start === -1) return false;
|
|
383
|
+
const end = lines.findIndex((l, i) => i > start && l.startsWith("## "));
|
|
384
|
+
return lines.slice(start + 1, end === -1 ? void 0 : end).some((line) => SCENARIO_LINES.confirmed.pattern.test(line) || SCENARIO_LINES.skipped.pattern.test(line));
|
|
385
|
+
}
|
|
386
|
+
/** Whether a path lies under `tmp/` at the root of the hub or of a repository config.yaml lists. */
|
|
387
|
+
function inProjectTmp(file, root) {
|
|
388
|
+
const repos = readYaml(join(root, DATA_DIR, "config.yaml"), configSchema).data.repos;
|
|
389
|
+
return [root, ...Object.values(repos).map((path) => resolve(root, path))].some((dir) => relative(dir, file).startsWith(`tmp/`));
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* The open tasks of this hub that the session's actions in progress serve, created on or after START_GATE_FROM,
|
|
393
|
+
* whose 紀錄 has neither line. A task that cannot be read is left out: the gate only stops what it can show is missing.
|
|
394
|
+
*/
|
|
395
|
+
function unconfirmedTasks(root, session) {
|
|
396
|
+
const tasks = /* @__PURE__ */ new Set();
|
|
397
|
+
for (const action of session.actions) {
|
|
398
|
+
if (action.status !== "doing") continue;
|
|
399
|
+
for (const ref of action.serves) {
|
|
400
|
+
const parsed = parseRef(ref);
|
|
401
|
+
if ((parsed?.kind === "task" || parsed?.kind === "checklistItem") && parsed.hub === void 0) tasks.add(`${parsed.mainline}/${parsed.task}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return [...tasks].filter((ref) => {
|
|
405
|
+
const [mainline, task] = ref.split("/");
|
|
406
|
+
try {
|
|
407
|
+
const { data, body } = readFrontMatter(taskFile(root, mainline, task), taskSchema);
|
|
408
|
+
return data.created >= "2026-10-19" && data.status !== "done" && data.status !== "dropped" && !hasScenarioLine(body);
|
|
409
|
+
} catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* PreToolUse for the tools that write a file: deny the write while a task the session's action in progress
|
|
416
|
+
* serves has neither a confirmation nor a skip line under 紀錄, so code waits for its acceptance scenarios.
|
|
417
|
+
* Writes under `.mainflow/` or a project's `tmp/`, a session with no record or no such task, print nothing. A
|
|
418
|
+
* subagent's call carries the main session's session_id, so it is judged by the main session's action. Any failure
|
|
419
|
+
* stays silent, since the fallback only answers Bash, and the write goes on.
|
|
420
|
+
*/
|
|
421
|
+
function decideStartGate(root, { payload, cli }) {
|
|
422
|
+
const path = payload.tool_input?.file_path ?? payload.tool_input?.notebook_path;
|
|
423
|
+
const sessionId = payload.session_id;
|
|
424
|
+
if (typeof path !== "string" || !path || typeof sessionId !== "string" || !sessionId) return null;
|
|
425
|
+
const file = isAbsolute(path) ? path : resolve(typeof payload.cwd === "string" ? payload.cwd : root, path);
|
|
426
|
+
if (inMainflowData(file) || inProjectTmp(file, root)) return null;
|
|
427
|
+
const session = readSession(root, claudeCodeSessionId(sessionId));
|
|
428
|
+
if (!session) return null;
|
|
429
|
+
const missing = unconfirmedTasks(root, session);
|
|
430
|
+
if (missing.length === 0) return null;
|
|
431
|
+
const reason = [
|
|
432
|
+
`${missing.join("、")} 的「紀錄」沒有驗收情境的確認行,也沒有略過行,所以先不寫檔。`,
|
|
433
|
+
`使用者確認「完成定義」的驗收情境後,用 ${cli} section append <m/t> 紀錄 加一行「${SCENARIO_LINES.confirmed.form}」。`,
|
|
434
|
+
`改動屬於 ${SCENARIO_SKIP_KINDS.join("、")} 這四類時,改加「${SCENARIO_LINES.skipped.form}」。`
|
|
435
|
+
].join("");
|
|
436
|
+
return JSON.stringify({ hookSpecificOutput: {
|
|
437
|
+
hookEventName: "PreToolUse",
|
|
438
|
+
permissionDecision: "deny",
|
|
439
|
+
permissionDecisionReason: `[mainflow] ${reason}`
|
|
440
|
+
} });
|
|
441
|
+
}
|
|
358
442
|
/** How many problems outside the written file the reminder lists before it only gives the count. */
|
|
359
443
|
const OTHER_PROBLEMS_SHOWN = 5;
|
|
360
444
|
const problemLine = (p) => `- ${p.file}${p.field ? ` › ${p.field}` : ""}:${p.message}`;
|
|
@@ -498,4 +582,4 @@ async function runHook(platform, event, ctx) {
|
|
|
498
582
|
}
|
|
499
583
|
}
|
|
500
584
|
//#endregion
|
|
501
|
-
export { HOOK_PLATFORMS, claudeCodeSessionId, hookFallback, openLink, parsePayload, runHook, subagentTranscript };
|
|
585
|
+
export { HOOK_PLATFORMS, SCENARIO_LINES, SCENARIO_SKIP_KINDS, START_GATE_FROM, claudeCodeSessionId, hasScenarioLine, hookFallback, openLink, parsePayload, runHook, subagentTranscript };
|
|
@@ -8430,4 +8430,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
|
|
|
8430
8430
|
return true;
|
|
8431
8431
|
}
|
|
8432
8432
|
//#endregion
|
|
8433
|
-
export { unavailableHubs as $, headerLine as A, scopeOfPath as B, loadSessions as C,
|
|
8433
|
+
export { unavailableHubs as $, headerLine as A, scopeOfPath as B, loadSessions as C, worktreesOf as Ct, matchGrants as D, contentHash as Dt, grantScope as E, WriteError as Et, applyOffline as F, loadHub as G, checkHubName as H, renumberRequest as I, mainlineEdges as J, sectionText as K, renumberFiles as L, attributeTrace as M, traceTranscript as N, decorateReply as O, splitFrontMatter as Ot, transcriptTitle as P, remoteMembers as Q, renumberPath as R, turnReminder as S, findHubRoot as St, transcriptPath as T, DATA_DIR as Tt, HubError as U, applyOperation as V, loadConfig as W, memberOfGroup as X, linksInto as Y, refIndex as Z, setArchivedMany as _, hubFormatVersion as _t, openInApp as a, ACCESS_HEADERS as at, duplicateClaimLines as b, requireSlug as bt, siteBlock as c, MAX_FILE_BYTES as ct, migrateHub as d, comparePaths as dt, taskFile as et, retroReport as f, exceedsFileLimit as ft, ARCHIVE_KINDS as g, formatMismatch as gt, RETRY_RULES as h, textBytes as ht, hostName as i, nodeStorage as it, mayDecorate as j, endsWithMarker as k, syncHost as l, ROUTES as lt, receiverFromEnv as m, requestTime as mt, DEFAULT_UI_PORT as n, readFrontMatter as nt, removeDanglingLinks as o, BLOB_HASH as ot, retroReportFromReceiver as p, readsAsText as pt, dependencyGraph as q, caddySites as r, readYaml as rt, removeHostLink as s, MAX_BLOB_BYTES as st, APP_BUNDLE_ID as t, nowMinute as tt, groupPlan as u, SOCKET_MAX_AGE_CLOSE as ut, initHub as v, MAINLINE_FILE as vt, readSession as w, ConflictError as wt, sessionBriefing as x, currentBranch as xt, untilState as y, TASK_FILE as yt, renumberText as z };
|
package/dist/host.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as openInApp, c as siteBlock, i as hostName, l as syncHost, n as DEFAULT_UI_PORT, o as removeDanglingLinks, r as caddySites, s as removeHostLink, t as APP_BUNDLE_ID } from "./host-
|
|
1
|
+
import { a as openInApp, c as siteBlock, i as hostName, l as syncHost, n as DEFAULT_UI_PORT, o as removeDanglingLinks, r as caddySites, s as removeHostLink, t as APP_BUNDLE_ID } from "./host-erQ_Sr8U.js";
|
|
2
2
|
export { APP_BUNDLE_ID, DEFAULT_UI_PORT, caddySites, hostName, openInApp, removeDanglingLinks, removeHostLink, siteBlock, syncHost };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Et as WriteError, Tt as DATA_DIR, i as hostName, l as syncHost, n as DEFAULT_UI_PORT, s as removeHostLink } from "./host-erQ_Sr8U.js";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -276,7 +276,7 @@ async function launchUi(root, hub, configured, options) {
|
|
|
276
276
|
}
|
|
277
277
|
let server;
|
|
278
278
|
if (!reuse) {
|
|
279
|
-
const { serveUi } = await import("./ui-
|
|
279
|
+
const { serveUi } = await import("./ui-CFtibt8x.js");
|
|
280
280
|
server = await serveUi(root, choice.port);
|
|
281
281
|
writeUiRecord(root, {
|
|
282
282
|
port: choice.port,
|
package/dist/launch.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as networkProbe, c as realRoot, d as withUiLock, i as launchUi, l as startUiInBackground, n as choosePort, o as portCandidates, r as exitCodeFor, s as readUiRecord, t as PORT_SPAN, u as watchHubGone } from "./launch-
|
|
1
|
+
import { a as networkProbe, c as realRoot, d as withUiLock, i as launchUi, l as startUiInBackground, n as choosePort, o as portCandidates, r as exitCodeFor, s as readUiRecord, t as PORT_SPAN, u as watchHubGone } from "./launch-I_JDTAtU.js";
|
|
2
2
|
export { PORT_SPAN, choosePort, exitCodeFor, launchUi, networkProbe, portCandidates, readUiRecord, realRoot, startUiInBackground, watchHubGone, withUiLock };
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as scopeOfPath,
|
|
1
|
+
import { B as scopeOfPath, Dt as contentHash, Et as WriteError, F as applyOffline, I as renumberRequest, L as renumberFiles, Ot as splitFrontMatter, R as renumberPath, Tt as DATA_DIR, V as applyOperation, at as ACCESS_HEADERS, ct as MAX_FILE_BYTES, dt as comparePaths, ft as exceedsFileLimit, ht as textBytes, it as nodeStorage, lt as ROUTES, mt as requestTime, ot as BLOB_HASH, pt as readsAsText, st as MAX_BLOB_BYTES, tt as nowMinute, ut as SOCKET_MAX_AGE_CLOSE, wt as ConflictError, z as renumberText } from "./host-erQ_Sr8U.js";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { accessSync, appendFileSync, chmodSync, closeSync, constants, copyFileSync, createReadStream, createWriteStream, existsSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { P as configSchema } from "./dist-zSabglPq.js";
|
|
2
|
-
import { C as loadSessions,
|
|
2
|
+
import { C as loadSessions, Et as WriteError, G as loadHub, J as mainlineEdges, K as sectionText, M as attributeTrace, N as traceTranscript, P as transcriptTitle, Q as remoteMembers, T as transcriptPath, Tt as DATA_DIR, U as HubError, W as loadConfig, Y as linksInto, Z as refIndex, _ as setArchivedMany, a as openInApp, g as ARCHIVE_KINDS, q as dependencyGraph, rt as readYaml, u as groupPlan, w as readSession, wt as ConflictError } from "./host-erQ_Sr8U.js";
|
|
3
3
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
4
|
-
import { A as isSynced, _ as readDaemon, a as remoteMode, b as readRemote, h as callDaemon, j as syncPaths, k as SYNC_DIR, m as DaemonUnavailable, n as hubWriter, r as writtenLines, y as watchTree } from "./operation-
|
|
5
|
-
import { c as realRoot, f as CLI_VERSION, s as readUiRecord } from "./launch-
|
|
4
|
+
import { A as isSynced, _ as readDaemon, a as remoteMode, b as readRemote, h as callDaemon, j as syncPaths, k as SYNC_DIR, m as DaemonUnavailable, n as hubWriter, r as writtenLines, y as watchTree } from "./operation-Bf6DtnfD.js";
|
|
5
|
+
import { c as realRoot, f as CLI_VERSION, s as readUiRecord } from "./launch-I_JDTAtU.js";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { closeSync, existsSync, lstatSync, openSync, readFileSync, readSync, realpathSync, statSync } from "node:fs";
|
|
8
8
|
import { basename, dirname, extname, join, normalize, relative, resolve, sep } from "node:path";
|