@fabricorg/cli 0.3.3 → 0.3.4
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/fabric.js +116 -25
- package/dist/fabric.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/dist/fabric.js
CHANGED
|
@@ -57,7 +57,7 @@ import { Command as Command20 } from "commander";
|
|
|
57
57
|
// package.json
|
|
58
58
|
var package_default = {
|
|
59
59
|
name: "@fabricorg/cli",
|
|
60
|
-
version: "0.3.
|
|
60
|
+
version: "0.3.4",
|
|
61
61
|
description: "CLI for the Fabric AI platform",
|
|
62
62
|
keywords: [
|
|
63
63
|
"fabric",
|
|
@@ -1371,7 +1371,10 @@ var DRIVE_LETTER = /^[A-Za-z]:/;
|
|
|
1371
1371
|
var RESERVED_DEVICE_NAME = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i;
|
|
1372
1372
|
var FORBIDDEN_NAME_CHARACTER = /[<>:"|?*]/;
|
|
1373
1373
|
var RESERVED_ROOTS = [".git", ".fabric"];
|
|
1374
|
-
var RESERVED_EXACT_PATHS = [
|
|
1374
|
+
var RESERVED_EXACT_PATHS = [
|
|
1375
|
+
".claude/settings.local.json",
|
|
1376
|
+
".codex/hooks.json"
|
|
1377
|
+
];
|
|
1375
1378
|
function hasControlCharacter(input) {
|
|
1376
1379
|
for (let index = 0; index < input.length; index++) {
|
|
1377
1380
|
const code = input.charCodeAt(index);
|
|
@@ -1492,6 +1495,7 @@ function describeRejection(rejection) {
|
|
|
1492
1495
|
|
|
1493
1496
|
// src/lib/instructions/safe-write.ts
|
|
1494
1497
|
import { createHash, randomBytes } from "crypto";
|
|
1498
|
+
import { constants } from "fs";
|
|
1495
1499
|
import {
|
|
1496
1500
|
lstat,
|
|
1497
1501
|
mkdir,
|
|
@@ -1587,24 +1591,79 @@ function assertWritableTarget(root, relativePath) {
|
|
|
1587
1591
|
return check;
|
|
1588
1592
|
});
|
|
1589
1593
|
}
|
|
1590
|
-
function readFileSafely(
|
|
1591
|
-
return __async(this,
|
|
1594
|
+
function readFileSafely(_0, _1) {
|
|
1595
|
+
return __async(this, arguments, function* (root, relativePath, options = {}) {
|
|
1592
1596
|
const check = yield assertWritableTarget(root, relativePath);
|
|
1593
1597
|
if (!check.ok) {
|
|
1594
1598
|
throw new Error(`Refusing to sync: ${describeRejection(check)}.`);
|
|
1595
1599
|
}
|
|
1596
|
-
|
|
1597
|
-
if (stats === null) {
|
|
1598
|
-
return null;
|
|
1599
|
-
}
|
|
1600
|
+
let handle;
|
|
1600
1601
|
try {
|
|
1601
|
-
|
|
1602
|
+
handle = yield open(
|
|
1603
|
+
check.path,
|
|
1604
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
|
|
1605
|
+
);
|
|
1602
1606
|
} catch (error) {
|
|
1603
1607
|
if (error.code === "ENOENT") {
|
|
1604
1608
|
return null;
|
|
1605
1609
|
}
|
|
1606
1610
|
throw error;
|
|
1607
1611
|
}
|
|
1612
|
+
try {
|
|
1613
|
+
const opened = yield handle.stat();
|
|
1614
|
+
if (!opened.isFile()) {
|
|
1615
|
+
throw new Error(
|
|
1616
|
+
`Refusing to sync: ${relativePath} is not a regular file.`
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
const afterOpen = yield assertWritableTarget(root, relativePath);
|
|
1620
|
+
if (!afterOpen.ok) {
|
|
1621
|
+
throw new Error(
|
|
1622
|
+
`Refusing to sync: ${describeRejection(afterOpen)}.`
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
const current = yield lstatOrNull(afterOpen.path);
|
|
1626
|
+
if (current === null || !current.isFile() || current.dev !== opened.dev || current.ino !== opened.ino) {
|
|
1627
|
+
throw new Error(
|
|
1628
|
+
`Refusing to sync: ${relativePath} changed while it was being opened.`
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
if (options.maxBytes === void 0) {
|
|
1632
|
+
return { bytes: yield handle.readFile(), mode: opened.mode };
|
|
1633
|
+
}
|
|
1634
|
+
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
|
|
1635
|
+
throw new Error(
|
|
1636
|
+
"A safe file-read limit must be a non-negative integer."
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
if (opened.size > options.maxBytes) {
|
|
1640
|
+
throw new Error(
|
|
1641
|
+
`${relativePath} is too large to read safely (maximum ${options.maxBytes} bytes).`
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
const buffer = Buffer.allocUnsafe(options.maxBytes + 1);
|
|
1645
|
+
let total = 0;
|
|
1646
|
+
while (total <= options.maxBytes) {
|
|
1647
|
+
const { bytesRead } = yield handle.read(
|
|
1648
|
+
buffer,
|
|
1649
|
+
total,
|
|
1650
|
+
options.maxBytes + 1 - total,
|
|
1651
|
+
total
|
|
1652
|
+
);
|
|
1653
|
+
if (bytesRead === 0) {
|
|
1654
|
+
break;
|
|
1655
|
+
}
|
|
1656
|
+
total += bytesRead;
|
|
1657
|
+
}
|
|
1658
|
+
if (total > options.maxBytes) {
|
|
1659
|
+
throw new Error(
|
|
1660
|
+
`${relativePath} is too large to read safely (maximum ${options.maxBytes} bytes).`
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
return { bytes: buffer.subarray(0, total), mode: opened.mode };
|
|
1664
|
+
} finally {
|
|
1665
|
+
yield handle.close();
|
|
1666
|
+
}
|
|
1608
1667
|
});
|
|
1609
1668
|
}
|
|
1610
1669
|
function resolveExistingRoot(destination) {
|
|
@@ -2022,14 +2081,16 @@ function extractBundle(archive, wanted) {
|
|
|
2022
2081
|
}
|
|
2023
2082
|
|
|
2024
2083
|
// src/lib/instructions/hook.ts
|
|
2025
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
2026
2084
|
import path2 from "path";
|
|
2027
2085
|
var CLAUDE_SETTINGS_RELATIVE_PATH = path2.join(
|
|
2028
2086
|
".claude",
|
|
2029
2087
|
"settings.local.json"
|
|
2030
2088
|
);
|
|
2089
|
+
var CODEX_HOOKS_RELATIVE_PATH = path2.join(".codex", "hooks.json");
|
|
2031
2090
|
var CLAUDE_SETTINGS_POSIX_PATH = ".claude/settings.local.json";
|
|
2091
|
+
var CODEX_HOOKS_POSIX_PATH = ".codex/hooks.json";
|
|
2032
2092
|
var HOOK_TIMEOUT_SECONDS = 15;
|
|
2093
|
+
var MAX_HOOK_CONFIG_BYTES = 1024 * 1024;
|
|
2033
2094
|
var HOOK_SUBCOMMANDS = /* @__PURE__ */ new Set(["check", "sync"]);
|
|
2034
2095
|
function buildHookCommand(projectId, apply, org) {
|
|
2035
2096
|
const verb = apply ? "sync" : "check";
|
|
@@ -2047,17 +2108,25 @@ function assertKeyStaysOutside(destination, configPath) {
|
|
|
2047
2108
|
}
|
|
2048
2109
|
function mergeSessionStartHook(input) {
|
|
2049
2110
|
return __async(this, null, function* () {
|
|
2050
|
-
|
|
2051
|
-
|
|
2111
|
+
var _a2;
|
|
2112
|
+
const hookTarget = hookTargetFor((_a2 = input.tool) != null ? _a2 : "claude-code");
|
|
2113
|
+
const settingsPath = path2.join(input.root, hookTarget.relativePath);
|
|
2114
|
+
let existingRaw;
|
|
2052
2115
|
try {
|
|
2053
|
-
|
|
2116
|
+
const existing = yield readFileSafely(
|
|
2117
|
+
input.root,
|
|
2118
|
+
hookTarget.posixPath,
|
|
2119
|
+
{ maxBytes: MAX_HOOK_CONFIG_BYTES }
|
|
2120
|
+
);
|
|
2121
|
+
existingRaw = existing === null ? null : new TextDecoder().decode(existing.bytes);
|
|
2054
2122
|
} catch (error) {
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2123
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2124
|
+
throw new Error(
|
|
2125
|
+
`${settingsPath} could not be read safely: ${message}. It was left untouched.`
|
|
2126
|
+
);
|
|
2058
2127
|
}
|
|
2059
2128
|
let settings = {};
|
|
2060
|
-
if (existingRaw !== null
|
|
2129
|
+
if (existingRaw !== null) {
|
|
2061
2130
|
let parsed;
|
|
2062
2131
|
try {
|
|
2063
2132
|
parsed = JSON.parse(existingRaw);
|
|
@@ -2124,7 +2193,7 @@ function mergeSessionStartHook(input) {
|
|
|
2124
2193
|
});
|
|
2125
2194
|
yield writeFileSafely({
|
|
2126
2195
|
root: input.root,
|
|
2127
|
-
relativePath:
|
|
2196
|
+
relativePath: hookTarget.posixPath,
|
|
2128
2197
|
bytes: new TextEncoder().encode(
|
|
2129
2198
|
`${JSON.stringify(next, null, 2).replace(/\r\n/g, "\n")}
|
|
2130
2199
|
`
|
|
@@ -2138,6 +2207,15 @@ function mergeSessionStartHook(input) {
|
|
|
2138
2207
|
};
|
|
2139
2208
|
});
|
|
2140
2209
|
}
|
|
2210
|
+
function hookTargetFor(tool) {
|
|
2211
|
+
return tool === "codex" ? {
|
|
2212
|
+
relativePath: CODEX_HOOKS_RELATIVE_PATH,
|
|
2213
|
+
posixPath: CODEX_HOOKS_POSIX_PATH
|
|
2214
|
+
} : {
|
|
2215
|
+
relativePath: CLAUDE_SETTINGS_RELATIVE_PATH,
|
|
2216
|
+
posixPath: CLAUDE_SETTINGS_POSIX_PATH
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2141
2219
|
function isFabricHookFor(hook, projectId) {
|
|
2142
2220
|
if (!isRecord(hook) || typeof hook.command !== "string") {
|
|
2143
2221
|
return false;
|
|
@@ -2179,7 +2257,7 @@ function isRecord(value) {
|
|
|
2179
2257
|
}
|
|
2180
2258
|
|
|
2181
2259
|
// src/lib/instructions/lock.ts
|
|
2182
|
-
import { readFile as
|
|
2260
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2183
2261
|
import path3 from "path";
|
|
2184
2262
|
var LOCK_DIRECTORY = ".fabric";
|
|
2185
2263
|
var LOCK_FILENAME = "instructions.lock";
|
|
@@ -2194,7 +2272,7 @@ function readLock(destination) {
|
|
|
2194
2272
|
const file = lockPath(destination);
|
|
2195
2273
|
let raw;
|
|
2196
2274
|
try {
|
|
2197
|
-
raw = yield
|
|
2275
|
+
raw = yield readFile2(file, "utf8");
|
|
2198
2276
|
} catch (error) {
|
|
2199
2277
|
if (error.code === "ENOENT") {
|
|
2200
2278
|
return null;
|
|
@@ -2785,7 +2863,7 @@ function buildInstructionsCommand() {
|
|
|
2785
2863
|
"Write a SessionStart hook for a coding tool and take the first copy"
|
|
2786
2864
|
).requiredOption("--project <id>", "Project ID").requiredOption(
|
|
2787
2865
|
"--tool <tool>",
|
|
2788
|
-
"Coding tool to configure: claude-code"
|
|
2866
|
+
"Coding tool to configure: claude-code|codex"
|
|
2789
2867
|
).option("--dest <dir>", "Destination directory (default: cwd)").option("--org <slug>", "Organization context").option(
|
|
2790
2868
|
"--apply",
|
|
2791
2869
|
"Let the hook apply changes instead of only reporting them"
|
|
@@ -3367,12 +3445,13 @@ function reportPush(outcome) {
|
|
|
3367
3445
|
}
|
|
3368
3446
|
function runInit(opts, format) {
|
|
3369
3447
|
return __async(this, null, function* () {
|
|
3370
|
-
if (opts.tool
|
|
3448
|
+
if (!isInstructionsHookTool(opts.tool)) {
|
|
3371
3449
|
throw new CliFailure(
|
|
3372
|
-
`Unsupported tool "${opts.tool}".
|
|
3450
|
+
`Unsupported tool "${opts.tool}". Use --tool claude-code or --tool codex.`,
|
|
3373
3451
|
2
|
|
3374
3452
|
);
|
|
3375
3453
|
}
|
|
3454
|
+
const tool = opts.tool;
|
|
3376
3455
|
const root = yield resolveDestinationRoot(destinationOf(opts));
|
|
3377
3456
|
yield readLockForProject(root, opts.project);
|
|
3378
3457
|
try {
|
|
@@ -3403,7 +3482,8 @@ function runInit(opts, format) {
|
|
|
3403
3482
|
const merged = yield mergeSessionStartHook({
|
|
3404
3483
|
root,
|
|
3405
3484
|
projectId: opts.project,
|
|
3406
|
-
command
|
|
3485
|
+
command,
|
|
3486
|
+
tool
|
|
3407
3487
|
});
|
|
3408
3488
|
if (format === "json") {
|
|
3409
3489
|
printOutput(
|
|
@@ -3427,6 +3507,11 @@ function runInit(opts, format) {
|
|
|
3427
3507
|
line(
|
|
3428
3508
|
opts.apply ? " It applies changes at session start." : " It only reports changes; add --apply to init to have it apply them."
|
|
3429
3509
|
);
|
|
3510
|
+
if (tool === "codex") {
|
|
3511
|
+
line(
|
|
3512
|
+
" Start Codex in this checkout, then use `/hooks` to review and trust the project hook."
|
|
3513
|
+
);
|
|
3514
|
+
}
|
|
3430
3515
|
if (outcome === null) {
|
|
3431
3516
|
line(
|
|
3432
3517
|
"This project has nothing published yet, so there is nothing to copy \u2014 the hook will pick it up once there is."
|
|
@@ -3437,10 +3522,16 @@ function runInit(opts, format) {
|
|
|
3437
3522
|
}
|
|
3438
3523
|
line("");
|
|
3439
3524
|
line(
|
|
3440
|
-
`${
|
|
3525
|
+
`${hookPathFor(tool)} and ${LOCK_DIRECTORY}/ are local to this machine. If this repository does not ignore them already, add them to your own ignore rules \u2014 this command does not edit .gitignore.`
|
|
3441
3526
|
);
|
|
3442
3527
|
});
|
|
3443
3528
|
}
|
|
3529
|
+
function isInstructionsHookTool(tool) {
|
|
3530
|
+
return tool === "claude-code" || tool === "codex";
|
|
3531
|
+
}
|
|
3532
|
+
function hookPathFor(tool) {
|
|
3533
|
+
return tool === "codex" ? CODEX_HOOKS_RELATIVE_PATH : CLAUDE_SETTINGS_RELATIVE_PATH;
|
|
3534
|
+
}
|
|
3444
3535
|
|
|
3445
3536
|
// src/commands/mcp/index.ts
|
|
3446
3537
|
import { Command as Command12 } from "commander";
|