facult 2.14.0 → 2.15.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/README.md +5 -6
- package/assets/packs/facult-operating-model/{AGENTS.global.md → snippets/templates/agents-global.md} +4 -4
- package/docs/README.md +1 -0
- package/docs/built-in-pack.md +11 -3
- package/docs/codex-plugin.md +57 -0
- package/docs/pack-upgrades.md +7 -1
- package/docs/roadmap.md +4 -3
- package/package.json +6 -1
- package/plugins/fclt/.codex-plugin/plugin.json +31 -0
- package/plugins/fclt/.mcp.json +11 -0
- package/plugins/fclt/scripts/fclt-mcp.cjs +321 -0
- package/plugins/fclt/skills/fclt-capability-review/SKILL.md +51 -0
- package/plugins/fclt/skills/fclt-evolution/SKILL.md +65 -0
- package/plugins/fclt/skills/fclt-setup/SKILL.md +65 -0
- package/plugins/fclt/skills/fclt-writeback/SKILL.md +57 -0
- package/src/builtin-assets.ts +6 -1
- package/src/builtin.ts +56 -1
- package/src/doctor.ts +5 -2
- package/src/global-docs.ts +6 -2
- package/src/manage.ts +116 -15
- package/src/remote.ts +112 -10
package/src/manage.ts
CHANGED
|
@@ -15,7 +15,11 @@ import { basename, dirname, join } from "node:path";
|
|
|
15
15
|
import { getAdapter } from "./adapters";
|
|
16
16
|
import { renderCanonicalText } from "./agents";
|
|
17
17
|
import { ensureAiIndexPath } from "./ai-state";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
builtinSyncDefaultsEnabled,
|
|
20
|
+
facultBuiltinCodexPluginRoot,
|
|
21
|
+
facultBuiltinPackRoot,
|
|
22
|
+
} from "./builtin";
|
|
19
23
|
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
20
24
|
import { renderBullets, renderCode, renderPage } from "./cli-ui";
|
|
21
25
|
import { contentHash, normalizeText } from "./conflicts";
|
|
@@ -86,6 +90,8 @@ export interface ManagedState {
|
|
|
86
90
|
|
|
87
91
|
type SyncLedgerAction = "add" | "adopt" | "remove" | "skip" | "write";
|
|
88
92
|
|
|
93
|
+
const FCLT_CODEX_PLUGIN_NAME = "fclt";
|
|
94
|
+
|
|
89
95
|
interface SyncLedgerEvent {
|
|
90
96
|
version: 1;
|
|
91
97
|
id: string;
|
|
@@ -259,7 +265,8 @@ async function isGeneratedOnlyProjectRoot(args: {
|
|
|
259
265
|
function renderedSourceKindForPath(
|
|
260
266
|
sourcePath: string
|
|
261
267
|
): ManagedRenderedTargetState["sourceKind"] {
|
|
262
|
-
return sourcePath.startsWith(facultBuiltinPackRoot())
|
|
268
|
+
return sourcePath.startsWith(facultBuiltinPackRoot()) ||
|
|
269
|
+
sourcePath.startsWith(facultBuiltinCodexPluginRoot())
|
|
263
270
|
? "builtin"
|
|
264
271
|
: "canonical";
|
|
265
272
|
}
|
|
@@ -328,6 +335,45 @@ function normalizeCodexMarketplaceText(text: string): string {
|
|
|
328
335
|
}
|
|
329
336
|
}
|
|
330
337
|
|
|
338
|
+
function fcltCodexMarketplaceEntry(): Record<string, unknown> {
|
|
339
|
+
return {
|
|
340
|
+
name: FCLT_CODEX_PLUGIN_NAME,
|
|
341
|
+
source: {
|
|
342
|
+
source: "local",
|
|
343
|
+
path: `./plugins/${FCLT_CODEX_PLUGIN_NAME}`,
|
|
344
|
+
},
|
|
345
|
+
policy: {
|
|
346
|
+
installation: "AVAILABLE",
|
|
347
|
+
authentication: "NONE",
|
|
348
|
+
},
|
|
349
|
+
category: "Productivity",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function withBuiltinFcltCodexMarketplaceEntry(text: string | null): string {
|
|
354
|
+
const base =
|
|
355
|
+
text == null
|
|
356
|
+
? {
|
|
357
|
+
name: "local",
|
|
358
|
+
interface: { displayName: "Local Plugins" },
|
|
359
|
+
plugins: [],
|
|
360
|
+
}
|
|
361
|
+
: (JSON.parse(text) as unknown);
|
|
362
|
+
if (!isPlainObject(base)) {
|
|
363
|
+
return text == null ? normalizeCodexMarketplaceText("{}") : text;
|
|
364
|
+
}
|
|
365
|
+
const plugins = Array.isArray(base.plugins) ? [...base.plugins] : [];
|
|
366
|
+
const hasFclt = plugins.some(
|
|
367
|
+
(entry) => isPlainObject(entry) && entry.name === FCLT_CODEX_PLUGIN_NAME
|
|
368
|
+
);
|
|
369
|
+
if (!hasFclt) {
|
|
370
|
+
plugins.push(fcltCodexMarketplaceEntry());
|
|
371
|
+
}
|
|
372
|
+
return normalizeCodexMarketplaceText(
|
|
373
|
+
JSON.stringify({ ...base, plugins }, null, 2)
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
331
377
|
function isSafeCodexPluginName(name: string): boolean {
|
|
332
378
|
const trimmed = name.trim();
|
|
333
379
|
return (
|
|
@@ -899,7 +945,8 @@ async function listRelativeFilesWithDotfiles(root: string): Promise<string[]> {
|
|
|
899
945
|
}
|
|
900
946
|
|
|
901
947
|
async function loadCanonicalCodexPlugins(
|
|
902
|
-
rootDir: string
|
|
948
|
+
rootDir: string,
|
|
949
|
+
homeDir?: string
|
|
903
950
|
): Promise<CanonicalPluginEntry[]> {
|
|
904
951
|
const pluginsRoot = codexCanonicalPluginsRoot(rootDir);
|
|
905
952
|
const entries = await readdir(pluginsRoot, { withFileTypes: true }).catch(
|
|
@@ -907,36 +954,81 @@ async function loadCanonicalCodexPlugins(
|
|
|
907
954
|
);
|
|
908
955
|
const out: CanonicalPluginEntry[] = [];
|
|
909
956
|
|
|
957
|
+
async function loadEntry(
|
|
958
|
+
name: string,
|
|
959
|
+
sourceDir: string
|
|
960
|
+
): Promise<CanonicalPluginEntry | null> {
|
|
961
|
+
if (!(await fileExists(join(sourceDir, ".codex-plugin", "plugin.json")))) {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
const files = new Map<string, Uint8Array>();
|
|
965
|
+
for (const relPath of await listRelativeFilesWithDotfiles(sourceDir)) {
|
|
966
|
+
files.set(relPath, await Bun.file(join(sourceDir, relPath)).bytes());
|
|
967
|
+
}
|
|
968
|
+
return { name, sourceDir, files };
|
|
969
|
+
}
|
|
970
|
+
|
|
910
971
|
for (const entry of entries) {
|
|
911
972
|
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
912
973
|
continue;
|
|
913
974
|
}
|
|
914
975
|
const sourceDir = join(pluginsRoot, entry.name);
|
|
915
|
-
|
|
916
|
-
|
|
976
|
+
const plugin = await loadEntry(entry.name, sourceDir);
|
|
977
|
+
if (plugin) {
|
|
978
|
+
out.push(plugin);
|
|
917
979
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (
|
|
983
|
+
(await builtinSyncDefaultsEnabled(rootDir, homeDir)) &&
|
|
984
|
+
!out.some((entry) => entry.name === FCLT_CODEX_PLUGIN_NAME)
|
|
985
|
+
) {
|
|
986
|
+
const plugin = await loadEntry(
|
|
987
|
+
FCLT_CODEX_PLUGIN_NAME,
|
|
988
|
+
facultBuiltinCodexPluginRoot()
|
|
989
|
+
);
|
|
990
|
+
if (plugin) {
|
|
991
|
+
out.push(plugin);
|
|
921
992
|
}
|
|
922
|
-
out.push({ name: entry.name, sourceDir, files });
|
|
923
993
|
}
|
|
924
994
|
|
|
925
995
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
926
996
|
}
|
|
927
997
|
|
|
928
|
-
async function canonicalCodexPluginsExist(
|
|
998
|
+
async function canonicalCodexPluginsExist(
|
|
999
|
+
rootDir: string,
|
|
1000
|
+
homeDir?: string
|
|
1001
|
+
): Promise<boolean> {
|
|
929
1002
|
if (await fileExists(codexCanonicalPluginMarketplacePath(rootDir))) {
|
|
930
1003
|
return true;
|
|
931
1004
|
}
|
|
932
|
-
return (await loadCanonicalCodexPlugins(rootDir)).length > 0;
|
|
1005
|
+
return (await loadCanonicalCodexPlugins(rootDir, homeDir)).length > 0;
|
|
933
1006
|
}
|
|
934
1007
|
|
|
935
1008
|
async function loadCanonicalCodexMarketplaceText(
|
|
936
|
-
rootDir: string
|
|
1009
|
+
rootDir: string,
|
|
1010
|
+
homeDir?: string
|
|
937
1011
|
): Promise<{ text: string | null; sourcePath: string }> {
|
|
938
1012
|
const sourcePath = codexCanonicalPluginMarketplacePath(rootDir);
|
|
939
1013
|
const raw = await readTextOrNull(sourcePath);
|
|
1014
|
+
const builtinEnabled = await builtinSyncDefaultsEnabled(rootDir, homeDir);
|
|
1015
|
+
if (builtinEnabled) {
|
|
1016
|
+
try {
|
|
1017
|
+
return {
|
|
1018
|
+
text: withBuiltinFcltCodexMarketplaceEntry(raw),
|
|
1019
|
+
sourcePath:
|
|
1020
|
+
raw == null
|
|
1021
|
+
? join(
|
|
1022
|
+
facultBuiltinCodexPluginRoot(),
|
|
1023
|
+
".codex-plugin",
|
|
1024
|
+
"plugin.json"
|
|
1025
|
+
)
|
|
1026
|
+
: sourcePath,
|
|
1027
|
+
};
|
|
1028
|
+
} catch {
|
|
1029
|
+
// Fall back to the canonical marketplace text if it is not JSON.
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
940
1032
|
return {
|
|
941
1033
|
text: raw == null ? null : normalizeCodexMarketplaceText(raw),
|
|
942
1034
|
sourcePath,
|
|
@@ -2919,6 +3011,7 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2919
3011
|
const pluginPreview =
|
|
2920
3012
|
tool === "codex" && toolPaths.pluginsDir && toolPaths.pluginMarketplacePath
|
|
2921
3013
|
? await planCodexPluginFileChanges({
|
|
3014
|
+
homeDir: home,
|
|
2922
3015
|
rootDir,
|
|
2923
3016
|
pluginsDir: toolPaths.pluginsDir,
|
|
2924
3017
|
pluginMarketplacePath: toolPaths.pluginMarketplacePath,
|
|
@@ -3390,7 +3483,7 @@ async function repairManagedToolEntry(args: {
|
|
|
3390
3483
|
!(next.pluginsDir && next.pluginMarketplacePath) &&
|
|
3391
3484
|
toolPaths.pluginsDir &&
|
|
3392
3485
|
toolPaths.pluginMarketplacePath &&
|
|
3393
|
-
(await canonicalCodexPluginsExist(rootDir))
|
|
3486
|
+
(await canonicalCodexPluginsExist(rootDir, homeDir))
|
|
3394
3487
|
) {
|
|
3395
3488
|
if (!next.pluginsDir) {
|
|
3396
3489
|
next.pluginsBackup = await backupPath(toolPaths.pluginsDir);
|
|
@@ -4409,6 +4502,7 @@ async function adoptExistingCodexPlugins(args: {
|
|
|
4409
4502
|
}
|
|
4410
4503
|
|
|
4411
4504
|
async function planCodexPluginFileChanges(args: {
|
|
4505
|
+
homeDir?: string;
|
|
4412
4506
|
rootDir: string;
|
|
4413
4507
|
pluginsDir: string;
|
|
4414
4508
|
pluginMarketplacePath?: string;
|
|
@@ -4423,14 +4517,20 @@ async function planCodexPluginFileChanges(args: {
|
|
|
4423
4517
|
const sources = new Map<string, string>();
|
|
4424
4518
|
const desiredPaths = new Set<string>();
|
|
4425
4519
|
|
|
4426
|
-
const marketplace = await loadCanonicalCodexMarketplaceText(
|
|
4520
|
+
const marketplace = await loadCanonicalCodexMarketplaceText(
|
|
4521
|
+
args.rootDir,
|
|
4522
|
+
args.homeDir
|
|
4523
|
+
);
|
|
4427
4524
|
if (marketplace.text != null && args.pluginMarketplacePath) {
|
|
4428
4525
|
desiredPaths.add(args.pluginMarketplacePath);
|
|
4429
4526
|
contents.set(args.pluginMarketplacePath, marketplace.text);
|
|
4430
4527
|
sources.set(args.pluginMarketplacePath, marketplace.sourcePath);
|
|
4431
4528
|
}
|
|
4432
4529
|
|
|
4433
|
-
for (const plugin of await loadCanonicalCodexPlugins(
|
|
4530
|
+
for (const plugin of await loadCanonicalCodexPlugins(
|
|
4531
|
+
args.rootDir,
|
|
4532
|
+
args.homeDir
|
|
4533
|
+
)) {
|
|
4434
4534
|
for (const [relPath, bytes] of plugin.files.entries()) {
|
|
4435
4535
|
const targetPath = join(args.pluginsDir, plugin.name, relPath);
|
|
4436
4536
|
desiredPaths.add(targetPath);
|
|
@@ -4641,6 +4741,7 @@ async function syncManagedToolEntry({
|
|
|
4641
4741
|
const pluginPlan =
|
|
4642
4742
|
tool === "codex" && entry.pluginsDir
|
|
4643
4743
|
? await planCodexPluginFileChanges({
|
|
4744
|
+
homeDir,
|
|
4644
4745
|
rootDir,
|
|
4645
4746
|
pluginsDir: entry.pluginsDir,
|
|
4646
4747
|
pluginMarketplacePath: entry.pluginMarketplacePath,
|
package/src/remote.ts
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
resolve,
|
|
12
12
|
} from "node:path";
|
|
13
13
|
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
builtinOperatingModelInstallRelPath,
|
|
16
|
+
facultBuiltinPackRoot,
|
|
17
|
+
} from "./builtin";
|
|
15
18
|
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
16
19
|
import {
|
|
17
20
|
renderBullets,
|
|
@@ -22,7 +25,11 @@ import {
|
|
|
22
25
|
renderTable,
|
|
23
26
|
} from "./cli-ui";
|
|
24
27
|
import { buildIndex } from "./index-builder";
|
|
25
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
facultRootDir,
|
|
30
|
+
projectRootFromAiRoot,
|
|
31
|
+
readFacultConfig,
|
|
32
|
+
} from "./paths";
|
|
26
33
|
import {
|
|
27
34
|
assertManifestIntegrity,
|
|
28
35
|
assertManifestSignature,
|
|
@@ -1341,6 +1348,84 @@ function serializeBuiltinPackManifest(manifest: BuiltinPackManifest): string {
|
|
|
1341
1348
|
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
1342
1349
|
}
|
|
1343
1350
|
|
|
1351
|
+
const OPERATING_MODEL_SNIPPET_FRAME = `## Working mode
|
|
1352
|
+
|
|
1353
|
+
<!-- fclty:global/baseline -->
|
|
1354
|
+
<!-- /fclty:global/baseline -->
|
|
1355
|
+
|
|
1356
|
+
<!-- fclty:global/core/work-units -->
|
|
1357
|
+
<!-- /fclty:global/core/work-units -->
|
|
1358
|
+
|
|
1359
|
+
<!-- fclty:global/core/feedback-loops -->
|
|
1360
|
+
<!-- /fclty:global/core/feedback-loops -->
|
|
1361
|
+
|
|
1362
|
+
<!-- fclty:global/core/verification -->
|
|
1363
|
+
<!-- /fclty:global/core/verification -->
|
|
1364
|
+
|
|
1365
|
+
<!-- fclty:global/core/writeback -->
|
|
1366
|
+
<!-- /fclty:global/core/writeback -->
|
|
1367
|
+
|
|
1368
|
+
## Shared instruction sources
|
|
1369
|
+
|
|
1370
|
+
- For work-unit definition and scope clarification, read \${refs.work_units}.
|
|
1371
|
+
- For identifying, improving, and validating feedback loops, read \${refs.feedback_loops}.
|
|
1372
|
+
- For verification and anti-false-positive checks, read \${refs.verification}.
|
|
1373
|
+
- For checking integration boundaries, read \${refs.integration}.
|
|
1374
|
+
- For learning, decisions, and writeback, read \${refs.learning_writeback}.
|
|
1375
|
+
- For capability evolution, proposal kinds, and \`facult ai\` workflow, read \${refs.evolution}.
|
|
1376
|
+
- For deciding whether something belongs in global or project scope, read \${refs.project_capability}.
|
|
1377
|
+
- Add private language, coding, or writing refs in local config only when they belong to the user's own operating layer.
|
|
1378
|
+
`;
|
|
1379
|
+
|
|
1380
|
+
function appendOperatingModelFrame(seedText: string): string {
|
|
1381
|
+
const normalized = seedText.trimEnd();
|
|
1382
|
+
if (normalized.includes("<!-- fclty:global/baseline -->")) {
|
|
1383
|
+
return `${normalized}\n`;
|
|
1384
|
+
}
|
|
1385
|
+
return `${normalized}\n\n## Facult Operating Model\n\n${OPERATING_MODEL_SNIPPET_FRAME}`;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
async function firstExistingFileText(
|
|
1389
|
+
candidates: string[]
|
|
1390
|
+
): Promise<string | null> {
|
|
1391
|
+
for (const candidate of candidates) {
|
|
1392
|
+
if (await pathExists(candidate)) {
|
|
1393
|
+
return await Bun.file(candidate).text();
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return null;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
async function seedAgentsGlobalText(args: {
|
|
1400
|
+
rootDir: string;
|
|
1401
|
+
homeDir?: string;
|
|
1402
|
+
fallbackText: string;
|
|
1403
|
+
}): Promise<{ text: string; seededFromExisting: boolean }> {
|
|
1404
|
+
const home = args.homeDir ?? homedir();
|
|
1405
|
+
const projectRoot = projectRootFromAiRoot(args.rootDir, home);
|
|
1406
|
+
const seedText = await firstExistingFileText(
|
|
1407
|
+
projectRoot
|
|
1408
|
+
? [
|
|
1409
|
+
join(projectRoot, "AGENTS.md"),
|
|
1410
|
+
join(projectRoot, "CLAUDE.md"),
|
|
1411
|
+
join(projectRoot, ".codex", "AGENTS.md"),
|
|
1412
|
+
join(projectRoot, ".claude", "CLAUDE.md"),
|
|
1413
|
+
]
|
|
1414
|
+
: [
|
|
1415
|
+
join(home, ".codex", "AGENTS.md"),
|
|
1416
|
+
join(home, ".claude", "CLAUDE.md"),
|
|
1417
|
+
join(home, ".cursor", "AGENTS.md"),
|
|
1418
|
+
]
|
|
1419
|
+
);
|
|
1420
|
+
if (!seedText?.trim()) {
|
|
1421
|
+
return { text: args.fallbackText, seededFromExisting: false };
|
|
1422
|
+
}
|
|
1423
|
+
return {
|
|
1424
|
+
text: appendOperatingModelFrame(seedText),
|
|
1425
|
+
seededFromExisting: true,
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1344
1429
|
async function scaffoldBuiltinOperatingModelPack(args: {
|
|
1345
1430
|
rootDir: string;
|
|
1346
1431
|
homeDir?: string;
|
|
@@ -1364,20 +1449,33 @@ async function scaffoldBuiltinOperatingModelPack(args: {
|
|
|
1364
1449
|
if (!relPath || relPath.startsWith("..")) {
|
|
1365
1450
|
continue;
|
|
1366
1451
|
}
|
|
1367
|
-
const
|
|
1368
|
-
const
|
|
1452
|
+
const targetRelPath = builtinOperatingModelInstallRelPath(relPath);
|
|
1453
|
+
const targetPath = join(rootDir, targetRelPath);
|
|
1454
|
+
const rawSourceText = await Bun.file(sourcePath).text();
|
|
1455
|
+
const targetExists = await pathExists(targetPath);
|
|
1456
|
+
const seed =
|
|
1457
|
+
targetRelPath === "AGENTS.global.md" && !targetExists
|
|
1458
|
+
? await seedAgentsGlobalText({
|
|
1459
|
+
rootDir,
|
|
1460
|
+
homeDir: args.homeDir,
|
|
1461
|
+
fallbackText: rawSourceText,
|
|
1462
|
+
})
|
|
1463
|
+
: null;
|
|
1464
|
+
const sourceText = seed?.text ?? rawSourceText;
|
|
1465
|
+
const trackInManifest = !seed?.seededFromExisting;
|
|
1369
1466
|
const sourceHash = sha256Text(sourceText);
|
|
1370
|
-
|
|
1371
|
-
let shouldWrite = !exists || Boolean(args.force);
|
|
1467
|
+
let shouldWrite = !targetExists || Boolean(args.force);
|
|
1372
1468
|
|
|
1373
|
-
if (
|
|
1469
|
+
if (targetExists && !shouldWrite) {
|
|
1374
1470
|
const targetText = await Bun.file(targetPath).text();
|
|
1375
1471
|
const targetHash = sha256Text(targetText);
|
|
1376
1472
|
if (targetHash === sourceHash) {
|
|
1377
|
-
|
|
1473
|
+
if (trackInManifest) {
|
|
1474
|
+
manifestFiles[targetRelPath] = { sha256: sourceHash };
|
|
1475
|
+
}
|
|
1378
1476
|
} else if (
|
|
1379
1477
|
args.update &&
|
|
1380
|
-
existingManifest?.files[
|
|
1478
|
+
existingManifest?.files[targetRelPath]?.sha256 === targetHash
|
|
1381
1479
|
) {
|
|
1382
1480
|
shouldWrite = true;
|
|
1383
1481
|
} else if (args.update) {
|
|
@@ -1389,7 +1487,11 @@ async function scaffoldBuiltinOperatingModelPack(args: {
|
|
|
1389
1487
|
continue;
|
|
1390
1488
|
}
|
|
1391
1489
|
changedPaths.push(targetPath);
|
|
1392
|
-
|
|
1490
|
+
if (trackInManifest) {
|
|
1491
|
+
manifestFiles[targetRelPath] = { sha256: sourceHash };
|
|
1492
|
+
} else {
|
|
1493
|
+
delete manifestFiles[targetRelPath];
|
|
1494
|
+
}
|
|
1393
1495
|
if (!args.dryRun) {
|
|
1394
1496
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
1395
1497
|
await Bun.write(targetPath, sourceText);
|