@fabricorg/cli 0.3.3 → 0.3.5
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 +142 -30
- package/dist/fabric.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
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.5",
|
|
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;
|
|
@@ -2766,12 +2844,15 @@ function buildInstructionsCommand() {
|
|
|
2766
2844
|
});
|
|
2767
2845
|
});
|
|
2768
2846
|
instructions.command("push").description(
|
|
2769
|
-
"
|
|
2847
|
+
"Propose this checkout's edits to the project's coding instructions, or publish them with --publish"
|
|
2770
2848
|
).requiredOption("--project <id>", "Project ID").option("--dest <dir>", "Destination directory (default: cwd)").option("--org <slug>", "Organization context").option(
|
|
2771
2849
|
"--add <path>",
|
|
2772
2850
|
"Also send a file the last sync did not write (repeatable)",
|
|
2773
2851
|
collectAdded,
|
|
2774
2852
|
[]
|
|
2853
|
+
).option(
|
|
2854
|
+
"--publish",
|
|
2855
|
+
"Publish the change as a new version instead of proposing it (needs a key with instructions:publish)"
|
|
2775
2856
|
).option("--dry-run", "Print the change set and send nothing").option("--format <format>", "Output format: text|json").action(function(opts) {
|
|
2776
2857
|
return __async(this, null, function* () {
|
|
2777
2858
|
yield run(
|
|
@@ -2785,7 +2866,7 @@ function buildInstructionsCommand() {
|
|
|
2785
2866
|
"Write a SessionStart hook for a coding tool and take the first copy"
|
|
2786
2867
|
).requiredOption("--project <id>", "Project ID").requiredOption(
|
|
2787
2868
|
"--tool <tool>",
|
|
2788
|
-
"Coding tool to configure: claude-code"
|
|
2869
|
+
"Coding tool to configure: claude-code|codex"
|
|
2789
2870
|
).option("--dest <dir>", "Destination directory (default: cwd)").option("--org <slug>", "Organization context").option(
|
|
2790
2871
|
"--apply",
|
|
2791
2872
|
"Let the hook apply changes instead of only reporting them"
|
|
@@ -3269,6 +3350,7 @@ function runPush(opts, format) {
|
|
|
3269
3350
|
projectId: opts.project,
|
|
3270
3351
|
destination: root,
|
|
3271
3352
|
dryRun: Boolean(opts.dryRun),
|
|
3353
|
+
publish: Boolean(opts.publish),
|
|
3272
3354
|
baseSnapshotId: lock.snapshotId,
|
|
3273
3355
|
baseVersion: lock.snapshotVersion,
|
|
3274
3356
|
put: plan.entries.filter((entry) => entry.action === "put").map((entry) => entry.path),
|
|
@@ -3277,11 +3359,17 @@ function runPush(opts, format) {
|
|
|
3277
3359
|
snapshotId: null,
|
|
3278
3360
|
version: null,
|
|
3279
3361
|
proposalStatus: null,
|
|
3280
|
-
status: null
|
|
3362
|
+
status: null,
|
|
3363
|
+
published: null
|
|
3281
3364
|
};
|
|
3282
3365
|
if (!opts.dryRun) {
|
|
3283
3366
|
try {
|
|
3284
|
-
const submitted = yield client.instructions.
|
|
3367
|
+
const submitted = opts.publish ? yield client.instructions.publishChange(
|
|
3368
|
+
opts.project,
|
|
3369
|
+
lock.snapshotId,
|
|
3370
|
+
plan.changes,
|
|
3371
|
+
{ org: orgSlugFor(opts) }
|
|
3372
|
+
) : yield client.instructions.submitChange(
|
|
3285
3373
|
opts.project,
|
|
3286
3374
|
lock.snapshotId,
|
|
3287
3375
|
plan.changes,
|
|
@@ -3291,6 +3379,7 @@ function runPush(opts, format) {
|
|
|
3291
3379
|
outcome.version = submitted.version;
|
|
3292
3380
|
outcome.proposalStatus = submitted.proposalStatus;
|
|
3293
3381
|
outcome.status = submitted.status;
|
|
3382
|
+
outcome.published = submitted.published;
|
|
3294
3383
|
} catch (error) {
|
|
3295
3384
|
throw asPushFailure(error);
|
|
3296
3385
|
}
|
|
@@ -3330,6 +3419,16 @@ function asPushFailure(error) {
|
|
|
3330
3419
|
}
|
|
3331
3420
|
function pushVerdict(outcome) {
|
|
3332
3421
|
var _a2;
|
|
3422
|
+
if (outcome.publish) {
|
|
3423
|
+
if (outcome.published) {
|
|
3424
|
+
return `Published version ${outcome.version}. Run \`fabric instructions sync\` to bring this checkout onto it.`;
|
|
3425
|
+
}
|
|
3426
|
+
if (outcome.status === "FAILED" || outcome.status === "REJECTED") {
|
|
3427
|
+
return `Version ${outcome.version} did not pass its checks (${outcome.status}), so nothing was published; open the project's Coding Instructions tab to see why.`;
|
|
3428
|
+
}
|
|
3429
|
+
const sent = `Sent as version ${outcome.version}. It publishes on its own once its checks pass (${outcome.status}) \u2014 then run \`fabric instructions sync\` to bring this checkout onto it.`;
|
|
3430
|
+
return outcome.status === "READY" ? `${sent} It has passed its checks; its publication state is shown in the project's Coding Instructions tab.` : sent;
|
|
3431
|
+
}
|
|
3333
3432
|
if (outcome.proposalStatus === "PENDING" && outcome.status === "RECEIVING") {
|
|
3334
3433
|
return `An earlier attempt is still sending this change; if version ${outcome.version} stays unfinished, cancel it in the project's Coding Instructions tab, or leave it and it is closed out automatically after six hours.`;
|
|
3335
3434
|
}
|
|
@@ -3356,23 +3455,24 @@ function reportPush(outcome) {
|
|
|
3356
3455
|
listPaths("deleted", outcome.deleted);
|
|
3357
3456
|
if (outcome.dryRun) {
|
|
3358
3457
|
line(
|
|
3359
|
-
"Nothing was sent. Without --dry-run this would open a proposal for review."
|
|
3458
|
+
outcome.publish ? "Nothing was sent. Without --dry-run this would create a new version that publishes on its own once its checks pass, with no review." : "Nothing was sent. Without --dry-run this would open a proposal for review."
|
|
3360
3459
|
);
|
|
3361
3460
|
return;
|
|
3362
3461
|
}
|
|
3363
3462
|
line(pushVerdict(outcome));
|
|
3364
3463
|
line(
|
|
3365
|
-
"Your lock was not changed: it still names the published version, which is what `fabric instructions sync` compares against."
|
|
3464
|
+
outcome.publish ? `Your lock was not changed: it still names version ${outcome.baseVersion}, which is what \`fabric instructions sync\` compares against.` : "Your lock was not changed: it still names the published version, which is what `fabric instructions sync` compares against."
|
|
3366
3465
|
);
|
|
3367
3466
|
}
|
|
3368
3467
|
function runInit(opts, format) {
|
|
3369
3468
|
return __async(this, null, function* () {
|
|
3370
|
-
if (opts.tool
|
|
3469
|
+
if (!isInstructionsHookTool(opts.tool)) {
|
|
3371
3470
|
throw new CliFailure(
|
|
3372
|
-
`Unsupported tool "${opts.tool}".
|
|
3471
|
+
`Unsupported tool "${opts.tool}". Use --tool claude-code or --tool codex.`,
|
|
3373
3472
|
2
|
|
3374
3473
|
);
|
|
3375
3474
|
}
|
|
3475
|
+
const tool = opts.tool;
|
|
3376
3476
|
const root = yield resolveDestinationRoot(destinationOf(opts));
|
|
3377
3477
|
yield readLockForProject(root, opts.project);
|
|
3378
3478
|
try {
|
|
@@ -3403,7 +3503,8 @@ function runInit(opts, format) {
|
|
|
3403
3503
|
const merged = yield mergeSessionStartHook({
|
|
3404
3504
|
root,
|
|
3405
3505
|
projectId: opts.project,
|
|
3406
|
-
command
|
|
3506
|
+
command,
|
|
3507
|
+
tool
|
|
3407
3508
|
});
|
|
3408
3509
|
if (format === "json") {
|
|
3409
3510
|
printOutput(
|
|
@@ -3427,6 +3528,11 @@ function runInit(opts, format) {
|
|
|
3427
3528
|
line(
|
|
3428
3529
|
opts.apply ? " It applies changes at session start." : " It only reports changes; add --apply to init to have it apply them."
|
|
3429
3530
|
);
|
|
3531
|
+
if (tool === "codex") {
|
|
3532
|
+
line(
|
|
3533
|
+
" Start Codex in this checkout, then use `/hooks` to review and trust the project hook."
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3430
3536
|
if (outcome === null) {
|
|
3431
3537
|
line(
|
|
3432
3538
|
"This project has nothing published yet, so there is nothing to copy \u2014 the hook will pick it up once there is."
|
|
@@ -3437,10 +3543,16 @@ function runInit(opts, format) {
|
|
|
3437
3543
|
}
|
|
3438
3544
|
line("");
|
|
3439
3545
|
line(
|
|
3440
|
-
`${
|
|
3546
|
+
`${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
3547
|
);
|
|
3442
3548
|
});
|
|
3443
3549
|
}
|
|
3550
|
+
function isInstructionsHookTool(tool) {
|
|
3551
|
+
return tool === "claude-code" || tool === "codex";
|
|
3552
|
+
}
|
|
3553
|
+
function hookPathFor(tool) {
|
|
3554
|
+
return tool === "codex" ? CODEX_HOOKS_RELATIVE_PATH : CLAUDE_SETTINGS_RELATIVE_PATH;
|
|
3555
|
+
}
|
|
3444
3556
|
|
|
3445
3557
|
// src/commands/mcp/index.ts
|
|
3446
3558
|
import { Command as Command12 } from "commander";
|