@fenglimg/fabric-cli 1.6.0 → 1.8.0-rc.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 +8 -14
- package/dist/{chunk-QSAEGVKE.js → chunk-NMMUETVK.js} +4 -8
- package/dist/{chunk-AEOYCVBG.js → chunk-QPCRBQ5Y.js} +52 -5
- package/dist/doctor-F52XWWZC.js +98 -0
- package/dist/index.js +5 -20
- package/dist/{init-LBVOI2QI.js → init-AEO5JU7R.js} +1084 -167
- package/dist/{scan-QH76LC7Z.js → scan-NNBNGIZG.js} +2 -4
- package/dist/{serve-4J2CQY25.js → serve-466QXQ5Q.js} +17 -9
- package/package.json +5 -7
- package/templates/agents-md/AGENTS.md.template +7 -7
- package/templates/agents-md/variants/cocos.md +7 -7
- package/templates/agents-md/variants/next.md +7 -7
- package/templates/agents-md/variants/vite.md +7 -7
- package/templates/bootstrap/CLAUDE.md +3 -1
- package/templates/bootstrap/GEMINI.md +3 -1
- package/templates/bootstrap/codex-AGENTS-header.md +3 -1
- package/templates/bootstrap/cursor-fabric-bootstrap.mdc +5 -6
- package/templates/bootstrap/roo-fabric.md +5 -6
- package/templates/bootstrap/windsurf-fabric.md +5 -6
- package/templates/claude-skills/fabric-init/SKILL.md +163 -0
- package/templates/codex-skills/fabric-init/SKILL.md +153 -18
- package/templates/husky/pre-commit +9 -24
- package/templates/skill-source/fabric-init/SOURCE.md +157 -0
- package/templates/skill-source/fabric-init/clients.json +17 -0
- package/dist/approve-YT4DEABS.js +0 -138
- package/dist/bootstrap-VGL3AR26.js +0 -16
- package/dist/chunk-2YW5CJ32.js +0 -147
- package/dist/chunk-6ICJICVU.js +0 -10
- package/dist/chunk-BEKSXO5N.js +0 -442
- package/dist/chunk-BVTMVW5M.js +0 -159
- package/dist/chunk-KOAEIH72.js +0 -270
- package/dist/chunk-L43IGJ6X.js +0 -106
- package/dist/chunk-T2WJF5I3.js +0 -254
- package/dist/chunk-WWNXR34K.js +0 -49
- package/dist/chunk-YDZJRLHL.js +0 -155
- package/dist/config-EC5L2QNI.js +0 -16
- package/dist/doctor-4BPYHV7V.js +0 -134
- package/dist/hooks-ZSWVH2JD.js +0 -12
- package/dist/human-lint-YSFOZHZ7.js +0 -13
- package/dist/ledger-append-3MDNR3GU.js +0 -10
- package/dist/pre-commit-53ENJDRZ.js +0 -98
- package/dist/sync-meta-IZR2WLIL.js +0 -16
- package/dist/update-M5M5PYKE.js +0 -116
- package/templates/claude-skills/agents-md-init/SKILL.md +0 -86
- package/templates/fabric/human-lock.json +0 -12
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# Fabric pre-commit hook
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
exit 1
|
|
11
|
-
fi
|
|
12
|
-
|
|
13
|
-
# Single Node invocation covering all three checks + meta guard.
|
|
14
|
-
# The `pre-commit` meta-command chains sync-meta/human-lint/ledger-append
|
|
15
|
-
# inside one process for minimal startup overhead.
|
|
16
|
-
"$FAB_BIN" pre-commit || exit $?
|
|
17
|
-
|
|
18
|
-
# Guard: block manual edits to .fabric/agents.meta.json
|
|
19
|
-
if git diff --cached --name-only | grep -q '^\.fabric/agents\.meta\.json$'; then
|
|
20
|
-
if [ "$FAB_ALLOW_META_EDIT" != '1' ]; then
|
|
21
|
-
echo '.fabric/agents.meta.json cannot be manually edited; use fab_update_registry or set FAB_ALLOW_META_EDIT=1' >&2
|
|
22
|
-
exit 1
|
|
23
|
-
fi
|
|
24
|
-
fi
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Fabric pre-commit hook: block manual edits to generated metadata.
|
|
3
|
+
|
|
4
|
+
if git diff --cached --name-only | grep -q '^\.fabric/agents\.meta\.json$'; then
|
|
5
|
+
if [ "$FAB_ALLOW_META_EDIT" != '1' ]; then
|
|
6
|
+
echo '.fabric/agents.meta.json cannot be manually edited; update .fabric/rules and run fabric doctor --fix, or set FAB_ALLOW_META_EDIT=1' >&2
|
|
7
|
+
exit 1
|
|
8
|
+
fi
|
|
9
|
+
fi
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# fabric-init — Canonical Skill Source
|
|
2
|
+
|
|
3
|
+
> This file is the single source of truth for the fabric-init skill.
|
|
4
|
+
> Do NOT edit the per-client SKILL.md files directly.
|
|
5
|
+
> Run `packages/cli/scripts/derive-skills.ts` to regenerate them from this source.
|
|
6
|
+
|
|
7
|
+
## Precondition
|
|
8
|
+
|
|
9
|
+
MUST: Read `.fabric/forensic.json` before taking any other action. If the file does not
|
|
10
|
+
exist, stop immediately and tell the user: run `fab init` first to generate the evidence
|
|
11
|
+
package.
|
|
12
|
+
|
|
13
|
+
MUST: Check `.fabric/init-context.json`. If it already exists, stop and report that this
|
|
14
|
+
repository appears to have completed initialization already.
|
|
15
|
+
|
|
16
|
+
MUST: Treat `.fabric/bootstrap/README.md` as the authoritative initialization guide for
|
|
17
|
+
the current repository.
|
|
18
|
+
|
|
19
|
+
MUST: Use `.fabric/forensic.json` and repository structure as evidence when deciding what
|
|
20
|
+
to do next.
|
|
21
|
+
|
|
22
|
+
MUST: Preserve protected tokens exactly as written — see the Protected Tokens section.
|
|
23
|
+
|
|
24
|
+
NEVER: Claim initialization is complete without having checked `.fabric/init-context.json`.
|
|
25
|
+
|
|
26
|
+
NEVER: Rewrite or translate protected tokens.
|
|
27
|
+
|
|
28
|
+
NEVER: Ignore `.fabric/bootstrap/README.md` when determining the next initialization step.
|
|
29
|
+
|
|
30
|
+
Treat the following state as initialization pending:
|
|
31
|
+
|
|
32
|
+
- `.fabric/forensic.json` exists
|
|
33
|
+
- `.fabric/init-context.json` does not exist
|
|
34
|
+
|
|
35
|
+
## Execution Flow — 3 Phases / 3 Rounds
|
|
36
|
+
|
|
37
|
+
### Phase 1 — Framework Confirmation (1 round, efficient)
|
|
38
|
+
|
|
39
|
+
Display a summary of `framework`, `topology.by_ext`, and `entry_points` from
|
|
40
|
+
`.fabric/forensic.json`. Ask the user 1–2 clarifying questions about the framework
|
|
41
|
+
architecture.
|
|
42
|
+
|
|
43
|
+
Example (Cocos Creator 3.x):
|
|
44
|
+
|
|
45
|
+
> I detected a Cocos Creator 3.8 project. Main scripts are in `assets/scripts` using the
|
|
46
|
+
> `@ccclass + extends Component` pattern. Please confirm: (1) Is this a TypeScript project
|
|
47
|
+
> (not JavaScript)? (2) Are node references injected mainly via `@property(Node)`, or via
|
|
48
|
+
> `find/getChildByName`?
|
|
49
|
+
|
|
50
|
+
Store the user's answers as verified framework assumptions before proceeding to Phase 2.
|
|
51
|
+
|
|
52
|
+
### Phase 2 — Invariant Extraction (1 round, critical)
|
|
53
|
+
|
|
54
|
+
Based on the `recommendations_for_skill` list in `.fabric/forensic.json`, ask the user
|
|
55
|
+
3–5 invariant questions covering three categories:
|
|
56
|
+
|
|
57
|
+
- `ban`: things that must never appear — e.g. `any`, `async` in `update()`, find-by-name
|
|
58
|
+
- `require`: things that must always be present — e.g. strict TypeScript, `@ccclass`
|
|
59
|
+
decorator, imports only from `cc`
|
|
60
|
+
- `protect`: directories or files that AI must not modify — typically
|
|
61
|
+
`assets/prefabs/**`, `assets/scenes/**`, `**/*.meta`
|
|
62
|
+
|
|
63
|
+
Principles:
|
|
64
|
+
|
|
65
|
+
- Ask only about invariants, not about preferences.
|
|
66
|
+
- Each question accepts only yes / no / a concrete rule — never accept vague answers.
|
|
67
|
+
- Do not auto-infer hard constraints the user has not confirmed.
|
|
68
|
+
|
|
69
|
+
### Phase 3 — Construction and Landing (1 round, automated)
|
|
70
|
+
|
|
71
|
+
#### 3.1 Write `.fabric/init-context.json`
|
|
72
|
+
|
|
73
|
+
Fields required:
|
|
74
|
+
|
|
75
|
+
- `framework`
|
|
76
|
+
- `architecture_patterns`
|
|
77
|
+
- `invariants`
|
|
78
|
+
- `domain_groups`
|
|
79
|
+
- `interview_trail`
|
|
80
|
+
- `forensic_ref`
|
|
81
|
+
|
|
82
|
+
Writing rules:
|
|
83
|
+
|
|
84
|
+
- `invariants[].type` MUST be one of `ban`, `require`, `protect`.
|
|
85
|
+
- `domain_groups` is inferred from `entry_points` and interview results.
|
|
86
|
+
- `interview_trail[]` MUST record the raw Q&A from Phase 1 and Phase 2.
|
|
87
|
+
- `forensic_ref` MUST be `.fabric/forensic.json`.
|
|
88
|
+
|
|
89
|
+
#### 3.2 Generate layered `AGENTS.md`
|
|
90
|
+
|
|
91
|
+
Root `AGENTS.md` requirements:
|
|
92
|
+
|
|
93
|
+
- MUST be within 300 lines.
|
|
94
|
+
- Structure:
|
|
95
|
+
- `# {projectName} — L0 AGENTS.md`
|
|
96
|
+
- `<!-- fab:index -->`: populated with the `domain_groups` index
|
|
97
|
+
- `## L0 AI Constraints`: derived from invariants, grouped by `ban`, `require`, `protect`
|
|
98
|
+
- `## @HUMAN`: protect paths and any human-declared protection rules
|
|
99
|
+
- `## L1 Candidate Notes`: candidate sub-module descriptions for each domain group
|
|
100
|
+
|
|
101
|
+
If `domain_groups.length >= 2`, generate a `{group_path}/AGENTS.md` for each group.
|
|
102
|
+
Maximum depth is L3; total nesting MUST NOT exceed 4 levels.
|
|
103
|
+
|
|
104
|
+
#### 3.3 Update `.fabric/agents.meta.json`
|
|
105
|
+
|
|
106
|
+
- The `nodes` tree MUST match the generated AGENTS hierarchy.
|
|
107
|
+
- Update the hash of every AGENTS.md file that was written.
|
|
108
|
+
- Maintain a consistent internal revision hash chain.
|
|
109
|
+
|
|
110
|
+
#### 3.4 Final output
|
|
111
|
+
|
|
112
|
+
List all generated files for the user and recommend running `fabric doctor --fix` for
|
|
113
|
+
ongoing maintenance.
|
|
114
|
+
|
|
115
|
+
## Hard Rules
|
|
116
|
+
|
|
117
|
+
- Zero TODO: never generate `TODO`, `TBD`, placeholders, or stubs in output files.
|
|
118
|
+
- No YAML frontmatter in outputs: generated `AGENTS.md` files MUST NOT contain YAML
|
|
119
|
+
frontmatter.
|
|
120
|
+
- Root `AGENTS.md` MUST be <= 300 lines.
|
|
121
|
+
- Total AGENTS nesting MUST be <= 4 levels.
|
|
122
|
+
- Do not auto-infer invariants the user has not confirmed.
|
|
123
|
+
- When content is uncertain, omit it — do not leave placeholders.
|
|
124
|
+
|
|
125
|
+
## Output Contract
|
|
126
|
+
|
|
127
|
+
On successful completion the following files exist or are updated:
|
|
128
|
+
|
|
129
|
+
| File | Action |
|
|
130
|
+
|------|--------|
|
|
131
|
+
| `.fabric/init-context.json` | Created with all required fields |
|
|
132
|
+
| `AGENTS.md` | Created (root L0) |
|
|
133
|
+
| `{group_path}/AGENTS.md` | Created for each domain group (when applicable) |
|
|
134
|
+
| `.fabric/agents.meta.json` | Updated nodes tree + hashes |
|
|
135
|
+
|
|
136
|
+
On failure or early termination the skill MUST leave no partial files. If a write fails
|
|
137
|
+
mid-sequence, report the failure and the exact file that was not written.
|
|
138
|
+
|
|
139
|
+
## Protected Tokens
|
|
140
|
+
|
|
141
|
+
The following tokens MUST be preserved exactly as shown — same casing, same punctuation,
|
|
142
|
+
never translated:
|
|
143
|
+
|
|
144
|
+
| Token | Type |
|
|
145
|
+
|-------|------|
|
|
146
|
+
| `AGENTS.md` | Filename |
|
|
147
|
+
| `FABRIC.md` | Filename |
|
|
148
|
+
| `.fabric/agents.meta.json` | Path |
|
|
149
|
+
| `.fabric/init-context.json` | Path |
|
|
150
|
+
| `.fabric/forensic.json` | Path |
|
|
151
|
+
| `.fabric/bootstrap/README.md` | Path |
|
|
152
|
+
| `MUST` | Keyword |
|
|
153
|
+
| `NEVER` | Keyword |
|
|
154
|
+
| `fab init` | CLI command |
|
|
155
|
+
| `fabric doctor --fix` | CLI command |
|
|
156
|
+
| `<!-- fab:index -->` | HTML comment marker |
|
|
157
|
+
| `@HUMAN` | Section marker |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"claude": {
|
|
3
|
+
"outputName": "fabric-init",
|
|
4
|
+
"frontmatter": {
|
|
5
|
+
"name": "fabric-init",
|
|
6
|
+
"description": "Use this skill when fab init just completed, when forensic.json was generated, or when the user is asking to initialize AGENTS.md. This skill runs a 3-phase initialization interview, writes .fabric/init-context.json, generates layered AGENTS.md, and updates .fabric/agents.meta.json.",
|
|
7
|
+
"allowed-tools": ["Read", "Write", "Glob", "Grep", "Bash"]
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"codex": {
|
|
11
|
+
"outputName": "fabric-init",
|
|
12
|
+
"frontmatter": {
|
|
13
|
+
"name": "fabric-init",
|
|
14
|
+
"description": "Use this skill when .fabric/forensic.json exists and this repository still needs the remaining Fabric initialization steps."
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/approve-YT4DEABS.js
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
padEnd
|
|
4
|
-
} from "./chunk-WWNXR34K.js";
|
|
5
|
-
import {
|
|
6
|
-
t
|
|
7
|
-
} from "./chunk-6ICJICVU.js";
|
|
8
|
-
|
|
9
|
-
// src/commands/approve.ts
|
|
10
|
-
import { createInterface } from "readline/promises";
|
|
11
|
-
import { stdin as input, stdout as output } from "process";
|
|
12
|
-
import { isAbsolute, resolve } from "path";
|
|
13
|
-
import { approveHumanLock, readHumanLock } from "@fenglimg/fabric-server";
|
|
14
|
-
import { defineCommand, renderUsage } from "citty";
|
|
15
|
-
var approveCommand = defineCommand({
|
|
16
|
-
meta: {
|
|
17
|
-
name: "approve",
|
|
18
|
-
description: t("cli.approve.description")
|
|
19
|
-
},
|
|
20
|
-
args: {
|
|
21
|
-
all: {
|
|
22
|
-
type: "boolean",
|
|
23
|
-
description: t("cli.approve.args.all.description"),
|
|
24
|
-
default: false
|
|
25
|
-
},
|
|
26
|
-
interactive: {
|
|
27
|
-
type: "boolean",
|
|
28
|
-
description: t("cli.approve.args.interactive.description"),
|
|
29
|
-
default: false
|
|
30
|
-
},
|
|
31
|
-
target: {
|
|
32
|
-
type: "string",
|
|
33
|
-
description: t("cli.approve.args.target.description"),
|
|
34
|
-
default: process.cwd()
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
async run({ args }) {
|
|
38
|
-
const target = normalizeTarget(args.target);
|
|
39
|
-
if (args.all === args.interactive) {
|
|
40
|
-
writeStdout(await renderUsage(approveCommand));
|
|
41
|
-
process.exitCode = 1;
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (args.all) {
|
|
45
|
-
await runApproveAll(target);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
await runApproveInteractive(target);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
var approve_default = approveCommand;
|
|
52
|
-
async function runApproveAll(projectRoot) {
|
|
53
|
-
const driftEntries = await readDriftEntries(projectRoot);
|
|
54
|
-
if (driftEntries.length === 0) {
|
|
55
|
-
writeStdout(t("cli.approve.no-drift"));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
let approvedCount = 0;
|
|
59
|
-
for (const entry of driftEntries) {
|
|
60
|
-
await approveEntry(projectRoot, entry);
|
|
61
|
-
approvedCount += 1;
|
|
62
|
-
}
|
|
63
|
-
writeStdout(t("cli.approve.summary", { approved: String(approvedCount), skipped: "0", total: String(driftEntries.length) }));
|
|
64
|
-
}
|
|
65
|
-
async function runApproveInteractive(projectRoot) {
|
|
66
|
-
const driftEntries = await readDriftEntries(projectRoot);
|
|
67
|
-
if (driftEntries.length === 0) {
|
|
68
|
-
writeStdout(t("cli.approve.no-drift"));
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
const rl = createInterface({ input, output });
|
|
72
|
-
let approvedCount = 0;
|
|
73
|
-
let skippedCount = 0;
|
|
74
|
-
try {
|
|
75
|
-
for (const entry of driftEntries) {
|
|
76
|
-
writeStdout(formatEntry(entry));
|
|
77
|
-
const answer = (await rl.question(t("cli.approve.prompt"))).trim().toLowerCase();
|
|
78
|
-
if (answer === "y" || answer === "yes") {
|
|
79
|
-
await approveEntry(projectRoot, entry);
|
|
80
|
-
approvedCount += 1;
|
|
81
|
-
writeStdout(t("cli.approve.approved-one", { location: formatLocation(entry) }));
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
skippedCount += 1;
|
|
85
|
-
writeStdout(t("cli.approve.skipped-one", { location: formatLocation(entry) }));
|
|
86
|
-
}
|
|
87
|
-
} finally {
|
|
88
|
-
rl.close();
|
|
89
|
-
}
|
|
90
|
-
writeStdout(
|
|
91
|
-
t("cli.approve.summary", {
|
|
92
|
-
approved: String(approvedCount),
|
|
93
|
-
skipped: String(skippedCount),
|
|
94
|
-
total: String(driftEntries.length)
|
|
95
|
-
})
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
async function readDriftEntries(projectRoot) {
|
|
99
|
-
const entries = await readHumanLock(projectRoot);
|
|
100
|
-
return entries.filter((entry) => entry.drift);
|
|
101
|
-
}
|
|
102
|
-
async function approveEntry(projectRoot, entry) {
|
|
103
|
-
await approveHumanLock(projectRoot, {
|
|
104
|
-
file: entry.file,
|
|
105
|
-
start_line: entry.start_line,
|
|
106
|
-
end_line: entry.end_line,
|
|
107
|
-
new_hash: entry.current_hash
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
function normalizeTarget(targetInput) {
|
|
111
|
-
return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
|
|
112
|
-
}
|
|
113
|
-
function formatEntry(entry) {
|
|
114
|
-
return [
|
|
115
|
-
formatLocation(entry),
|
|
116
|
-
`${padEnd(t("cli.approve.table.expected"), 10)} ${shortenHash(entry.hash)}`,
|
|
117
|
-
`${padEnd(t("cli.approve.table.current"), 10)} ${shortenHash(entry.current_hash)}`
|
|
118
|
-
].join("\n");
|
|
119
|
-
}
|
|
120
|
-
function formatLocation(entry) {
|
|
121
|
-
return `${entry.file}:${entry.start_line}-${entry.end_line}`;
|
|
122
|
-
}
|
|
123
|
-
function shortenHash(value) {
|
|
124
|
-
if (value === "missing") {
|
|
125
|
-
return t("cli.shared.missing");
|
|
126
|
-
}
|
|
127
|
-
return value.slice(0, 15);
|
|
128
|
-
}
|
|
129
|
-
function writeStdout(message) {
|
|
130
|
-
process.stdout.write(`${message}
|
|
131
|
-
`);
|
|
132
|
-
}
|
|
133
|
-
export {
|
|
134
|
-
approveCommand,
|
|
135
|
-
approve_default as default,
|
|
136
|
-
runApproveAll,
|
|
137
|
-
runApproveInteractive
|
|
138
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
bootstrapCommand,
|
|
4
|
-
bootstrap_default,
|
|
5
|
-
installBootstrap
|
|
6
|
-
} from "./chunk-T2WJF5I3.js";
|
|
7
|
-
import "./chunk-QSAEGVKE.js";
|
|
8
|
-
import "./chunk-AEOYCVBG.js";
|
|
9
|
-
import "./chunk-WWNXR34K.js";
|
|
10
|
-
import "./chunk-BEKSXO5N.js";
|
|
11
|
-
import "./chunk-6ICJICVU.js";
|
|
12
|
-
export {
|
|
13
|
-
bootstrapCommand,
|
|
14
|
-
bootstrap_default as default,
|
|
15
|
-
installBootstrap
|
|
16
|
-
};
|
package/dist/chunk-2YW5CJ32.js
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
t
|
|
4
|
-
} from "./chunk-6ICJICVU.js";
|
|
5
|
-
|
|
6
|
-
// src/commands/ledger-append.ts
|
|
7
|
-
import { execSync } from "child_process";
|
|
8
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "fs";
|
|
9
|
-
import { basename, isAbsolute, resolve } from "path";
|
|
10
|
-
import { getLedgerPath, getLegacyLedgerPath, LEGACY_LEDGER_PATH, LEDGER_PATH } from "@fenglimg/fabric-server";
|
|
11
|
-
import { defineCommand } from "citty";
|
|
12
|
-
var INITIAL_PARENT_SHA = "root";
|
|
13
|
-
var ledgerAppendCommand = defineCommand({
|
|
14
|
-
meta: {
|
|
15
|
-
name: "ledger-append",
|
|
16
|
-
description: t("cli.ledger-append.description")
|
|
17
|
-
},
|
|
18
|
-
args: {
|
|
19
|
-
target: {
|
|
20
|
-
type: "string",
|
|
21
|
-
description: t("cli.ledger-append.args.target.description"),
|
|
22
|
-
default: process.cwd()
|
|
23
|
-
},
|
|
24
|
-
staged: {
|
|
25
|
-
type: "boolean",
|
|
26
|
-
description: t("cli.ledger-append.args.staged.description"),
|
|
27
|
-
default: false
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
async run({ args }) {
|
|
31
|
-
const target = normalizeTarget(args.target);
|
|
32
|
-
assertExistingDirectory(target);
|
|
33
|
-
if (!args.staged) {
|
|
34
|
-
writeStderr(t("cli.ledger-append.requires-staged"));
|
|
35
|
-
process.exitCode = 1;
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const stagedFiles = getStagedFiles(target).filter((file) => file !== LEGACY_LEDGER_PATH && file !== LEDGER_PATH);
|
|
39
|
-
if (stagedFiles.length === 0) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
const intent = deriveIntent(stagedFiles);
|
|
43
|
-
const diffStat = readDiffStat(target).trim();
|
|
44
|
-
const entry = {
|
|
45
|
-
ts: Date.now(),
|
|
46
|
-
source: "human",
|
|
47
|
-
parent_sha: readParentSha(target),
|
|
48
|
-
intent,
|
|
49
|
-
affected_paths: stagedFiles,
|
|
50
|
-
diff_stat: diffStat
|
|
51
|
-
};
|
|
52
|
-
if (hasMatchingTailEntry(target, entry)) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const ledgerPath = getLedgerPath(target);
|
|
56
|
-
mkdirSync(resolve(target, ".fabric"), { recursive: true });
|
|
57
|
-
appendFileSync(ledgerPath, `${JSON.stringify(entry)}
|
|
58
|
-
`, "utf8");
|
|
59
|
-
execGit(target, `git add ${LEDGER_PATH}`);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
var ledger_append_default = ledgerAppendCommand;
|
|
63
|
-
function normalizeTarget(targetInput) {
|
|
64
|
-
return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
|
|
65
|
-
}
|
|
66
|
-
function assertExistingDirectory(target) {
|
|
67
|
-
if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
68
|
-
throw new Error(t("cli.shared.target-invalid", { target }));
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
function getStagedFiles(target) {
|
|
72
|
-
const output = execGit(target, "git diff --cached --name-only --no-renames");
|
|
73
|
-
return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
74
|
-
}
|
|
75
|
-
function readDiffStat(target) {
|
|
76
|
-
return execGit(target, "git diff --cached --stat");
|
|
77
|
-
}
|
|
78
|
-
function readParentSha(target) {
|
|
79
|
-
try {
|
|
80
|
-
return execGit(target, "git rev-parse --short HEAD").trim();
|
|
81
|
-
} catch {
|
|
82
|
-
return INITIAL_PARENT_SHA;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function deriveIntent(stagedFiles) {
|
|
86
|
-
const explicitIntent = process.env.FABRIC_INTENT?.trim();
|
|
87
|
-
if (explicitIntent) {
|
|
88
|
-
return explicitIntent;
|
|
89
|
-
}
|
|
90
|
-
const uniqueNames = Array.from(new Set(stagedFiles.map((file) => basename(file))));
|
|
91
|
-
const head = uniqueNames.slice(0, 2).join(", ");
|
|
92
|
-
const suffix = uniqueNames.length > 2 ? t("cli.ledger-append.intent.auto-more", { count: String(uniqueNames.length - 2) }) : "";
|
|
93
|
-
return t("cli.ledger-append.intent.auto", { head, suffix });
|
|
94
|
-
}
|
|
95
|
-
function hasMatchingTailEntry(target, entry) {
|
|
96
|
-
const ledgerPath = resolveTailLedgerPath(target);
|
|
97
|
-
if (!existsSync(ledgerPath)) {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
const tail = readFileSync(ledgerPath, "utf8").trim().split(/\r?\n/).filter(Boolean).reverse().find((line) => isLedgerEntryLine(line));
|
|
101
|
-
if (!tail) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
try {
|
|
105
|
-
const parsed = JSON.parse(tail);
|
|
106
|
-
return parsed.parent_sha === entry.parent_sha && parsed.intent === entry.intent && Array.isArray(parsed.affected_paths) && parsed.affected_paths.length === entry.affected_paths.length && parsed.affected_paths.every((value, index) => value === entry.affected_paths[index]) && normalizeDiffStat(parsed.diff_stat) === normalizeDiffStat(entry.diff_stat);
|
|
107
|
-
} catch {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
function resolveTailLedgerPath(target) {
|
|
112
|
-
const canonicalPath = getLedgerPath(target);
|
|
113
|
-
if (existsSync(canonicalPath)) {
|
|
114
|
-
return canonicalPath;
|
|
115
|
-
}
|
|
116
|
-
return getLegacyLedgerPath(target);
|
|
117
|
-
}
|
|
118
|
-
function isLedgerEntryLine(line) {
|
|
119
|
-
try {
|
|
120
|
-
const parsed = JSON.parse(line);
|
|
121
|
-
return parsed.kind !== "mcp-event";
|
|
122
|
-
} catch {
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function normalizeDiffStat(diffStat) {
|
|
127
|
-
if (typeof diffStat !== "string") {
|
|
128
|
-
return "";
|
|
129
|
-
}
|
|
130
|
-
return diffStat.split(/\r?\n/).map((line) => line.trim()).map((line) => line.replace(/\s+\|\s+/g, " | ")).map((line) => line.replace(/\s+/g, " ")).filter((line) => line.length > 0).filter((line) => !line.includes(LEGACY_LEDGER_PATH)).filter((line) => !line.includes(LEDGER_PATH)).filter((line) => !/\d+ files? changed(?:, \d+ insertions?\(\+\))?(?:, \d+ deletions?\(-\))?$/.test(line.trim())).join("\n");
|
|
131
|
-
}
|
|
132
|
-
function execGit(target, command) {
|
|
133
|
-
return execSync(command, {
|
|
134
|
-
cwd: target,
|
|
135
|
-
encoding: "utf8",
|
|
136
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
function writeStderr(message) {
|
|
140
|
-
process.stderr.write(`${message}
|
|
141
|
-
`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export {
|
|
145
|
-
ledgerAppendCommand,
|
|
146
|
-
ledger_append_default
|
|
147
|
-
};
|