@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 +17 -11
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +30 -12
- package/dist/ignore.js.map +1 -1
- package/dist/ignore.test.d.ts +10 -0
- package/dist/ignore.test.d.ts.map +1 -0
- package/dist/ignore.test.js +57 -0
- package/dist/ignore.test.js.map +1 -0
- package/package.json +1 -1
- package/src/ignore.test.ts +71 -0
- package/src/ignore.ts +29 -12
package/dist/ignore.d.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ignore-file parser for cloud sync.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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;
|
package/dist/ignore.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ignore.d.ts","sourceRoot":"","sources":["../src/ignore.ts"],"names":[],"mappings":"AAAA
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
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
|
/**
|
package/dist/ignore.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ignore.js","sourceRoot":"","sources":["../src/ignore.ts"],"names":[],"mappings":"AAAA
|
|
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
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
123
|
+
if (ig.ignores(relative)) return false;
|
|
124
|
+
if (includeMatcher && !includeMatcher.ignores(relative)) return false;
|
|
125
|
+
return true;
|
|
109
126
|
};
|
|
110
127
|
}
|
|
111
128
|
|