faf-cli 7.12.0 → 7.13.0

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.
@@ -1,4 +1,5 @@
1
1
  import type { FafData } from '../core/types.js';
2
+ import { type RenderedResult } from '../core/render-hash.js';
2
3
  import { type ServerCardOptions } from './servercard.js';
3
4
  /** A2A extension URI — dereference, not the MCP `_meta` key `one.faf/context`. */
4
5
  export declare const A2A_CONTEXT_URI = "https://faf.one/context";
@@ -111,9 +112,24 @@ export declare function buildA2ACard(fafa: FafaDoc, faf: FafData, opts?: Project
111
112
  /** @deprecated Use {@link buildA2ACard}. Removed in the next major. */
112
113
  export declare const generateA2ACard: typeof buildA2ACard;
113
114
  export declare function catalogEntriesFor(fafa: FafaDoc, faf: FafData, opts?: ProjectCardsOptions): CatalogEntry[];
114
- /** Upsert projector entries into an existing catalog. Leaves unknown rows alone.
115
- * On match, only url / type / updatedAt move — host copy (title, tags) stays. */
115
+ /** Upsert projector entries into an existing catalog. Leaves every other row
116
+ * alone: a row is faf's only when its identifier is exactly faf's (never by
117
+ * type or URL). On match, only url / type / updatedAt move — host copy
118
+ * (title, tags) stays; any other faf row is appended. */
116
119
  export declare function upsertCatalog(existing: AiCatalog | undefined, incoming: CatalogEntry[]): AiCatalog;
120
+ /**
121
+ * {@link upsertCatalog} as a text edit of the catalog's JSON: faf's own rows
122
+ * (identifier exactly faf's) get their url / type / updatedAt values changed
123
+ * in place, faf's other rows are appended after the last entry, and every
124
+ * other byte — your rows, their order and layout, other keys — stays. With no
125
+ * text (no catalog yet) a new catalog is returned. Throws a JsonEditError,
126
+ * changing nothing, when the catalog cannot be edited that way (not a JSON
127
+ * object, `entries` not an array, faf's row there twice, …).
128
+ */
129
+ export declare function upsertCatalogText(text: string | null, incoming: CatalogEntry[]): {
130
+ text: string;
131
+ changed: boolean;
132
+ };
117
133
  export declare function projectCards(input: {
118
134
  faf: FafData;
119
135
  fafa?: FafaDoc;
@@ -122,5 +138,38 @@ export declare function projectCards(input: {
122
138
  }): ProjectedCards;
123
139
  /** Byte-identical context block on every emitted door. */
124
140
  export declare function assertSameBlock(cards: ProjectedCards): void;
125
- export declare function writeJson(path: string, value: unknown): void;
141
+ /** True when `bytes` are an A2A Agent Card faf wrote: JSON whose
142
+ * `capabilities.extensions` carry the FAF context extension
143
+ * ({@link A2A_CONTEXT_URI}), as every card faf has written does. */
144
+ export declare function hasA2ACardMark(bytes: Uint8Array): boolean;
145
+ /** True when `bytes` carry any of faf's card marks: the MCP Server Card's
146
+ * `_meta["one.faf/context"]`, a registry server.json's publisher-provided
147
+ * `one.faf/context`, or the A2A card's FAF context extension. */
148
+ export declare function hasFafCardMark(bytes: Uint8Array): boolean;
149
+ /** Options for {@link writeJson}. */
150
+ export interface WriteJsonOptions {
151
+ /** True when the bytes already at the path carry faf's older mark (a file
152
+ * faf wrote before render hashes). Default: one of faf's card marks
153
+ * ({@link hasFafCardMark}). */
154
+ owns?: (existing: Buffer) => boolean;
155
+ /** faf's mark in words, for the refusal. */
156
+ mark?: string;
157
+ /** Replace a file faf cannot prove it wrote anyway — the explicit overwrite (`--force`). */
158
+ force?: boolean;
159
+ }
160
+ /** Write `value` as JSON (2-space, final newline) with faf's render hash at
161
+ * `_meta["one.faf/render"]` — atomically, and never through a link that
162
+ * leaves `root` (default: the file's own folder) or dangles. The folder is
163
+ * created when missing, never through a link that leaves `root`. A file
164
+ * already there is replaced only when it is byte for byte what faf last wrote
165
+ * (its render hash still fits); a file edited since, one without faf's mark,
166
+ * or one from before 7.13 that is not exactly this JSON is refused
167
+ * (SafePathError `not-owned`) and left byte for byte, unless `force`.
168
+ * Returns what it did: `created`, `updated`, or `unchanged` (the file
169
+ * already held exactly these bytes, and nothing was written). */
170
+ export declare function writeJson(path: string, value: unknown, root?: string, write?: WriteJsonOptions): RenderedResult;
171
+ /** True when `bytes` are JSON laid out exactly as faf writes it (2-space
172
+ * JSON and a final newline), so re-writing it loses nothing: no hand
173
+ * formatting, no repeated key, no number JSON cannot hold exactly. */
174
+ export declare function isFafJsonLayout(bytes: Uint8Array): boolean;
126
175
  export declare function parseTargets(raw?: string): CardTarget[] | undefined;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Claude Code auto-memory — the MEMORY.md Claude Code loads at the start of a
3
+ * session: `<config>/projects/<project-id>/memory/MEMORY.md`.
4
+ *
5
+ * Moved here from claude-faf-mcp (its tri-sync writer and path resolver) so
6
+ * every FAF tool shares one copy, with what the audit found fixed.
7
+ *
8
+ * WHERE the file is — Claude Code's own rule (mirrored from Claude Code 2.1.x):
9
+ * - Project root: the canonical git root of the folder — the first folder at
10
+ * or above it that has a `.git` (a folder or a file). A linked worktree
11
+ * resolves to its main checkout, so every worktree shares one memory. A
12
+ * folder that is not in git is its own root.
13
+ * - Project id: that path with every character outside [a-zA-Z0-9] replaced
14
+ * by `-` (so `/home/me/my_app` → `-home-me-my-app`). An id longer than
15
+ * 200 characters is cut to 200 and gets `-` plus a base-36 hash of the
16
+ * full path.
17
+ * - Base: CLAUDE_CODE_REMOTE_MEMORY_DIR when set, else Claude's config
18
+ * folder: CLAUDE_CONFIG_DIR, else ~/.claude.
19
+ * Not read: Claude Code's `autoMemoryDirectory` setting (or any other
20
+ * override of the whole memory folder). When one is set, pass `memoryDir`.
21
+ *
22
+ * WHAT faf writes — one managed block (the same markers and injector as
23
+ * CLAUDE.md). Everything else in the file is Claude's and is kept byte for
24
+ * byte, CRLF line ends and a BOM included:
25
+ * - no MEMORY.md → the file is created with the block
26
+ * - faf's block is there → only the text between its marker lines changes
27
+ * - claude-faf-mcp's earlier section (its `# Project Context (from
28
+ * project.faf)` line through its `*This section is managed by tri-sync.`
29
+ * line, both whole lines) → replaced in place by the block, once
30
+ * - anything else → the block goes on top; nothing is removed
31
+ * Markers match whole lines only (faf's own exactly), never substrings, and
32
+ * only outside fenced code, raw HTML blocks and multi-line HTML comments, as
33
+ * both CommonMark and a plain column-0 reading see them (a note that quotes
34
+ * the block or the old section is an example, not a marker; see inject.ts).
35
+ * When the block goes on top of a file whose older faf block sits in such a
36
+ * region, the result's warnings say so in one line. Only a missing file reads
37
+ * as "no file": any other read error is thrown and nothing is written, and a
38
+ * file that is not UTF-8 is refused. The write is atomic, is refused if the
39
+ * file changed on disk after faf read it, and a run that changes nothing
40
+ * writes nothing.
41
+ */
42
+ import type { FafData } from '../core/types.js';
43
+ export interface ClaudeMemoryOptions {
44
+ /** Claude Code's config folder. Default: CLAUDE_CODE_REMOTE_MEMORY_DIR,
45
+ * else CLAUDE_CONFIG_DIR, else ~/.claude. */
46
+ configDir?: string;
47
+ /** The memory folder itself, when you know better than the rule (Claude
48
+ * Code's `autoMemoryDirectory` setting, say). Skips the resolver. */
49
+ memoryDir?: string;
50
+ }
51
+ /** What `writeClaudeMemory` did. */
52
+ export type ClaudeMemoryAction = 'created' | 'updated' | 'migrated' | 'added' | 'unchanged';
53
+ export interface ClaudeMemoryResult {
54
+ /** The MEMORY.md (its real path). */
55
+ path: string;
56
+ /**
57
+ * - `created` there was no MEMORY.md; it now holds the block
58
+ * - `updated` faf's block was replaced in place
59
+ * - `migrated` claude-faf-mcp's earlier section was replaced by the block
60
+ * - `added` the file had neither; the block went on top
61
+ * - `unchanged` the block was already exactly this; nothing was written
62
+ */
63
+ action: ClaudeMemoryAction;
64
+ /** False when nothing was written (`unchanged`). */
65
+ written: boolean;
66
+ /** Every byte outside faf's block is still in the file, in place — checked
67
+ * by reading the file back after the write. */
68
+ preserved: boolean;
69
+ /** Lines in the file now. */
70
+ lines: number;
71
+ /** Plain-words notes (e.g. lines past the 200 Claude Code loads). */
72
+ warnings: string[];
73
+ }
74
+ /** What is in the MEMORY.md Claude Code loads for a project (read only). */
75
+ export interface ClaudeMemoryStatus {
76
+ /** The MEMORY.md path (whether or not it exists). */
77
+ path: string;
78
+ exists: boolean;
79
+ /** Lines in the file. */
80
+ lines: number;
81
+ /** faf's block is in the file. */
82
+ hasBlock: boolean;
83
+ /** Lines in faf's block, its marker lines included (0 when there is none). */
84
+ blockLines: number;
85
+ /** claude-faf-mcp's earlier tri-sync section is in the file (the next write replaces it). */
86
+ hasLegacySection: boolean;
87
+ /** Non-blank lines outside faf's block: Claude's own notes. */
88
+ otherLines: number;
89
+ /** Plain-words notes (e.g. lines past the 200 Claude Code loads). */
90
+ warnings: string[];
91
+ }
92
+ /**
93
+ * Claude Code's folder name for a project path: every character outside
94
+ * [a-zA-Z0-9] becomes `-`; past 200 characters the id is cut to 200 and gets
95
+ * `-` plus a base-36 hash of the full path. `/home/me/my_app` → `-home-me-my-app`.
96
+ * Pass the project root (see {@link claudeProjectRoot}), not any folder.
97
+ */
98
+ export declare function claudeProjectId(projectRoot: string): string;
99
+ /**
100
+ * The project root Claude Code keys a folder's memory by: the canonical git
101
+ * root (a linked worktree → its main checkout), or the folder itself when it
102
+ * is not in git. The folder is resolved on disk first (links followed), as
103
+ * Claude Code sees its working folder; the result is NFC-normalized.
104
+ */
105
+ export declare function claudeProjectRoot(dir: string): string;
106
+ /** `<config>/projects/<project-id>/memory` for the project `dir` is in. */
107
+ export declare function resolveClaudeMemoryDir(dir: string, opts?: ClaudeMemoryOptions): string;
108
+ /** The MEMORY.md Claude Code loads for the project `dir` is in. */
109
+ export declare function resolveClaudeMemoryPath(dir: string, opts?: ClaudeMemoryOptions): string;
110
+ /**
111
+ * The block body faf keeps in MEMORY.md, from .faf data: a short project
112
+ * summary (name, goal, language, type), the filled stack slots, the 6 Ws,
113
+ * and up to 12 commands and key files. Kept short on purpose — Claude Code
114
+ * loads only the first 200 lines, and the rest of the file is Claude's.
115
+ */
116
+ export declare function renderClaudeMemory(data: FafData): string;
117
+ /**
118
+ * Read what is in the MEMORY.md Claude Code loads for the project `dir` is in:
119
+ * whether faf's block (or claude-faf-mcp's earlier section) is there, and how
120
+ * many lines are Claude's own. Reads only; never creates or writes anything.
121
+ * A file that exists but cannot be read throws.
122
+ */
123
+ export declare function claudeMemoryStatus(dir: string, opts?: ClaudeMemoryOptions): ClaudeMemoryStatus;
124
+ /**
125
+ * Write faf's block into the MEMORY.md Claude Code loads for the project `dir`
126
+ * is in (see the file header for the path rule and what is kept). Creates the
127
+ * memory folder when it is not there yet. Throws — having written nothing —
128
+ * when the file cannot be read (other than not existing), is not UTF-8, is a
129
+ * link that leaves the memory folder, changed on disk while faf was writing,
130
+ * cannot be written (the original is kept), or is one where faf cannot place
131
+ * its block where its next run finds it again (SafePathError `unplaceable`,
132
+ * the same one-line refusal as injectFafBlock).
133
+ */
134
+ export declare function writeClaudeMemory(dir: string, data: FafData, opts?: ClaudeMemoryOptions): ClaudeMemoryResult;
@@ -1,5 +1,9 @@
1
1
  import type { FafData } from '../core/types.js';
2
- /** Read CLAUDE.md from a directory */
2
+ /** Read CLAUDE.md from a directory. A CLAUDE.md link that leaves the
3
+ * directory, or leads to a file that is not an AI context file, is refused
4
+ * (SafePathError) and nothing is read; CLAUDE.md → AGENTS.md is read
5
+ * through. A CLAUDE.md that is not UTF-8 is refused too (`faf sync --direction
6
+ * pull` writes what it reads into project.faf). */
3
7
  export declare function readClaudeMd(dir: string): string | null;
4
8
  /** Write CLAUDE.md — non-destructive: injects/updates the faf block, preserves the rest. */
5
9
  export declare function writeClaudeMd(dir: string, content: string): void;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * CommonMark's block structure, line by line — just enough of it to say which
3
+ * lines Markdown shows as code, or hides in a raw HTML block or a multi-line
4
+ * comment. faf uses it to tell its own marker lines from marker examples.
5
+ *
6
+ * The algorithm is the reference implementation's (commonmark.js, spec 0.31):
7
+ * each line first continues the open containers (block quotes, list items —
8
+ * a list item's lines sit at its content column, a tab after the marker
9
+ * advancing to the next multiple of 4), then may open new blocks, then is
10
+ * added to the innermost open block or continues a paragraph lazily. Only
11
+ * what decides block structure is kept: no inline parsing, no link reference
12
+ * definitions, no tables.
13
+ *
14
+ * A reader made with `containers: false` reads every line at column 0 with no
15
+ * block quotes and no list items — the plain reading a text editor, an older
16
+ * faf or another tool may apply. faf takes a block as its own only when both
17
+ * readings agree (see inject.ts).
18
+ *
19
+ * What a line counts as hidden in:
20
+ * - fenced code (its opening and closing lines included) and indented code;
21
+ * - an HTML block of types 1-5 — <pre>/<script>/<style>/<textarea> up to the
22
+ * closing tag, a comment up to `-->`, <?…?>, <!X…>, <![CDATA[…]]> — except
23
+ * a comment that opens and closes on the one line (faf's own marker
24
+ * lines are such comments);
25
+ * - not an HTML block of types 6 or 7 (<div>, <details>, any lone tag): a
26
+ * comment inside raw HTML is still a comment. No fence or other block
27
+ * opens inside one until a blank line ends it.
28
+ *
29
+ * Speed: each line costs time in proportion to its length, however deep the
30
+ * nesting — the reader never re-reads the rest of a line per nesting level.
31
+ * Past MAX_OPEN_CONTAINERS open block quotes, lists and list items the reader
32
+ * stops reading (`tooDeep`): faf cannot say for sure where such a file's
33
+ * regions are, so the file is the user's — faf's block goes on top and every
34
+ * byte stays.
35
+ */
36
+ /** More open block quotes, lists and list items than this, and a reader stops
37
+ * reading (see {@link BlockReader.tooDeep}). */
38
+ export declare const MAX_OPEN_CONTAINERS = 100;
39
+ /** What hides a line: its region, as faf names it to the user. */
40
+ export type HiddenIn = 'code fence' | 'indented code' | 'html' | 'comment' | `<${string}>`;
41
+ /** Feeds a text line by line (terminators removed) and says which lines are hidden. */
42
+ export declare class BlockReader {
43
+ private readonly containers;
44
+ private stack;
45
+ private ln;
46
+ private offset;
47
+ private column;
48
+ private nextNonspace;
49
+ private nextNonspaceColumn;
50
+ private indent;
51
+ private indented;
52
+ private blank;
53
+ private allClosed;
54
+ private matched;
55
+ private htmlStarted;
56
+ private hiddenIn;
57
+ /** Where the last whitespace scan of this line started (-1: none yet). */
58
+ private scannedFrom;
59
+ /** Per line, lazily: 1 where the rest of the line is a thematic break. */
60
+ private breaks;
61
+ private deep;
62
+ /** With a limit (see {@link overflowAt}): open containers allowed, and
63
+ * where the line first opened one past it (-1: nowhere yet). */
64
+ private limit;
65
+ private overflow;
66
+ private readonly starts;
67
+ /** `containers: false` — the plain column-0 reading: no block quotes, no list items. */
68
+ constructor(containers?: boolean);
69
+ /** An independent copy, for trying lines out. */
70
+ clone(): BlockReader;
71
+ /**
72
+ * Where in `ln` a reader in this state would open a block quote, list or
73
+ * list item past `limit` open ones — the offset of that quote's `>` or that
74
+ * item's marker — or -1 when the line leaves at most `limit` open. This
75
+ * reader does not move: a copy reads the line.
76
+ */
77
+ overflowAt(ln: string, limit: number): number;
78
+ /** True once more than MAX_OPEN_CONTAINERS block quotes, lists and list
79
+ * items were open at once. The reader then stops reading: what it says
80
+ * about that line and every later one means nothing, and a caller treats
81
+ * the file as one it cannot read for sure. */
82
+ get tooDeep(): boolean;
83
+ /** Read one line (no terminator). Returns what hides it, or null when it is shown as text. */
84
+ line(ln: string): HiddenIn | null;
85
+ /** The line that would close the region a column-0 line would now be hidden
86
+ * in (a fence or an HTML block of types 1-5 at the top level), or null. */
87
+ closer(): string | null;
88
+ private get tip();
89
+ /** Step 1: the open containers the line continues. Returns the index of the
90
+ * last one it continues, or -1 when it closed a fence. */
91
+ private continueContainers;
92
+ /** Step 2: the blocks the line opens, from the container at `index`. Returns the block the rest of the line goes to. */
93
+ private openBlocks;
94
+ /** A block just opened at `at` (`res` 1: a container): with a limit set,
95
+ * remember the first place the line went past it (below the document
96
+ * only containers are open when a container is added). */
97
+ private noteOverflow;
98
+ /** Step 3: the rest of the line goes to `block`. */
99
+ private addText;
100
+ private addHtmlLine;
101
+ /** Does the line continue the open block `b` (at `index`)? 0 yes, 1 no, 2 it closed a fence. */
102
+ private continues;
103
+ /** The document and a list always continue; a heading or a break never
104
+ * does; a paragraph and an HTML block of type 6 or 7 end at a blank line. */
105
+ private continueOther;
106
+ private continueQuote;
107
+ private continueItem;
108
+ private continueFence;
109
+ private continueIndentedCode;
110
+ /** The character at the next non-space position ('' at the line's end). */
111
+ private get ch();
112
+ /** True when the rest of the line, from the next non-space position, is a
113
+ * thematic break (see {@link thematicBreaks}; worked out once per line). */
114
+ private breakAhead;
115
+ private startQuote;
116
+ private startHeading;
117
+ private startFence;
118
+ private startHtml;
119
+ private startSetext;
120
+ private startBreak;
121
+ private startItem;
122
+ private startIndentedCode;
123
+ /** The list marker at the reader's position: its kind, where it sits and
124
+ * its content column (padding); null when there is none. */
125
+ private listMarker;
126
+ /** A bullet or an ordered marker followed by a space, a tab or the line's end. */
127
+ private markerAt;
128
+ private markerKind;
129
+ /** The columns from the marker to the item's content: 1-4 spaces (or tabs
130
+ * to that many columns) after it; with 5 or more, or none (a blank item),
131
+ * the content starts one column after the marker. */
132
+ private paddingAfter;
133
+ private addChild;
134
+ private closeUnmatched;
135
+ /** The next non-space position from where the reader stands, its column,
136
+ * and the indent to it. A second call from inside the same run of spaces
137
+ * and tabs reuses the first scan (the column a run ends at does not depend
138
+ * on where in the run the scan starts), so a line is scanned once however
139
+ * many containers it continues. */
140
+ private findNextNonspace;
141
+ private advanceNextNonspace;
142
+ /** Move `count` characters on (or, with `columns`, `count` columns: a tab may be taken in part). */
143
+ private advanceOffset;
144
+ }
@@ -22,5 +22,6 @@ export declare function renderCopilotInstructions(data: FafData): string;
22
22
  /**
23
23
  * Write `.github/copilot-instructions.md` — non-destructive (injects/updates the
24
24
  * faf block, preserves the rest). Creates the `.github/` directory if absent.
25
+ * The project folder is the boundary: a `.github` linked outside it is refused.
25
26
  */
26
27
  export declare function writeCopilotInstructions(dir: string, data: FafData): void;
@@ -1,24 +1,136 @@
1
+ import { type Document } from 'yaml';
1
2
  import type { FafData } from '../core/types.js';
2
- /** Read and parse a .faf file */
3
+ import { type KeptAlias } from '../core/yaml-edit.js';
4
+ /** Run `call` — a call into faf's scoring kernel with the text of the .faf
5
+ * at `path` (or the bytes of the .fafb there) — and turn a rejection by the
6
+ * kernel into the one-line refusal. The kernel throws a bare string (or an
7
+ * Error) for text it cannot read, some of which yaml reads (a 30-digit
8
+ * integer, nesting past the kernel's depth limit); that becomes a
9
+ * SafePathError (`not-yaml`): "<file>: faf's scoring kernel could not read
10
+ * it (<reason>) — faf left it unchanged". A SafePathError from `call` passes
11
+ * through as it is. Every command that hands project.faf (or a .fafb) to
12
+ * the kernel calls it through here, after it has read the file as a .faf
13
+ * ({@link readFaf}, or {@link readFafFromString} with the path); one that
14
+ * edits project.faf (`faf auto`, `faf go`, `faf sync --direction pull`)
15
+ * asks the kernel before it writes, so the line is true. `faf diff` and
16
+ * `faf log` score a version the kernel cannot read as 0 instead. */
17
+ export declare function withKernel<T>(path: string, call: () => T): T;
18
+ /** Read and parse a .faf file. Always a mapping: an empty file reads as `{}`;
19
+ * a file that parses to a scalar or a list throws a SafePathError
20
+ * (`not-yaml`, one line) instead of handing callers a value they would
21
+ * spread into character keys. A file
22
+ * that is not valid YAML throws a SafePathError (`not-yaml`): "<file> is not
23
+ * valid YAML (<reason>, line N) — faf left it unchanged". A link that
24
+ * leaves the folder, or does not end at a .faf/.fafm file, is refused, and so
25
+ * is a file that is not UTF-8. The data remembers the text it was read from:
26
+ * writeFaf refuses to write it back over a file that changed since. */
3
27
  export declare function readFaf(path: string): FafData;
4
- /** Write a .faf file from data.
28
+ /** Serialize .faf data to YAML text — the exact bytes writeFaf writes for a
29
+ * new file (an existing file is updated in place instead).
5
30
  *
6
31
  * If `data._meta.found` is present, it's stripped before serialization and
7
32
  * rendered as a `# found: <list>` YAML comment next to the `type:` field —
8
33
  * Glass Hood doctrine: the user sees WHY the cli classified the project as
9
- * it did. `_meta` is a runtime hint, never a serialized .faf field. */
10
- /** Serialize .faf data to YAML text (the exact bytes writeFaf would write).
11
- * Strips runtime `_meta` and renders its `found` rationale as a `# found:`
12
- * comment by `type:`. Used by writeFaf and by `faf git --stdout`. */
34
+ * it did. `_meta` is a runtime hint, never a serialized .faf field. Used by
35
+ * writeFaf and by `faf git --stdout`. */
13
36
  export declare function serializeFaf(data: FafData): string;
14
- export declare function writeFaf(path: string, data: FafData): void;
15
- /** Read raw YAML text from a .faf file */
37
+ /** What {@link updateFafFile} did. */
38
+ export interface UpdateFafResult {
39
+ /** True when the file changed on disk; false when the change was a no-op and
40
+ * nothing was written. */
41
+ written: boolean;
42
+ /** The file's text after the update (its original text when nothing changed). */
43
+ text: string;
44
+ /** Aliases the change would have replaced, left as written: `path` is where
45
+ * (`stack`), `alias` what the file says there (`*base`). faf never replaces
46
+ * or expands an alias; the change at that path is not written. */
47
+ keptAliases?: KeptAlias[];
48
+ }
49
+ export type { KeptAlias };
50
+ /**
51
+ * Update an existing .faf in place, keeping every byte the change does not
52
+ * touch. The file is parsed with yaml's `parseDocument`; `mutate` changes the
53
+ * Document through its node APIs (`doc.setIn(['stack', 'database'], 'Postgres')`,
54
+ * `doc.deleteIn([...])`, `doc.getIn([...], true)`); only the text of the nodes
55
+ * that changed is rewritten. Comments, blank lines, key order, quoting, scalar
56
+ * source text (`version: 1.10`, `0x1F90`, a 20-digit integer), anchors, unknown
57
+ * keys (a user's own `_meta` included), CRLF, a BOM and the `%YAML` / `---` /
58
+ * `...` framing survive — nothing folds at 80 columns. When the change leaves
59
+ * the data as it was, nothing is written (`written: false`), even if faf's own
60
+ * layout of the file would differ.
61
+ *
62
+ * An alias the change replaces (`doc.set('stack', …)` over `stack: *base`)
63
+ * is put back: faf never replaces or expands an alias, so that path stays as
64
+ * written and is listed in `keptAliases`; the rest of the change is written.
65
+ *
66
+ * The file must exist. The same link rules as {@link readFaf} (a link must
67
+ * stay in the folder and end at a .faf/.fafm file) and the atomic write of
68
+ * `safeWriteFile` apply; the write itself goes only through a link to a file
69
+ * of the same name. A file that is not valid YAML (SafePathError `not-yaml`,
70
+ * one line), or not UTF-8, is refused (nothing written); so is a file that
71
+ * parses to a scalar or a list — the same `not-yaml` refusal readFaf gives,
72
+ * before `mutate` is called — anything `mutate` throws, and a file that
73
+ * changed on disk while faf was writing.
74
+ */
75
+ export declare function updateFafFile(path: string, mutate: (doc: Document) => void): UpdateFafResult;
76
+ /** Options for {@link writeFaf}. */
77
+ export interface WriteFafOptions {
78
+ /** Write a fresh file from `data` even when one exists — the explicit
79
+ * overwrite (`faf init --force`, `faf git --force`). Default: an existing
80
+ * file is updated in place with {@link updateFafFile}. */
81
+ replace?: boolean;
82
+ /** Called for each alias of an existing file that `data` would change
83
+ * (`stack: *base` while `data.stack` has more keys): faf leaves it as
84
+ * written — it never replaces or expands an alias — and that path is not
85
+ * written. For a fill (data from updateExistingFaf) it is also called for
86
+ * each node with an anchor an alias reads that the fill would change
87
+ * (`kind: 'anchor'`). `faf auto` prints one line for each. */
88
+ onAliasKept?: (kept: KeptAlias) => void;
89
+ }
90
+ /** The one line faf prints for an alias (or an anchor an alias reads) it left as written. */
91
+ export declare function aliasKeptNote(kept: KeptAlias): string;
92
+ /** Write a .faf file from data — atomically (temp file, fsync, rename; a
93
+ * failure leaves the original as it was), and never through a link that
94
+ * leaves the folder or dangles (SafePathError). An in-project link is written
95
+ * through and stays a link.
96
+ *
97
+ * An existing file is updated in place ({@link updateFafFile}): `data` is
98
+ * compared with what the file holds, and only the values that changed are
99
+ * rewritten — so comments, formatting, source text and key order survive, a
100
+ * value `data` merely repeats (the text an alias `*g` read as, say) never
101
+ * replaces the node the file has, and a write that changes nothing writes
102
+ * nothing. A key the file has and `data` leaves out is kept (faf removes
103
+ * nothing it did not write). Pass `{ replace: true }` to overwrite the file
104
+ * with a fresh render instead. Returns false when nothing was written.
105
+ *
106
+ * An alias in the file (`stack: *base`) is never replaced or expanded: it
107
+ * stays as written, and a change `data` makes under it is not written —
108
+ * `opts.onAliasKept` hears of each such path. When `data` is a fill (from
109
+ * updateExistingFaf), a node with an anchor that an alias reads is left as
110
+ * written as well (filling it would change every alias), and reported the
111
+ * same way with `kind: 'anchor'`.
112
+ *
113
+ * When `data` came from readFaf of this file (directly, or through
114
+ * updateExistingFaf), the write is refused if the file changed on disk since
115
+ * that read (SafePathError `changed`: "not written; original kept"), so an
116
+ * edit made meanwhile is never written over. A new file is not written over
117
+ * one that appeared meanwhile. */
118
+ export declare function writeFaf(path: string, data: FafData, opts?: WriteFafOptions): boolean;
119
+ /** Read raw YAML text from a .faf file (the same link rules as readFaf; a
120
+ * file that is not UTF-8 is refused). */
16
121
  export declare function readFafRaw(path: string): string;
17
122
  /** Parse .faf data from a YAML string — e.g. the output of `git show <ref>:project.faf`.
18
123
  * The readers above are path-only; `faf diff` needs to parse a version that
19
- * lives in git history, never on disk. */
20
- export declare function readFafFromString(text: string): FafData;
21
- /** Find the .faf file in a directory (walks up) */
124
+ * lives in git history, never on disk. With `path` (the file the text was
125
+ * read from) the text is read as {@link readFaf} reads a file: text that is
126
+ * not valid YAML throws a SafePathError (`not-yaml`) naming it, and so does
127
+ * text that parses to a scalar or a list (faf's shape check,
128
+ * `asFafMapping`); empty text reads as `{}`. Without `path`, the parsed
129
+ * value as it is, or yaml's own error. */
130
+ export declare function readFafFromString(text: string, path?: string): FafData;
131
+ /** Find the .faf file in a directory (walks up one level). The path is
132
+ * returned as spelled; a symlinked candidate must pass the readFaf link rules
133
+ * or this throws a SafePathError. */
22
134
  export declare function findFafFile(dir?: string): string | null;
23
135
  /** Repo-root-relative path of a .faf, computed BY git — runtime- and OS-independent.
24
136
  * Replaces `path.relative(top, fafPath)`, which breaks on Windows when git's