facult 2.16.0 → 2.17.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 +3 -1
- package/docs/codex-plugin.md +16 -3
- package/docs/managed-mode.md +8 -0
- package/docs/reference.md +2 -1
- package/package.json +1 -1
- package/src/index.ts +7 -0
- package/src/manage.ts +253 -0
package/README.md
CHANGED
|
@@ -181,6 +181,7 @@ fclt index
|
|
|
181
181
|
Managed mode writes rendered files into a tool home. Use it only when `fclt` should own that rendered surface.
|
|
182
182
|
|
|
183
183
|
```bash
|
|
184
|
+
fclt setup codex-plugin
|
|
184
185
|
fclt manage codex --dry-run
|
|
185
186
|
fclt manage codex --adopt-existing
|
|
186
187
|
fclt sync codex --dry-run
|
|
@@ -377,6 +378,7 @@ fclt index [--force]
|
|
|
377
378
|
Managed mode:
|
|
378
379
|
|
|
379
380
|
```bash
|
|
381
|
+
fclt setup codex-plugin [--dry-run] [--json]
|
|
380
382
|
fclt manage <tool> [--dry-run] [--adopt-existing]
|
|
381
383
|
fclt sync [tool] [--dry-run] [--adopt-live]
|
|
382
384
|
fclt enable <selector> --for codex,claude
|
|
@@ -426,7 +428,7 @@ Start with:
|
|
|
426
428
|
|
|
427
429
|
### Does fclt run an MCP server?
|
|
428
430
|
|
|
429
|
-
The core product is still CLI-first.
|
|
431
|
+
The core product is still CLI-first. `fclt setup codex-plugin` installs the first-party Codex plugin without putting all of Codex under managed mode. The plugin includes a small stdio MCP wrapper that delegates to the installed `fclt` binary for status, doctor, paths, setup, writeback, and evolution workflows. See [Codex plugin](./docs/codex-plugin.md).
|
|
430
432
|
|
|
431
433
|
### Does fclt have to manage Codex or Claude files?
|
|
432
434
|
|
package/docs/codex-plugin.md
CHANGED
|
@@ -34,15 +34,28 @@ These tools are thin wrappers around CLI commands and return command output. Mut
|
|
|
34
34
|
|
|
35
35
|
## Install In Codex
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Use the narrow setup command for normal installs:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
fclt setup codex-plugin
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That updates only the local `fclt` plugin payload under `~/plugins/fclt`, merges the `hack-local` marketplace entry at `~/.agents/plugins/marketplace.json`, and runs `codex plugin add fclt@hack-local --json` when the `codex` command is available. It does not enter managed mode, adopt Codex state, render `~/.codex/AGENTS.md`, or touch existing Codex skills/rules/config.
|
|
44
|
+
|
|
45
|
+
Useful flags:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
fclt setup codex-plugin --dry-run --json
|
|
49
|
+
fclt setup codex-plugin --no-codex-install
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use managed sync only when you intentionally want `fclt` to render broader Codex tool files:
|
|
38
53
|
|
|
39
54
|
```bash
|
|
40
55
|
fclt manage codex --global
|
|
41
56
|
fclt sync codex --global
|
|
42
57
|
```
|
|
43
58
|
|
|
44
|
-
That writes plugin files under the Codex plugin location and updates the personal marketplace entry. Use managed sync only when you want `fclt` to write Codex tool files.
|
|
45
|
-
|
|
46
59
|
For local plugin development, run the lightweight checks that ship with the repository:
|
|
47
60
|
|
|
48
61
|
```bash
|
package/docs/managed-mode.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Managed mode is optional. Use it when you want `fclt` to write rendered files into a tool home. Do not use it just to inspect or normalize existing tool-native state.
|
|
4
4
|
|
|
5
|
+
If you only want the first-party fclt Codex plugin, use the narrow setup path instead:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
fclt setup codex-plugin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That installs/exposes the bundled plugin without adopting or rendering the rest of Codex state.
|
|
12
|
+
|
|
5
13
|
Prefer this default workflow:
|
|
6
14
|
|
|
7
15
|
```bash
|
package/docs/reference.md
CHANGED
|
@@ -65,6 +65,7 @@ Use these to create or normalize canonical capability in `~/.ai` or `<repo>/.ai`
|
|
|
65
65
|
## Managed mode
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
+
fclt setup codex-plugin [--dry-run] [--json] [--no-codex-install]
|
|
68
69
|
fclt manage <tool> [--dry-run] [--adopt-existing]
|
|
69
70
|
fclt sync [tool] [--dry-run] [--adopt-live]
|
|
70
71
|
fclt enable <selector> --for codex,claude
|
|
@@ -73,7 +74,7 @@ fclt managed
|
|
|
73
74
|
fclt unmanage <tool>
|
|
74
75
|
```
|
|
75
76
|
|
|
76
|
-
Managed mode writes rendered output into tool homes. Read [Managed mode](./managed-mode.md) before using it on an existing setup.
|
|
77
|
+
`setup codex-plugin` is the narrow path for exposing the bundled fclt Codex plugin without entering managed mode. Managed mode writes rendered output into tool homes. Read [Managed mode](./managed-mode.md) before using it on an existing setup.
|
|
77
78
|
|
|
78
79
|
## Writeback and evolution
|
|
79
80
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -142,6 +142,10 @@ function printHelp() {
|
|
|
142
142
|
"manage/sync",
|
|
143
143
|
"Enter managed mode and render tool-native output",
|
|
144
144
|
],
|
|
145
|
+
[
|
|
146
|
+
"setup",
|
|
147
|
+
"Install narrow agent integrations without full managed mode",
|
|
148
|
+
],
|
|
145
149
|
["ai", "Capture writeback and evolve canonical assets"],
|
|
146
150
|
],
|
|
147
151
|
}),
|
|
@@ -1365,6 +1369,9 @@ async function main(argv: string[]) {
|
|
|
1365
1369
|
case "sync":
|
|
1366
1370
|
await import("./manage").then(({ syncCommand }) => syncCommand(rest));
|
|
1367
1371
|
return;
|
|
1372
|
+
case "setup":
|
|
1373
|
+
await import("./manage").then(({ setupCommand }) => setupCommand(rest));
|
|
1374
|
+
return;
|
|
1368
1375
|
case "autosync":
|
|
1369
1376
|
await import("./autosync").then(({ autosyncCommand }) =>
|
|
1370
1377
|
autosyncCommand(rest)
|
package/src/manage.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import {
|
|
3
4
|
appendFile,
|
|
@@ -146,6 +147,29 @@ export interface SyncOptions {
|
|
|
146
147
|
adoptLive?: boolean;
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
export interface SetupCodexPluginOptions {
|
|
151
|
+
homeDir?: string;
|
|
152
|
+
dryRun?: boolean;
|
|
153
|
+
installInCodex?: boolean;
|
|
154
|
+
codexBin?: string | null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface SetupCodexPluginResult {
|
|
158
|
+
pluginDir: string;
|
|
159
|
+
marketplacePath: string;
|
|
160
|
+
marketplaceName: string;
|
|
161
|
+
changedPaths: string[];
|
|
162
|
+
dryRun: boolean;
|
|
163
|
+
codexInstall: {
|
|
164
|
+
status: "skipped" | "succeeded" | "failed";
|
|
165
|
+
command?: string[];
|
|
166
|
+
stdout?: string;
|
|
167
|
+
stderr?: string;
|
|
168
|
+
code?: number | null;
|
|
169
|
+
reason?: string;
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
149
173
|
const MANAGED_VERSION = 1 as const;
|
|
150
174
|
|
|
151
175
|
function nowIso(now?: () => Date): string {
|
|
@@ -350,6 +374,54 @@ function fcltCodexMarketplaceEntry(): Record<string, unknown> {
|
|
|
350
374
|
};
|
|
351
375
|
}
|
|
352
376
|
|
|
377
|
+
function hackLocalCodexMarketplaceText(text: string | null): string {
|
|
378
|
+
const base =
|
|
379
|
+
text == null
|
|
380
|
+
? {
|
|
381
|
+
name: "hack-local",
|
|
382
|
+
interface: { displayName: "Hack Local Plugins" },
|
|
383
|
+
plugins: [],
|
|
384
|
+
}
|
|
385
|
+
: (JSON.parse(text) as unknown);
|
|
386
|
+
if (!isPlainObject(base)) {
|
|
387
|
+
throw new Error("Codex plugin marketplace must be a JSON object.");
|
|
388
|
+
}
|
|
389
|
+
const plugins = Array.isArray(base.plugins) ? [...base.plugins] : [];
|
|
390
|
+
const nextPlugins = plugins.filter(
|
|
391
|
+
(entry) => !(isPlainObject(entry) && entry.name === FCLT_CODEX_PLUGIN_NAME)
|
|
392
|
+
);
|
|
393
|
+
nextPlugins.push(fcltCodexMarketplaceEntry());
|
|
394
|
+
return normalizeCodexMarketplaceText(
|
|
395
|
+
JSON.stringify(
|
|
396
|
+
{
|
|
397
|
+
...base,
|
|
398
|
+
name:
|
|
399
|
+
typeof base.name === "string" && base.name.trim()
|
|
400
|
+
? base.name
|
|
401
|
+
: "hack-local",
|
|
402
|
+
interface: isPlainObject(base.interface)
|
|
403
|
+
? base.interface
|
|
404
|
+
: { displayName: "Hack Local Plugins" },
|
|
405
|
+
plugins: nextPlugins,
|
|
406
|
+
},
|
|
407
|
+
null,
|
|
408
|
+
2
|
|
409
|
+
)
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function codexMarketplaceNameFromText(text: string): string {
|
|
414
|
+
const parsed = JSON.parse(text) as unknown;
|
|
415
|
+
if (
|
|
416
|
+
isPlainObject(parsed) &&
|
|
417
|
+
typeof parsed.name === "string" &&
|
|
418
|
+
parsed.name.trim()
|
|
419
|
+
) {
|
|
420
|
+
return parsed.name;
|
|
421
|
+
}
|
|
422
|
+
return "hack-local";
|
|
423
|
+
}
|
|
424
|
+
|
|
353
425
|
function withBuiltinFcltCodexMarketplaceEntry(text: string | null): string {
|
|
354
426
|
const base =
|
|
355
427
|
text == null
|
|
@@ -4600,6 +4672,125 @@ async function planCodexPluginFileChanges(args: {
|
|
|
4600
4672
|
};
|
|
4601
4673
|
}
|
|
4602
4674
|
|
|
4675
|
+
async function runCodexPluginAdd(args: {
|
|
4676
|
+
codexBin: string;
|
|
4677
|
+
cwd: string;
|
|
4678
|
+
marketplaceName: string;
|
|
4679
|
+
}): Promise<SetupCodexPluginResult["codexInstall"]> {
|
|
4680
|
+
const command = [
|
|
4681
|
+
args.codexBin,
|
|
4682
|
+
"plugin",
|
|
4683
|
+
"add",
|
|
4684
|
+
`fclt@${args.marketplaceName}`,
|
|
4685
|
+
"--json",
|
|
4686
|
+
];
|
|
4687
|
+
return await new Promise((resolve) => {
|
|
4688
|
+
const child = spawn(args.codexBin, command.slice(1), {
|
|
4689
|
+
cwd: args.cwd,
|
|
4690
|
+
env: process.env,
|
|
4691
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4692
|
+
});
|
|
4693
|
+
let stdout = "";
|
|
4694
|
+
let stderr = "";
|
|
4695
|
+
child.stdout.on("data", (chunk) => {
|
|
4696
|
+
stdout += chunk.toString();
|
|
4697
|
+
});
|
|
4698
|
+
child.stderr.on("data", (chunk) => {
|
|
4699
|
+
stderr += chunk.toString();
|
|
4700
|
+
});
|
|
4701
|
+
child.on("error", (error) => {
|
|
4702
|
+
resolve({
|
|
4703
|
+
status: "failed",
|
|
4704
|
+
command,
|
|
4705
|
+
stdout,
|
|
4706
|
+
stderr: error.message,
|
|
4707
|
+
code: null,
|
|
4708
|
+
});
|
|
4709
|
+
});
|
|
4710
|
+
child.on("close", (code) => {
|
|
4711
|
+
resolve({
|
|
4712
|
+
status: code === 0 ? "succeeded" : "failed",
|
|
4713
|
+
command,
|
|
4714
|
+
stdout,
|
|
4715
|
+
stderr,
|
|
4716
|
+
code,
|
|
4717
|
+
});
|
|
4718
|
+
});
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
|
|
4722
|
+
export async function setupCodexPlugin(
|
|
4723
|
+
opts: SetupCodexPluginOptions = {}
|
|
4724
|
+
): Promise<SetupCodexPluginResult> {
|
|
4725
|
+
const home = opts.homeDir ?? homedir();
|
|
4726
|
+
const pluginDir = codexPluginsDir(home);
|
|
4727
|
+
const marketplacePath = codexPluginMarketplacePath(home);
|
|
4728
|
+
const sourceDir = facultBuiltinCodexPluginRoot();
|
|
4729
|
+
const changedPaths: string[] = [];
|
|
4730
|
+
const sourceHash = await hashDirectoryTree(sourceDir);
|
|
4731
|
+
const pluginTargetDir = join(pluginDir, FCLT_CODEX_PLUGIN_NAME);
|
|
4732
|
+
const targetHash = await hashDirectoryTree(pluginTargetDir);
|
|
4733
|
+
|
|
4734
|
+
if (sourceHash !== targetHash) {
|
|
4735
|
+
changedPaths.push(pluginTargetDir);
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4738
|
+
const marketplaceRaw = await readTextOrNull(marketplacePath);
|
|
4739
|
+
let marketplaceText: string;
|
|
4740
|
+
try {
|
|
4741
|
+
marketplaceText = hackLocalCodexMarketplaceText(marketplaceRaw);
|
|
4742
|
+
} catch (error) {
|
|
4743
|
+
throw new Error(
|
|
4744
|
+
`Cannot update Codex plugin marketplace at ${marketplacePath}: ${
|
|
4745
|
+
error instanceof Error ? error.message : String(error)
|
|
4746
|
+
}`
|
|
4747
|
+
);
|
|
4748
|
+
}
|
|
4749
|
+
const marketplaceName = codexMarketplaceNameFromText(marketplaceText);
|
|
4750
|
+
if (
|
|
4751
|
+
(await readTargetHash(marketplacePath, { normalizeText: false })) !==
|
|
4752
|
+
targetContentHash(marketplaceText, { normalizeText: false })
|
|
4753
|
+
) {
|
|
4754
|
+
changedPaths.push(marketplacePath);
|
|
4755
|
+
}
|
|
4756
|
+
|
|
4757
|
+
const installInCodex = opts.installInCodex ?? true;
|
|
4758
|
+
let codexInstall: SetupCodexPluginResult["codexInstall"] = {
|
|
4759
|
+
status: "skipped",
|
|
4760
|
+
reason: installInCodex
|
|
4761
|
+
? "dry-run"
|
|
4762
|
+
: "codex plugin install disabled by --no-codex-install",
|
|
4763
|
+
};
|
|
4764
|
+
|
|
4765
|
+
if (!opts.dryRun) {
|
|
4766
|
+
await rm(pluginTargetDir, { recursive: true, force: true });
|
|
4767
|
+
await ensureDir(pluginDir);
|
|
4768
|
+
await cp(sourceDir, pluginTargetDir, { recursive: true });
|
|
4769
|
+
await ensureDir(dirname(marketplacePath));
|
|
4770
|
+
await Bun.write(marketplacePath, marketplaceText);
|
|
4771
|
+
|
|
4772
|
+
if (installInCodex) {
|
|
4773
|
+
const codexBin =
|
|
4774
|
+
opts.codexBin === undefined ? Bun.which("codex") : opts.codexBin;
|
|
4775
|
+
codexInstall = codexBin
|
|
4776
|
+
? await runCodexPluginAdd({ codexBin, cwd: home, marketplaceName })
|
|
4777
|
+
: {
|
|
4778
|
+
status: "skipped",
|
|
4779
|
+
reason: "codex command not found",
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4784
|
+
return {
|
|
4785
|
+
pluginDir: pluginTargetDir,
|
|
4786
|
+
marketplacePath,
|
|
4787
|
+
marketplaceName,
|
|
4788
|
+
changedPaths: changedPaths.sort(),
|
|
4789
|
+
dryRun: Boolean(opts.dryRun),
|
|
4790
|
+
codexInstall,
|
|
4791
|
+
};
|
|
4792
|
+
}
|
|
4793
|
+
|
|
4603
4794
|
async function syncManagedToolEntry({
|
|
4604
4795
|
homeDir,
|
|
4605
4796
|
tool,
|
|
@@ -5329,6 +5520,68 @@ export async function managedCommand(argv: string[] = []) {
|
|
|
5329
5520
|
);
|
|
5330
5521
|
}
|
|
5331
5522
|
|
|
5523
|
+
export async function setupCommand(argv: string[]) {
|
|
5524
|
+
const args = [...argv];
|
|
5525
|
+
if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
|
|
5526
|
+
console.log(`fclt setup — set up narrow integrations without full managed mode
|
|
5527
|
+
|
|
5528
|
+
Usage:
|
|
5529
|
+
fclt setup codex-plugin [--dry-run] [--json] [--no-codex-install]
|
|
5530
|
+
|
|
5531
|
+
Options:
|
|
5532
|
+
--dry-run Show paths that would change without writing files
|
|
5533
|
+
--json Print machine-readable setup result
|
|
5534
|
+
--no-codex-install Only expose the local marketplace entry; do not run codex plugin add
|
|
5535
|
+
`);
|
|
5536
|
+
return;
|
|
5537
|
+
}
|
|
5538
|
+
const target = args.find((arg) => !arg.startsWith("-"));
|
|
5539
|
+
const dryRun = args.includes("--dry-run");
|
|
5540
|
+
const json = args.includes("--json");
|
|
5541
|
+
const installInCodex = !args.includes("--no-codex-install");
|
|
5542
|
+
if (target !== "codex-plugin") {
|
|
5543
|
+
console.error("setup requires a target: codex-plugin");
|
|
5544
|
+
process.exitCode = 1;
|
|
5545
|
+
return;
|
|
5546
|
+
}
|
|
5547
|
+
|
|
5548
|
+
try {
|
|
5549
|
+
const result = await setupCodexPlugin({
|
|
5550
|
+
dryRun,
|
|
5551
|
+
installInCodex,
|
|
5552
|
+
});
|
|
5553
|
+
if (json) {
|
|
5554
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5555
|
+
if (result.codexInstall.status === "failed") {
|
|
5556
|
+
process.exitCode = 1;
|
|
5557
|
+
}
|
|
5558
|
+
return;
|
|
5559
|
+
}
|
|
5560
|
+
if (dryRun) {
|
|
5561
|
+
console.log("codex plugin setup dry-run");
|
|
5562
|
+
} else {
|
|
5563
|
+
console.log("codex plugin setup complete");
|
|
5564
|
+
}
|
|
5565
|
+
console.log(`plugin: ${result.pluginDir}`);
|
|
5566
|
+
console.log(`marketplace: ${result.marketplacePath}`);
|
|
5567
|
+
console.log(`changed: ${result.changedPaths.length}`);
|
|
5568
|
+
if (result.codexInstall.status === "succeeded") {
|
|
5569
|
+
console.log("codex install: succeeded");
|
|
5570
|
+
} else if (result.codexInstall.status === "failed") {
|
|
5571
|
+
console.log("codex install: failed");
|
|
5572
|
+
if (result.codexInstall.stderr?.trim()) {
|
|
5573
|
+
console.log(result.codexInstall.stderr.trim());
|
|
5574
|
+
}
|
|
5575
|
+
process.exitCode = 1;
|
|
5576
|
+
} else {
|
|
5577
|
+
console.log(`codex install: skipped (${result.codexInstall.reason})`);
|
|
5578
|
+
}
|
|
5579
|
+
} catch (err) {
|
|
5580
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5581
|
+
process.exitCode = 1;
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
|
|
5332
5585
|
export async function syncCommand(argv: string[]) {
|
|
5333
5586
|
const parsed = parseCliContextArgs(argv);
|
|
5334
5587
|
if (
|