faf-cli 7.13.0 → 7.13.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.
@@ -1,3 +1,61 @@
1
+ /**
2
+ * Safe file access — the one primitive every faf writer, and every read of
3
+ * project context, goes through. It is also the only module in faf that calls
4
+ * a filesystem write, rename, delete, mkdir or chmod API (tests/write-guard
5
+ * enforces that for all of src/).
6
+ *
7
+ * Rule 1 — stay inside the project. Before faf touches a path it resolves it on
8
+ * disk (realpath: every link followed). If that leads outside the project
9
+ * folder, faf refuses. A dangling link is refused — faf never creates a file at
10
+ * the end of a link. Anything whose real path is inside a `.git` folder is
11
+ * refused, always — with two narrow exceptions: faf's own git hook
12
+ * (`allowGitHooks`: one file directly inside the repo's hooks folder), and
13
+ * the repo's own git config file (`allowGitConfig`: the file `config`
14
+ * directly in the folder git names for it), where `faf diff
15
+ * --uninstall-driver` removes the section faf's install wrote.
16
+ *
17
+ * Rule 2 — a link leads to the same kind of file. faf follows a link only to a
18
+ * file with the same name (CLAUDE.md → docs/CLAUDE.md), or from one AI context
19
+ * file to another (CLAUDE.md → AGENTS.md; the set is FAF_CONTEXT_FILES). The
20
+ * link itself survives: faf writes the file it points at. `CLAUDE.md →
21
+ * README.md` or `project.html → package.json` is refused — faf would be
22
+ * writing a file it was never asked to write. A read of project context
23
+ * through a link must land on a .faf or .fafm file instead, so `project.faf →
24
+ * .env` is refused even though .env is in the project — or, for a read of an
25
+ * AI context file, on another AI context file (`faf recover` reads CLAUDE.md →
26
+ * AGENTS.md), the rule the writers use.
27
+ *
28
+ * Rule 3 — never leave a half-written file. A write goes to a temp file in the
29
+ * same folder, is flushed to disk (fsync), then renamed over the original in
30
+ * one step, keeping the original's permissions. If any step fails the temp
31
+ * file is removed and the original is exactly as it was: "not written;
32
+ * original kept". A plain writeFileSync truncates first, so a full disk, a
33
+ * quota or a killed process used to leave the user's file cut short. With
34
+ * `expect` (the bytes the caller read), the file is read again just before the
35
+ * rename and the write is refused if it changed in the meantime. Its mode is
36
+ * checked too: a file made read-only, or given other permissions, while faf
37
+ * was writing is not replaced.
38
+ *
39
+ * Rule 4 — text faf edits is UTF-8. readUtf8 decodes strictly: a UTF-16 file or
40
+ * any bytes that are not UTF-8 are refused, never turned into U+FFFD and
41
+ * written back.
42
+ *
43
+ * Rule 5 — a whole file faf renders replaces a file already there only when
44
+ * faf can prove it wrote it: project.html, the cards, `faf server-card --out`,
45
+ * a `faf taf --output` snapshot and a `faf decompile --output` file must be
46
+ * byte for byte what faf last wrote (their render hash, render-hash.ts); a
47
+ * `.fafb` must carry the FAFB header (see {@link safeReplaceOwned}). Anything
48
+ * else is refused unless the caller passes `force` (the CLI's `--force`).
49
+ *
50
+ * Rule 6 — detection reads stay inside the project. The files faf reads to fill
51
+ * slots and render context (README.md, package.json, pyproject.toml,
52
+ * Cargo.toml, go.mod, …) go through {@link repoFile}: resolved on disk, every
53
+ * link followed. A file whose real path, or the folder it sits in, leaves the
54
+ * project folder, runs through `.git`, or is a dangling link is absent to
55
+ * detection: it does not exist, and reading it gives nothing. A link that
56
+ * stays inside the project is followed (README.md → docs/README.md).
57
+ */
58
+ import { type Dirent, type Stats } from 'fs';
1
59
  /** Why a path was refused. */
2
60
  export type SafePathReason = 'outside' | 'dangling' | 'not-a-file' | 'not-faf' | 'other-file' | 'git' | 'not-utf8' | 'changed' | 'not-owned' | 'not-yaml' | 'unplaceable';
3
61
  /**
@@ -133,6 +191,45 @@ export declare function resolveInside(dir: string, name: string, opts?: ResolveI
133
191
  export declare function readUtf8(path: string): string;
134
192
  /** The bytes at `path`, or null when nothing is there (only ENOENT). */
135
193
  export declare function readBytesIfPresent(path: string): Buffer | null;
