@praxisflux/gates 0.59.6 → 0.60.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.
@@ -4,8 +4,8 @@
4
4
  // These helpers make that dotfile-safe, idempotent, and honest (verify on disk, never clobber
5
5
  // what the user has grown). Zero-dependency.
6
6
 
7
- import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
- import { join, dirname } from "node:path";
7
+ import { cpSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
8
+ import { join, dirname, resolve } from "node:path";
9
9
 
10
10
  /**
11
11
  * Recursively copy `src` → `dest`. Uses node:fs cp, which copies dotfiles (e.g. `.template/`)
@@ -38,6 +38,43 @@ export function ensureGitignore(root, entry) {
38
38
  return true;
39
39
  }
40
40
 
41
+ /**
42
+ * Resolve the real git dir for `root`: the `.git` directory itself, or — when `.git` is a
43
+ * worktree pointer file (`gitdir: <path>`) — the path it points at. `null` when `.git` is
44
+ * absent (pre-`git init`, spec 060 R6) or unreadable.
45
+ */
46
+ function resolveGitDir(root) {
47
+ const p = join(root, ".git");
48
+ if (!existsSync(p)) return null;
49
+ if (statSync(p).isDirectory()) return p;
50
+ try {
51
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(readFileSync(p, "utf8"));
52
+ return m ? resolve(root, m[1]) : null;
53
+ } catch { return null; }
54
+ }
55
+
56
+ /**
57
+ * Ensure `<gitdir>/info/exclude` contains every line in `entries` (local-only planting,
58
+ * spec 060 R1/R2), creating `info/` as needed and appending only lines not already present.
59
+ * Never touches `.gitignore`. Returns `{ status: "no-git" }` (nothing written — R6) when
60
+ * `root` has no `.git`, else `{ status: "added"|"unchanged", added: string[] }`.
61
+ * `dryRun: true` (plant's `--check`) reports what would happen without writing.
62
+ */
63
+ export function ensureExclude(root, entries, { dryRun = false } = {}) {
64
+ const gitDir = resolveGitDir(root);
65
+ if (!gitDir) return { status: "no-git", added: [] };
66
+ const p = join(gitDir, "info", "exclude");
67
+ const existing = existsSync(p) ? readFileSync(p, "utf8") : "";
68
+ const have = new Set(existing.split("\n").map((l) => l.trim()));
69
+ const toAdd = entries.map((e) => e.trim()).filter((e) => !have.has(e));
70
+ if (toAdd.length === 0) return { status: "unchanged", added: [] };
71
+ if (dryRun) return { status: "added", added: toAdd };
72
+ mkdirSync(dirname(p), { recursive: true });
73
+ const prefix = existing === "" ? "" : existing.replace(/\n*$/, "\n");
74
+ writeFileSync(p, prefix + toAdd.join("\n") + "\n");
75
+ return { status: "added", added: toAdd };
76
+ }
77
+
41
78
  /** Which of `relPaths` (relative to `root`) are missing on disk. Empty = all present. */
42
79
  export function verifyPresent(root, relPaths) {
43
80
  return relPaths.filter((r) => !existsSync(join(root, r)));
@@ -4,8 +4,8 @@
4
4
  // These helpers make that dotfile-safe, idempotent, and honest (verify on disk, never clobber
5
5
  // what the user has grown). Zero-dependency.
6
6
 
7
- import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
- import { join, dirname } from "node:path";
7
+ import { cpSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
8
+ import { join, dirname, resolve } from "node:path";
9
9
 
10
10
  /**
11
11
  * Recursively copy `src` → `dest`. Uses node:fs cp, which copies dotfiles (e.g. `.template/`)
@@ -38,6 +38,43 @@ export function ensureGitignore(root, entry) {
38
38
  return true;
39
39
  }
40
40
 
41
+ /**
42
+ * Resolve the real git dir for `root`: the `.git` directory itself, or — when `.git` is a
43
+ * worktree pointer file (`gitdir: <path>`) — the path it points at. `null` when `.git` is
44
+ * absent (pre-`git init`, spec 060 R6) or unreadable.
45
+ */
46
+ function resolveGitDir(root) {
47
+ const p = join(root, ".git");
48
+ if (!existsSync(p)) return null;
49
+ if (statSync(p).isDirectory()) return p;
50
+ try {
51
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(readFileSync(p, "utf8"));
52
+ return m ? resolve(root, m[1]) : null;
53
+ } catch { return null; }
54
+ }
55
+
56
+ /**
57
+ * Ensure `<gitdir>/info/exclude` contains every line in `entries` (local-only planting,
58
+ * spec 060 R1/R2), creating `info/` as needed and appending only lines not already present.
59
+ * Never touches `.gitignore`. Returns `{ status: "no-git" }` (nothing written — R6) when
60
+ * `root` has no `.git`, else `{ status: "added"|"unchanged", added: string[] }`.
61
+ * `dryRun: true` (plant's `--check`) reports what would happen without writing.
62
+ */
63
+ export function ensureExclude(root, entries, { dryRun = false } = {}) {
64
+ const gitDir = resolveGitDir(root);
65
+ if (!gitDir) return { status: "no-git", added: [] };
66
+ const p = join(gitDir, "info", "exclude");
67
+ const existing = existsSync(p) ? readFileSync(p, "utf8") : "";
68
+ const have = new Set(existing.split("\n").map((l) => l.trim()));
69
+ const toAdd = entries.map((e) => e.trim()).filter((e) => !have.has(e));
70
+ if (toAdd.length === 0) return { status: "unchanged", added: [] };
71
+ if (dryRun) return { status: "added", added: toAdd };
72
+ mkdirSync(dirname(p), { recursive: true });
73
+ const prefix = existing === "" ? "" : existing.replace(/\n*$/, "\n");
74
+ writeFileSync(p, prefix + toAdd.join("\n") + "\n");
75
+ return { status: "added", added: toAdd };
76
+ }
77
+
41
78
  /** Which of `relPaths` (relative to `root`) are missing on disk. Empty = all present. */
42
79
  export function verifyPresent(root, relPaths) {
43
80
  return relPaths.filter((r) => !existsSync(join(root, r)));
package/lib/installer.mjs CHANGED
@@ -4,8 +4,8 @@
4
4
  // These helpers make that dotfile-safe, idempotent, and honest (verify on disk, never clobber
5
5
  // what the user has grown). Zero-dependency.
6
6
 
7
- import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
- import { join, dirname } from "node:path";
7
+ import { cpSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
8
+ import { join, dirname, resolve } from "node:path";
9
9
 
10
10
  /**
11
11
  * Recursively copy `src` → `dest`. Uses node:fs cp, which copies dotfiles (e.g. `.template/`)
@@ -38,6 +38,43 @@ export function ensureGitignore(root, entry) {
38
38
  return true;
39
39
  }
40
40
 
41
+ /**
42
+ * Resolve the real git dir for `root`: the `.git` directory itself, or — when `.git` is a
43
+ * worktree pointer file (`gitdir: <path>`) — the path it points at. `null` when `.git` is
44
+ * absent (pre-`git init`, spec 060 R6) or unreadable.
45
+ */
46
+ function resolveGitDir(root) {
47
+ const p = join(root, ".git");
48
+ if (!existsSync(p)) return null;
49
+ if (statSync(p).isDirectory()) return p;
50
+ try {
51
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(readFileSync(p, "utf8"));
52
+ return m ? resolve(root, m[1]) : null;
53
+ } catch { return null; }
54
+ }
55
+
56
+ /**
57
+ * Ensure `<gitdir>/info/exclude` contains every line in `entries` (local-only planting,
58
+ * spec 060 R1/R2), creating `info/` as needed and appending only lines not already present.
59
+ * Never touches `.gitignore`. Returns `{ status: "no-git" }` (nothing written — R6) when
60
+ * `root` has no `.git`, else `{ status: "added"|"unchanged", added: string[] }`.
61
+ * `dryRun: true` (plant's `--check`) reports what would happen without writing.
62
+ */
63
+ export function ensureExclude(root, entries, { dryRun = false } = {}) {
64
+ const gitDir = resolveGitDir(root);
65
+ if (!gitDir) return { status: "no-git", added: [] };
66
+ const p = join(gitDir, "info", "exclude");
67
+ const existing = existsSync(p) ? readFileSync(p, "utf8") : "";
68
+ const have = new Set(existing.split("\n").map((l) => l.trim()));
69
+ const toAdd = entries.map((e) => e.trim()).filter((e) => !have.has(e));
70
+ if (toAdd.length === 0) return { status: "unchanged", added: [] };
71
+ if (dryRun) return { status: "added", added: toAdd };
72
+ mkdirSync(dirname(p), { recursive: true });
73
+ const prefix = existing === "" ? "" : existing.replace(/\n*$/, "\n");
74
+ writeFileSync(p, prefix + toAdd.join("\n") + "\n");
75
+ return { status: "added", added: toAdd };
76
+ }
77
+
41
78
  /** Which of `relPaths` (relative to `root`) are missing on disk. Empty = all present. */
42
79
  export function verifyPresent(root, relPaths) {
43
80
  return relPaths.filter((r) => !existsSync(join(root, r)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisflux/gates",
3
- "version": "0.59.6",
3
+ "version": "0.60.0",
4
4
  "description": "praxisflux gate checks as a zero-dependency CLI (spec-bridge, wiki-freshness, course) — status can't exceed proven artifacts",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -4,8 +4,8 @@
4
4
  // These helpers make that dotfile-safe, idempotent, and honest (verify on disk, never clobber
5
5
  // what the user has grown). Zero-dependency.
6
6
 
7
- import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
- import { join, dirname } from "node:path";
7
+ import { cpSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
8
+ import { join, dirname, resolve } from "node:path";
9
9
 
10
10
  /**
11
11
  * Recursively copy `src` → `dest`. Uses node:fs cp, which copies dotfiles (e.g. `.template/`)
@@ -38,6 +38,43 @@ export function ensureGitignore(root, entry) {
38
38
  return true;
39
39
  }
40
40
 
41
+ /**
42
+ * Resolve the real git dir for `root`: the `.git` directory itself, or — when `.git` is a
43
+ * worktree pointer file (`gitdir: <path>`) — the path it points at. `null` when `.git` is
44
+ * absent (pre-`git init`, spec 060 R6) or unreadable.
45
+ */
46
+ function resolveGitDir(root) {
47
+ const p = join(root, ".git");
48
+ if (!existsSync(p)) return null;
49
+ if (statSync(p).isDirectory()) return p;
50
+ try {
51
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(readFileSync(p, "utf8"));
52
+ return m ? resolve(root, m[1]) : null;
53
+ } catch { return null; }
54
+ }
55
+
56
+ /**
57
+ * Ensure `<gitdir>/info/exclude` contains every line in `entries` (local-only planting,
58
+ * spec 060 R1/R2), creating `info/` as needed and appending only lines not already present.
59
+ * Never touches `.gitignore`. Returns `{ status: "no-git" }` (nothing written — R6) when
60
+ * `root` has no `.git`, else `{ status: "added"|"unchanged", added: string[] }`.
61
+ * `dryRun: true` (plant's `--check`) reports what would happen without writing.
62
+ */
63
+ export function ensureExclude(root, entries, { dryRun = false } = {}) {
64
+ const gitDir = resolveGitDir(root);
65
+ if (!gitDir) return { status: "no-git", added: [] };
66
+ const p = join(gitDir, "info", "exclude");
67
+ const existing = existsSync(p) ? readFileSync(p, "utf8") : "";
68
+ const have = new Set(existing.split("\n").map((l) => l.trim()));
69
+ const toAdd = entries.map((e) => e.trim()).filter((e) => !have.has(e));
70
+ if (toAdd.length === 0) return { status: "unchanged", added: [] };
71
+ if (dryRun) return { status: "added", added: toAdd };
72
+ mkdirSync(dirname(p), { recursive: true });
73
+ const prefix = existing === "" ? "" : existing.replace(/\n*$/, "\n");
74
+ writeFileSync(p, prefix + toAdd.join("\n") + "\n");
75
+ return { status: "added", added: toAdd };
76
+ }
77
+
41
78
  /** Which of `relPaths` (relative to `root`) are missing on disk. Empty = all present. */
42
79
  export function verifyPresent(root, relPaths) {
43
80
  return relPaths.filter((r) => !existsSync(join(root, r)));