faf-cli 7.12.1 → 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.
- package/README.md +39 -32
- package/dist/cli.js +364 -337
- package/dist/cli.js.map +70 -59
- package/dist/core/cwd-guard.d.ts +8 -0
- package/dist/core/faf-dna.d.ts +147 -0
- package/dist/core/faf-source.d.ts +27 -0
- package/dist/core/json-edit.d.ts +77 -0
- package/dist/core/render-hash.d.ts +55 -0
- package/dist/core/safe-write.d.ts +228 -0
- package/dist/core/scorer.d.ts +7 -2
- package/dist/core/shape.d.ts +11 -0
- package/dist/core/slots.d.ts +30 -0
- package/dist/core/typed-none.d.ts +25 -0
- package/dist/core/types.d.ts +8 -0
- package/dist/core/yaml-edit.d.ts +136 -0
- package/dist/detect/assemble.d.ts +49 -5
- package/dist/detect/git-repo.d.ts +50 -0
- package/dist/detect/scanner.d.ts +9 -4
- package/dist/detect/stack.d.ts +14 -1
- package/dist/detect/turbo-cat-knowledge.d.ts +10 -0
- package/dist/detect/turbo-cat.d.ts +6 -2
- package/dist/fafm/index.d.ts +1 -0
- package/dist/fafm/soul.d.ts +108 -6
- package/dist/index.d.ts +24 -3
- package/dist/index.js +203 -163
- package/dist/index.js.map +35 -23
- package/dist/interop/cards.d.ts +52 -3
- package/dist/interop/claude-memory.d.ts +134 -0
- package/dist/interop/claude.d.ts +5 -1
- package/dist/interop/commonmark.d.ts +144 -0
- package/dist/interop/copilot-instructions.d.ts +1 -0
- package/dist/interop/faf.d.ts +123 -11
- package/dist/interop/inject.d.ts +112 -22
- package/dist/interop/projecthtml.d.ts +19 -2
- package/dist/interop/servercard.d.ts +51 -2
- package/dist/wasm/kernel.d.ts +7 -4
- package/package.json +5 -4
- package/project.faf +11 -4
package/dist/interop/cards.d.ts
CHANGED
|
@@ -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
|
|
115
|
-
*
|
|
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
|
-
|
|
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;
|
package/dist/interop/claude.d.ts
CHANGED
|
@@ -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;
|
package/dist/interop/faf.d.ts
CHANGED
|
@@ -1,24 +1,136 @@
|
|
|
1
|
+
import { type Document } from 'yaml';
|
|
1
2
|
import type { FafData } from '../core/types.js';
|
|
2
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|