@indigoai-us/hq-cloud 6.14.46 → 6.14.47

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.
@@ -33,6 +33,24 @@ describe("isExpectedIgnore", () => {
33
33
  expect(isExpectedIgnore(".claude/skills/foo/.skill-media/hero.png")).toBe(true);
34
34
  });
35
35
 
36
+ it("classifies macOS Finder Icon\\r files as expected noise", () => {
37
+ // Finder scatters one into every directory it renders a custom icon for,
38
+ // and a folder-level cloud-sync agent propagates them tree-wide. Treated
39
+ // exactly like .DS_Store so a seeded tree stays silent rather than firing
40
+ // the "EXCLUDED from sync — review in case this is unintended" warning
41
+ // once per file.
42
+ expect(isExpectedIgnore("Icon\r")).toBe(true);
43
+ expect(isExpectedIgnore("companies/indigo/Icon\r")).toBe(true);
44
+ });
45
+
46
+ it("keeps a real file named Icons noteworthy (no `Icon?` over-match)", () => {
47
+ // The `Icon?` gitignore workaround would swallow these. Dropping real
48
+ // user content is a worse failure than the cruft being excluded.
49
+ expect(isExpectedIgnore("companies/indigo/Icons")).toBe(false);
50
+ expect(isExpectedIgnore("companies/indigo/Icon")).toBe(false);
51
+ expect(isExpectedIgnore("companies/indigo/Icons.md")).toBe(false);
52
+ });
53
+
36
54
  it("keeps nested repos/workspace and ordinary content noteworthy", () => {
37
55
  expect(isExpectedIgnore("companies/indigo/knowledge/repos/foo/notes.md")).toBe(false);
38
56
  expect(isExpectedIgnore("companies/indigo/knowledge/overview.md")).toBe(false);
@@ -85,6 +103,43 @@ describe("createIgnoreFilter", () => {
85
103
  expect(shouldSync(path.join(hqRoot, "node_modules/react/index.js"))).toBe(false);
86
104
  });
87
105
 
106
+ it("permissive mode: macOS Finder Icon\\r files never enter the walk", () => {
107
+ // A CR is a control character, so the vault key validator rejects the key
108
+ // (INVALID_KEY_CONTROL_CHARS) and the upload can only ever throw. Dropping
109
+ // them during the walk is what keeps them off the upload path entirely.
110
+ const shouldSync = createIgnoreFilter(hqRoot);
111
+ expect(shouldSync(path.join(hqRoot, "Icon\r"))).toBe(false);
112
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/Icon\r"))).toBe(false);
113
+ expect(shouldSync(path.join(hqRoot, ".agents/Icon\r"))).toBe(false);
114
+ // Caught at any depth, like the other OS-cruft exclusions.
115
+ expect(shouldSync(path.join(hqRoot, "a/b/c/d/Icon\r"))).toBe(false);
116
+ });
117
+
118
+ it("permissive mode: a real file named Icons still syncs", () => {
119
+ // Regression guard for the `Icon?` gitignore workaround, whose `?`
120
+ // single-char wildcard also matches Icons / Icon1 / IconX. This exclusion
121
+ // matches the exact Finder byte sequence instead, so real content survives.
122
+ const shouldSync = createIgnoreFilter(hqRoot);
123
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/Icons"))).toBe(true);
124
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/Icon"))).toBe(true);
125
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/Icon1"))).toBe(true);
126
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/Icons.md"))).toBe(true);
127
+ });
128
+
129
+ it("a literal Icon+CR line in .gitignore does NOT reach the ignore lib", () => {
130
+ // This is why the exclusion is a predicate and not a DEFAULT_IGNORES
131
+ // entry. ignore@5 strips a trailing CR from a pattern line, so the
132
+ // spelling git itself requires silently never matches. Pinning the
133
+ // upstream behavior here means a future ignore-lib bump that changes it
134
+ // surfaces as a failing test rather than a silent double-exclusion.
135
+ fs.writeFileSync(path.join(hqRoot, ".gitignore"), "Icon\r\n");
136
+ const shouldSync = createIgnoreFilter(hqRoot);
137
+ // Still excluded — by the predicate, not by the pattern.
138
+ expect(shouldSync(path.join(hqRoot, "Icon\r"))).toBe(false);
139
+ // Proof the pattern itself is inert: it did not take `Icons` with it.
140
+ expect(shouldSync(path.join(hqRoot, "Icons"))).toBe(true);
141
+ });
142
+
88
143
  it("repos/ exclusion is root-anchored to hqRoot (DEV-1791)", () => {
89
144
  const shouldSync = createIgnoreFilter(hqRoot);
90
145
  expect(shouldSync(path.join(hqRoot, "repos/private/hq-cloud/src/x.ts"))).toBe(false);
package/src/ignore.ts CHANGED
@@ -244,6 +244,52 @@ function segmentToRegex(seg: string): RegExp {
244
244
  return new RegExp(`^${body}$`);
245
245
  }
246
246
 
247
+ /**
248
+ * Match a path whose basename is the macOS Finder custom-icon file: the
249
+ * literal `Icon` followed by a carriage return (U+000D).
250
+ *
251
+ * This CANNOT be expressed as a `DEFAULT_IGNORES` entry, which is why it is a
252
+ * predicate instead of a pattern string. Two independent reasons:
253
+ *
254
+ * 1. A pattern line ending in a literal CR is inert. The `ignore` package
255
+ * strips the trailing CR, so the line degrades to `Icon` and never
256
+ * matches — verified against ignore@5.3.2:
257
+ * `ignore().add("Icon\r\n").ignores("Icon\r") === false`. Git does the
258
+ * same thing (it reads the CR as part of the line ending), which is why
259
+ * the GitHub macOS template has to end that line with TWO CRs.
260
+ * 2. The usual workaround pattern `Icon?` DOES match, but `?` is a
261
+ * single-character wildcard in gitignore syntax, so it also swallows any
262
+ * real file named `Icons`, `Icon1`, `IconX`. Silently dropping user
263
+ * content is worse than the cruft we are excluding.
264
+ *
265
+ * Matching the exact byte sequence keeps the exclusion precise. Finder writes
266
+ * these into every directory it renders a custom icon for, and a folder-level
267
+ * cloud-sync agent (iCloud Desktop, backup tooling) propagates them across an
268
+ * entire tree — including into `.git/`. They carry no user content.
269
+ *
270
+ * They are also unsyncable by construction: a CR is a control character, so
271
+ * the vault key validator rejects the key outright
272
+ * (INVALID_KEY_CONTROL_CHARS). Excluding them at the walk is what keeps them
273
+ * from reaching the upload path at all.
274
+ */
275
+ export function isMacFinderIconFile(relPath: string): boolean {
276
+ const basename = relPath.replace(/\/$/, "").split("/").pop() ?? "";
277
+ return basename === "Icon\r";
278
+ }
279
+
280
+ /**
281
+ * True when any segment of `relPath` contains a character the vault key
282
+ * validator rejects (see `KEY_CONTROL_CHARS` in s3.ts — C0 controls plus DEL).
283
+ *
284
+ * Kept in lock-step with that validator on purpose: a key this returns `true`
285
+ * for can never be stored, so planning an upload for it only produces a
286
+ * guaranteed per-file failure.
287
+ */
288
+ export function hasControlCharacters(relPath: string): boolean {
289
+ // eslint-disable-next-line no-control-regex
290
+ return /[\x00-\x1F\x7F]/.test(relPath);
291
+ }
292
+
247
293
  export const EXPECTED_IGNORE_SEGMENTS = new Set([
248
294
  ".git",
249
295
  "node_modules",
@@ -307,6 +353,11 @@ export function isExpectedIgnore(relPath: string): boolean {
307
353
  return (
308
354
  basename === ".DS_Store" ||
309
355
  basename === "Thumbs.db" ||
356
+ // Same class as .DS_Store: OS-generated, zero user content. Classified
357
+ // expected so a tree full of them stays silent instead of firing the
358
+ // "EXCLUDED from sync — review in case this is unintended" warning once
359
+ // per file (Finder scatters one into every directory it touches).
360
+ basename === "Icon\r" ||
310
361
  basename === "company.yaml" ||
311
362
  basename === "INDEX.md" ||
312
363
  basename === "modules.lock" ||
@@ -373,6 +424,12 @@ export function createIgnoreFilter(
373
424
  const relative = path.relative(hqRoot, filePath);
374
425
  if (!relative || relative.startsWith("..")) return true; // outside HQ root
375
426
 
427
+ // macOS Finder custom-icon files. Excluded here rather than as a
428
+ // DEFAULT_IGNORES pattern because neither available spelling works —
429
+ // see isMacFinderIconFile for the ignore@5 CR-stripping proof and the
430
+ // `Icon?` over-match hazard.
431
+ if (isMacFinderIconFile(relative)) return false;
432
+
376
433
  // Gitignore dir-only patterns (`foo/`) only match candidate paths that
377
434
  // end with `/`. The `ignore` lib has no stat awareness, so when the
378
435
  // caller knows the entry is a directory we hand the matcher the
package/src/s3.test.ts CHANGED
@@ -1679,6 +1679,12 @@ describe("validateVaultUploadKey — direct-S3 key poisoning guard (incident 202
1679
1679
  ["a/../b.md", "INVALID_KEY_DOT_COMPONENT"],
1680
1680
  ["a/./b.md", "INVALID_KEY_DOT_COMPONENT"],
1681
1681
  ["bad\x00key.md", "INVALID_KEY_CONTROL_CHARS"],
1682
+ // macOS Finder custom-icon file. Named explicitly because this is the
1683
+ // spelling that reaches the validator in the wild: Finder writes one into
1684
+ // every directory it renders a custom icon for, and the push walker now
1685
+ // excludes them precisely because this throw is unavoidable.
1686
+ ["Icon\r", "INVALID_KEY_CONTROL_CHARS"],
1687
+ ["docs/Icon\r", "INVALID_KEY_CONTROL_CHARS"],
1682
1688
  ])(
1683
1689
  "validateVaultUploadKey mirrors the server rule set: %j → %s",
1684
1690
  (key, code) => {