@indigoai-us/hq-cloud 5.2.1 → 5.3.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/dist/ignore.d.ts CHANGED
@@ -1,17 +1,23 @@
1
1
  /**
2
2
  * Ignore-file parser for cloud sync.
3
3
  *
4
- * Three layers, evaluated in order (later patterns override earlier ones):
5
- * 1. Built-in defaults things that should *never* sync (VCS, node_modules,
6
- * build artifacts, caches, env files). Cover the common stacks so that a
7
- * first-time sync over a random project folder doesn't try to push
8
- * `target/`, `node_modules/`, or `.next/` to S3.
9
- * 2. Repo `.gitignore` at hqRoot reuses the user's existing exclusions so
10
- * we don't re-list every build directory ourselves. Root-level only; we
11
- * do not recurse like real git.
12
- * 3. `.hqignore` (preferred) or `.hqsyncignore` (legacy name) at hqRoot —
13
- * sync-specific overrides. Use `!pattern` to re-include something an
14
- * earlier layer excluded.
4
+ * Two modes:
5
+ * - **Permissive (default)**: everything syncs except what ignore layers
6
+ * subtract. Three layers stack (later overrides earlier):
7
+ * 1. Built-in defaults VCS, node_modules, build artifacts, caches,
8
+ * env files. Covers the common stacks so a first-time sync over a
9
+ * random project folder doesn't push `target/` or `.next/` to S3.
10
+ * 2. Repo `.gitignore` at hqRoot reuses existing exclusions so we
11
+ * don't re-list every build directory. Root-level only.
12
+ * 3. `.hqignore` (preferred) or `.hqsyncignore` (legacy) sync-specific
13
+ * overrides. Use `!pattern` to re-include something earlier layers
14
+ * excluded.
15
+ *
16
+ * - **Allowlist**: triggered when `.hqinclude` exists at hqRoot. Nothing
17
+ * syncs unless its path matches at least one pattern in `.hqinclude`. The
18
+ * three exclusion layers still subtract on top — so even allowlisted
19
+ * subtrees won't push `node_modules/` or `.env`. Privacy-by-default for
20
+ * HQ trees that contain mixed personal + shareable data.
15
21
  */
16
22
  export declare const DEFAULT_IGNORES: string[];
17
23
  export declare function createIgnoreFilter(hqRoot: string): (filePath: string) => boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"ignore.d.ts","sourceRoot":"","sources":["../src/ignore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,eAAO,MAAM,eAAe,UAsD3B,CAAC;AAWF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAsBhF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAAmB,GAC1B,OAAO,CAOT"}