194
+ /**
195
+ * Where detection may read `rel` in the project folder `dir` — its real path —
196
+ * or null when detection treats it as absent (Rule 6). `rel` names a file or a
197
+ * folder, relative to `dir` (as `join(dir, rel)`). Every link on the way is
198
+ * followed, the folders included; the result is null when:
199
+ * - nothing is there, or a link on the way dangles or loops
200
+ * - the real path leaves `dir` (README.md → ~/.aws/credentials, or a folder
201
+ * on the way that is a link out)
202
+ * - the real path runs through `.git` (README.md → .git/config)
203
+ * A link that stays inside `dir` is followed. `dir` is the folder detection
204
+ * was handed: the project, or a folder below it that detection reads on its
205
+ * own (a subfolder's manifest), so a link may not leave that folder either.
206
+ */
207
+ export declare function repoFile(dir: string, rel: string): string | null;
208
+ /**
209
+ * Run `scan` with `root` as the project folder for every detection read under
210
+ * it: a read in a subfolder (web/package.json) may then follow a link to
211
+ * anywhere inside `root` (../shared/web-package.json), not only inside that
212
+ * subfolder. A link out of `root` is still absent. Subfolder scans use this.
213
+ */
214
+ export declare function withRepoRoot<T>(root: string, scan: () => T): T;
215
+ /** True when `rel` (a file or a folder) is in the project folder `dir` for
216
+ * detection: see {@link repoFile}. */
217
+ export declare function repoExists(dir: string, rel: string): boolean;
218
+ /** The text of the file `rel` in the project folder `dir` (UTF-8, read the way
219
+ * detection reads it), or null when detection treats it as absent (see
220
+ * {@link repoFile}), it is not a regular file, or it cannot be read. */
221
+ export declare function readRepoFile(dir: string, rel: string): string | null;
222
+ /** What `rel` is (its real path's stats) in the project folder `dir`, or null
223
+ * when detection treats it as absent (see {@link repoFile}). */
224
+ export declare function statRepoFile(dir: string, rel: string): Stats | null;
225
+ /**
226
+ * The entries of the folder `rel` (default: `dir` itself) in the project
227
+ * folder `dir`, or null when detection treats the folder as absent (see
228
+ * {@link repoFile}) or it cannot be read. A link among them that leaves `dir`,
229
+ * dangles, or leads into `.git` is left out: detection does not see it. Each
230
+ * entry says what it is itself (a link is a link, as readdirSync gives it).
231
+ */
232
+ export declare function readRepoDir(dir: string, rel?: string): Dirent[] | null;
136
233
  /**
137
234
  * A write that failed partway — a full disk, a quota, a read-only file, a
138
235
  * killed rename — with the file on disk exactly as it was: "<file>: not
@@ -32,8 +32,8 @@ export declare const PLACEHOLDERS: Set<string>;
32
32
  * Only in a tech slot the app-type leaves out does faf auto write
33
33
  * `slotignored` over them — the app-type's decision, never the words'. */
34
34
  export declare const EXPLICIT_NONE: ReadonlySet<string>;
35
- /** The value that marks a slot as not applicable (the What-Not). It comes
36
- * only from the app-type (shown to people as N/A); a typed word never means it. */
35
+ /** The value that marks a slot as not required and not scored (the What-Not).
36
+ * It comes only from the app-type; a typed word never means it. */
37
37
  export declare const SLOTIGNORED = "slotignored";
38
38
  /** True for a typed none — counts as an empty slot: `None`, `N/A`,
39
39
  * `not applicable`, `none` (any case, any padding). */
@@ -15,7 +15,7 @@ export interface TypedNoneHintOptions {
15
15
  * - a slot the app-type needs:
16
16
  * `<slot> says '<words>' — this app-type needs it, so it counts as empty until filled.`
17
17
  * - a slot the app-type leaves out (before `faf auto` has run):
18
- * `<slot> says '<words>' — this app-type doesn't use it; faf auto marks it slotignored (N/A).`
18
+ * `<slot> says '<words>' — this app-type doesn't use it; faf auto marks it slotignored.`
19
19
  * A file whose type faf does not know, or whose type is the `library`
20
20
  * detection fell back to (see {@link typeIsFallback}), gives the first line
21
21
  * for the slots a library needs (as faf's detection reads such a file) and no
@@ -29,7 +29,7 @@ export declare function assembleFreshFaf(dir: string): Record<string, unknown>;
29
29
  * has them, comment included; faf never writes `''` over them.
30
30
  * - In a tech slot the file's app-type (`project.type`) leaves out, the
31
31
  * app-type's decision is the fact: with no repo fact, the slot becomes
32
- * `slotignored` (shown as N/A) in place of the words, an empty value or a
32
+ * `slotignored` in place of the words, an empty value or a
33
33
  * placeholder. A real value there is kept. `slotignored` comes only from
34
34
  * the app-type — in a slot the app-type uses, it is never written, even
35
35
  * when detection reads the repo as another type; such a slot gets the