1
+ {"version":3,"file":"ignore.d.ts","sourceRoot":"","sources":["../src/ignore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAQH,eAAO,MAAM,eAAe,UAsD3B,CAAC;AAWF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAiChF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAAmB,GAC1B,OAAO,CAOT"}
package/dist/ignore.js CHANGED
@@ -1,17 +1,23 @@
1
1
  /**
2
2
  * Ignore-file parser for cloud sync.
3
3
  *
4
- * Three layers, evaluated in order (later patterns override earlier ones):
5
- * 1. Built-in defaults things that should *never* sync (VCS, node_modules,
6
- * build artifacts, caches, env files). Cover the common stacks so that a
7
- * first-time sync over a random project folder doesn't try to push
8
- * `target/`, `node_modules/`, or `.next/` to S3.
9
- * 2. Repo `.gitignore` at hqRoot reuses the user's existing exclusions so
10
- * we don't re-list every build directory ourselves. Root-level only; we
11
- * do not recurse like real git.
12
- * 3. `.hqignore` (preferred) or `.hqsyncignore` (legacy name) at hqRoot —
13
- * sync-specific overrides. Use `!pattern` to re-include something an
14
- * earlier layer excluded.
4
+ * Two modes:
5
+ * - **Permissive (default)**: everything syncs except what ignore layers
6
+ * subtract. Three layers stack (later overrides earlier):
7
+ * 1. Built-in defaults VCS, node_modules, build artifacts, caches,
8
+ * env files. Covers the common stacks so a first-time sync over a
9
+ * random project folder doesn't push `target/` or `.next/` to S3.
10
+ * 2. Repo `.gitignore` at hqRoot reuses existing exclusions so we
11
+ * don't re-list every build directory. Root-level only.
12
+ * 3. `.hqignore` (preferred) or `.hqsyncignore` (legacy) sync-specific
13
+ * overrides. Use `!pattern` to re-include something earlier layers
14
+ * excluded.
15
+ *
16
+ * - **Allowlist**: triggered when `.hqinclude` exists at hqRoot. Nothing
17
+ * syncs unless its path matches at least one pattern in `.hqinclude`. The
18
+ * three exclusion layers still subtract on top — so even allowlisted
19
+ * subtrees won't push `node_modules/` or `.env`. Privacy-by-default for
20
+ * HQ trees that contain mixed personal + shareable data.
15
21
  */
16
22
  import * as fs from "fs";
17
23
  import * as path from "path";
@@ -89,11 +95,23 @@ export function createIgnoreFilter(hqRoot) {
89
95
  readIgnoreFile(path.join(hqRoot, ".hqsyncignore"));
90
96
  if (hqignore)
91
97
  ig.add(hqignore);
98
+ // Allowlist mode: when `.hqinclude` exists, sync is opt-in. The matcher
99
+ // here treats include patterns as ignore patterns and inverts the verdict —
100
+ // a path is "allowed" iff its relative path matches at least one entry.
101
+ // Exclusion layers above still subtract, so build artifacts inside an
102
+ // allowlisted subtree (e.g. node_modules/ inside companies/x/repos/y/) are
103
+ // still skipped.
104
+ const hqinclude = readIgnoreFile(path.join(hqRoot, ".hqinclude"));
105
+ const includeMatcher = hqinclude ? ignore().add(hqinclude) : null;
92
106
  return (filePath) => {
93
107
  const relative = path.relative(hqRoot, filePath);
94
108
  if (!relative || relative.startsWith(".."))
95
109
  return true; // outside HQ root
96
- return !ig.ignores(relative);
110
+ if (ig.ignores(relative))
111
+ return false;
112
+ if (includeMatcher && !includeMatcher.ignores(relative))
113
+ return false;
114
+ return true;
97
115
  };
98
116
  }
99
117
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ignore.js","sourceRoot":"","sources":["../src/ignore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,4DAA4D;AAC5D,sDAAsD;AACtD,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,WAAW;IACX,OAAO;IACP,MAAM;IACN,WAAW;IACX,WAAW;IAEX,YAAY;IACZ,eAAe;IACf,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,cAAc;IACd,SAAS;IACT,gBAAgB;IAChB,QAAQ;IACR,WAAW;IAEX,eAAe;IACf,SAAS;IAET,SAAS;IACT,cAAc;IACd,OAAO;IACP,gBAAgB;IAChB,cAAc;IACd,cAAc;IACd,QAAQ;IACR,OAAO;IAEP,mBAAmB;IACnB,SAAS;IACT,MAAM;IACN,SAAS;IAET,wBAAwB;IACxB,SAAS;IACT,MAAM;IACN,OAAO;IAEP,kDAAkD;IAClD,OAAO;IACP,cAAc;IACd,uBAAuB;IACvB,qBAAqB;IACrB,cAAc;IAEd,sDAAsD;IACtD,QAAQ;IAER,gBAAgB;IAChB,MAAM;IACN,QAAQ;CACT,CAAC;AAEF,SAAS,cAAc,CAAC,QAAgB;IACtC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAC/C,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;IAEpB,6BAA6B;IAC7B,EAAE,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAExB,4EAA4E;IAC5E,MAAM,SAAS,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;IAClE,IAAI,SAAS;QAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAEjC,sEAAsE;IACtE,mDAAmD;IACnD,MAAM,QAAQ,GACZ,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAC9C,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;IACrD,IAAI,QAAQ;QAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAE/B,OAAO,CAAC,QAAgB,EAAW,EAAE;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACjD,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC,CAAC,kBAAkB;QAC3E,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/B,CAAC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAgB,EAChB,QAAQ,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI;IAE3B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"ignore.js","sourceRoot":"","sources":["../src/ignore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Unit tests for createIgnoreFilter.
3
+ *
4
+ * Covers both modes: legacy permissive (no .hqinclude) and allowlist mode
5
+ * (.hqinclude present). The allowlist tests guard against accidentally
6
+ * leaking sensitive subtrees like data/ or workers/ to S3 — a regression
7
+ * here would silently push private content on the next sync.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=ignore.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ignore.test.d.ts","sourceRoot":"","sources":["../src/ignore.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Unit tests for createIgnoreFilter.
3
+ *
4
+ * Covers both modes: legacy permissive (no .hqinclude) and allowlist mode
5
+ * (.hqinclude present). The allowlist tests guard against accidentally
6
+ * leaking sensitive subtrees like data/ or workers/ to S3 — a regression
7
+ * here would silently push private content on the next sync.
8
+ */
9
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
10
+ import * as fs from "fs";
11
+ import * as os from "os";
12
+ import * as path from "path";
13
+ import { createIgnoreFilter } from "./ignore.js";
14
+ describe("createIgnoreFilter", () => {
15
+ let hqRoot;
16
+ beforeEach(() => {
17
+ hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-ignore-test-"));
18
+ });
19
+ afterEach(() => {
20
+ fs.rmSync(hqRoot, { recursive: true, force: true });
21
+ });
22
+ it("permissive mode: regular files sync, defaults are ignored", () => {
23
+ const shouldSync = createIgnoreFilter(hqRoot);
24
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/notes.md"))).toBe(true);
25
+ expect(shouldSync(path.join(hqRoot, "node_modules/foo/x.js"))).toBe(false);
26
+ expect(shouldSync(path.join(hqRoot, ".env"))).toBe(false);
27
+ });
28
+ it("permissive mode: .hqignore patterns are honored", () => {
29
+ fs.writeFileSync(path.join(hqRoot, ".hqignore"), "companies/*/data/\n");
30
+ const shouldSync = createIgnoreFilter(hqRoot);
31
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/data/x.csv"))).toBe(false);
32
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/notes.md"))).toBe(true);
33
+ });
34
+ it("allowlist mode: presence of .hqinclude switches to opt-in", () => {
35
+ fs.writeFileSync(path.join(hqRoot, ".hqinclude"), "companies/*/knowledge/\ncompanies/*/projects/\n");
36
+ const shouldSync = createIgnoreFilter(hqRoot);
37
+ // Allowlisted paths sync.
38
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/knowledge/foo.md"))).toBe(true);
39
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/projects/p1/prd.json"))).toBe(true);
40
+ // Anything else stays local — this is the privacy guarantee.
41
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/data/leads.csv"))).toBe(false);
42
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/workers/cmo/skill.md"))).toBe(false);
43
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/settings/aws.json"))).toBe(false);
44
+ expect(shouldSync(path.join(hqRoot, "personal/journal/2026-04-26.md"))).toBe(false);
45
+ });
46
+ it("allowlist mode: exclusion layers still subtract on top", () => {
47
+ // Even when a subtree is allowlisted, default ignores like node_modules/
48
+ // and .env must still apply. Otherwise an allowlisted subdir would sync
49
+ // gigabytes of dependency junk or leak secret env files.
50
+ fs.writeFileSync(path.join(hqRoot, ".hqinclude"), "companies/*/projects/\n");
51
+ const shouldSync = createIgnoreFilter(hqRoot);
52
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/projects/p1/prd.json"))).toBe(true);
53
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/projects/p1/node_modules/react/index.js"))).toBe(false);
54
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/projects/p1/.env"))).toBe(false);
55
+ });
56
+ });
57
+ //# sourceMappingURL=ignore.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ignore.test.js","sourceRoot":"","sources":["../src/ignore.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cloud",
3
- "version": "5.2.1",
3
+ "version": "5.3.0",
4
4
  "description": "HQ by Indigo cloud sync engine — bidirectional S3 sync for mobile access",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Unit tests for createIgnoreFilter.
3
+ *
4
+ * Covers both modes: legacy permissive (no .hqinclude) and allowlist mode
5
+ * (.hqinclude present). The allowlist tests guard against accidentally
6
+ * leaking sensitive subtrees like data/ or workers/ to S3 — a regression
7
+ * here would silently push private content on the next sync.
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
11
+ import * as fs from "fs";
12
+ import * as os from "os";
13
+ import * as path from "path";
14
+ import { createIgnoreFilter } from "./ignore.js";
15
+
16
+ describe("createIgnoreFilter", () => {
17
+ let hqRoot: string;
18
+
19
+ beforeEach(() => {
20
+ hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-ignore-test-"));
21
+ });
22
+
23
+ afterEach(() => {
24
+ fs.rmSync(hqRoot, { recursive: true, force: true });
25
+ });
26
+
27
+ it("permissive mode: regular files sync, defaults are ignored", () => {
28
+ const shouldSync = createIgnoreFilter(hqRoot);
29
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/notes.md"))).toBe(true);
30
+ expect(shouldSync(path.join(hqRoot, "node_modules/foo/x.js"))).toBe(false);
31
+ expect(shouldSync(path.join(hqRoot, ".env"))).toBe(false);
32
+ });
33
+
34
+ it("permissive mode: .hqignore patterns are honored", () => {
35
+ fs.writeFileSync(path.join(hqRoot, ".hqignore"), "companies/*/data/\n");
36
+ const shouldSync = createIgnoreFilter(hqRoot);
37
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/data/x.csv"))).toBe(false);
38
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/notes.md"))).toBe(true);
39
+ });
40
+
41
+ it("allowlist mode: presence of .hqinclude switches to opt-in", () => {
42
+ fs.writeFileSync(
43
+ path.join(hqRoot, ".hqinclude"),
44
+ "companies/*/knowledge/\ncompanies/*/projects/\n",
45
+ );
46
+ const shouldSync = createIgnoreFilter(hqRoot);
47
+ // Allowlisted paths sync.
48
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/knowledge/foo.md"))).toBe(true);
49
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/projects/p1/prd.json"))).toBe(true);
50
+ // Anything else stays local — this is the privacy guarantee.
51
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/data/leads.csv"))).toBe(false);
52
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/workers/cmo/skill.md"))).toBe(false);
53
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/settings/aws.json"))).toBe(false);
54
+ expect(shouldSync(path.join(hqRoot, "personal/journal/2026-04-26.md"))).toBe(false);
55
+ });
56
+
57
+ it("allowlist mode: exclusion layers still subtract on top", () => {
58
+ // Even when a subtree is allowlisted, default ignores like node_modules/
59
+ // and .env must still apply. Otherwise an allowlisted subdir would sync
60
+ // gigabytes of dependency junk or leak secret env files.
61
+ fs.writeFileSync(path.join(hqRoot, ".hqinclude"), "companies/*/projects/\n");
62
+ const shouldSync = createIgnoreFilter(hqRoot);
63
+ expect(
64
+ shouldSync(path.join(hqRoot, "companies/indigo/projects/p1/prd.json")),
65
+ ).toBe(true);
66
+ expect(
67
+ shouldSync(path.join(hqRoot, "companies/indigo/projects/p1/node_modules/react/index.js")),
68
+ ).toBe(false);
69
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/projects/p1/.env"))).toBe(false);
70
+ });
71
+ });
package/src/ignore.ts CHANGED
@@ -1,17 +1,23 @@
1
1
  /**
2
2
  * Ignore-file parser for cloud sync.
3
3
  *
4
- * Three layers, evaluated in order (later patterns override earlier ones):
5
- * 1. Built-in defaults things that should *never* sync (VCS, node_modules,
6
- * build artifacts, caches, env files). Cover the common stacks so that a
7
- * first-time sync over a random project folder doesn't try to push
8
- * `target/`, `node_modules/`, or `.next/` to S3.
9
- * 2. Repo `.gitignore` at hqRoot reuses the user's existing exclusions so
10
- * we don't re-list every build directory ourselves. Root-level only; we
11
- * do not recurse like real git.
12
- * 3. `.hqignore` (preferred) or `.hqsyncignore` (legacy name) at hqRoot —
13
- * sync-specific overrides. Use `!pattern` to re-include something an
14
- * earlier layer excluded.
4
+ * Two modes:
5
+ * - **Permissive (default)**: everything syncs except what ignore layers
6
+ * subtract. Three layers stack (later overrides earlier):
7
+ * 1. Built-in defaults VCS, node_modules, build artifacts, caches,
8
+ * env files. Covers the common stacks so a first-time sync over a
9
+ * random project folder doesn't push `target/` or `.next/` to S3.
10
+ * 2. Repo `.gitignore` at hqRoot reuses existing exclusions so we
11
+ * don't re-list every build directory. Root-level only.
12
+ * 3. `.hqignore` (preferred) or `.hqsyncignore` (legacy) sync-specific
13
+ * overrides. Use `!pattern` to re-include something earlier layers
14
+ * excluded.
15
+ *
16
+ * - **Allowlist**: triggered when `.hqinclude` exists at hqRoot. Nothing
17
+ * syncs unless its path matches at least one pattern in `.hqinclude`. The
18
+ * three exclusion layers still subtract on top — so even allowlisted
19
+ * subtrees won't push `node_modules/` or `.env`. Privacy-by-default for
20
+ * HQ trees that contain mixed personal + shareable data.
15
21
  */
16
22
 
17
23
  import * as fs from "fs";
@@ -102,10 +108,21 @@ export function createIgnoreFilter(hqRoot: string): (filePath: string) => boolea
102
108
  readIgnoreFile(path.join(hqRoot, ".hqsyncignore"));
103
109
  if (hqignore) ig.add(hqignore);
104
110
 
111
+ // Allowlist mode: when `.hqinclude` exists, sync is opt-in. The matcher
112
+ // here treats include patterns as ignore patterns and inverts the verdict —
113
+ // a path is "allowed" iff its relative path matches at least one entry.
114
+ // Exclusion layers above still subtract, so build artifacts inside an
115
+ // allowlisted subtree (e.g. node_modules/ inside companies/x/repos/y/) are
116
+ // still skipped.
117
+ const hqinclude = readIgnoreFile(path.join(hqRoot, ".hqinclude"));
118
+ const includeMatcher = hqinclude ? ignore().add(hqinclude) : null;
119
+
105
120
  return (filePath: string): boolean => {
106
121
  const relative = path.relative(hqRoot, filePath);
107
122
  if (!relative || relative.startsWith("..")) return true; // outside HQ root
108
- return !ig.ignores(relative);
123
+ if (ig.ignores(relative)) return false;
124
+ if (includeMatcher && !includeMatcher.ignores(relative)) return false;
125
+ return true;
109
126
  };
110
127
  }
111
128