@indigoai-us/hq-cloud 6.14.36 → 6.14.39
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/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +20 -3
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +5 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +46 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +1 -9
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/share.d.ts +178 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +555 -32
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +780 -2
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +27 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +113 -13
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +188 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/lib/readlink-safe.d.ts +11 -0
- package/dist/lib/readlink-safe.d.ts.map +1 -0
- package/dist/lib/readlink-safe.js +27 -0
- package/dist/lib/readlink-safe.js.map +1 -0
- package/dist/lib/readlink-safe.test.d.ts +2 -0
- package/dist/lib/readlink-safe.test.d.ts.map +1 -0
- package/dist/lib/readlink-safe.test.js +34 -0
- package/dist/lib/readlink-safe.test.js.map +1 -0
- package/package.json +1 -1
- package/src/bin/sync-runner-company.ts +19 -3
- package/src/bin/sync-runner.test.ts +54 -0
- package/src/bin/sync-runner.ts +4 -0
- package/src/cli/reindex.ts +1 -9
- package/src/cli/share.test.ts +974 -2
- package/src/cli/share.ts +675 -32
- package/src/cli/sync.test.ts +209 -0
- package/src/cli/sync.ts +151 -13
- package/src/lib/readlink-safe.test.ts +43 -0
- package/src/lib/readlink-safe.ts +29 -0
- package/test/e2e/sync/windows-unreadable-link-leg.test.ts +191 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a link target without turning an expected filesystem race or an
|
|
3
|
+
* unsupported link shape into a caller-wide failure.
|
|
4
|
+
*
|
|
5
|
+
* A `null` result is deliberately distinct from an empty target string: it
|
|
6
|
+
* means the caller must preserve the link boundary and choose its conservative
|
|
7
|
+
* skip/defer path. Non-filesystem exceptions still propagate so programming
|
|
8
|
+
* errors cannot be mistaken for an unreadable link.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readlinkOrNull(linkPath: string, readlink?: (path: string) => string): string | null;
|
|
11
|
+
//# sourceMappingURL=readlink-safe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readlink-safe.d.ts","sourceRoot":"","sources":["../../src/lib/readlink-safe.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAgD,GAC3E,MAAM,GAAG,IAAI,CAcf"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
/**
|
|
3
|
+
* Read a link target without turning an expected filesystem race or an
|
|
4
|
+
* unsupported link shape into a caller-wide failure.
|
|
5
|
+
*
|
|
6
|
+
* A `null` result is deliberately distinct from an empty target string: it
|
|
7
|
+
* means the caller must preserve the link boundary and choose its conservative
|
|
8
|
+
* skip/defer path. Non-filesystem exceptions still propagate so programming
|
|
9
|
+
* errors cannot be mistaken for an unreadable link.
|
|
10
|
+
*/
|
|
11
|
+
export function readlinkOrNull(linkPath, readlink = (path) => fs.readlinkSync(path, "utf8")) {
|
|
12
|
+
try {
|
|
13
|
+
return readlink(linkPath);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
const code = err && typeof err === "object" && "code" in err
|
|
17
|
+
? err.code
|
|
18
|
+
: undefined;
|
|
19
|
+
// Node system errors use POSIX-style errno names such as EINVAL and
|
|
20
|
+
// ENOENT. Runtime/programming errors use ERR_* codes; those must remain
|
|
21
|
+
// loud rather than being misclassified as an unreadable link.
|
|
22
|
+
if (typeof code === "string" && /^E[A-Z0-9]+$/.test(code))
|
|
23
|
+
return null;
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=readlink-safe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readlink-safe.js","sourceRoot":"","sources":["../../src/lib/readlink-safe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAEzB;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAgB,EAChB,WAAqC,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC;IAE5E,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,MAAM,IAAI,GACR,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,IAAI,GAAG;YAC7C,CAAC,CAAE,GAA0B,CAAC,IAAI;YAClC,CAAC,CAAC,SAAS,CAAC;QAChB,oEAAoE;QACpE,wEAAwE;QACxE,8DAA8D;QAC9D,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACvE,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readlink-safe.test.d.ts","sourceRoot":"","sources":["../../src/lib/readlink-safe.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readlinkOrNull } from "./readlink-safe.js";
|
|
3
|
+
function errnoError(code) {
|
|
4
|
+
const err = new Error(`${code}: scripted readlink failure`);
|
|
5
|
+
err.code = code;
|
|
6
|
+
return err;
|
|
7
|
+
}
|
|
8
|
+
describe("readlinkOrNull", () => {
|
|
9
|
+
it("uses the filesystem reader by default", () => {
|
|
10
|
+
expect(readlinkOrNull(`/hq-cloud-missing-link-${process.pid}`)).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
it("returns the target string verbatim when readlink succeeds", () => {
|
|
13
|
+
expect(readlinkOrNull("/test/link", () => "../target with spaces")).toBe("../target with spaces");
|
|
14
|
+
});
|
|
15
|
+
it.each(["EINVAL", "ENOENT", "EPERM"])("returns null for %s", (code) => {
|
|
16
|
+
expect(readlinkOrNull("/test/link", () => {
|
|
17
|
+
throw errnoError(code);
|
|
18
|
+
})).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
it("rethrows non-filesystem failures", () => {
|
|
21
|
+
const defect = new Error("unexpected test defect");
|
|
22
|
+
expect(() => readlinkOrNull("/test/link", () => {
|
|
23
|
+
throw defect;
|
|
24
|
+
})).toThrow(defect);
|
|
25
|
+
});
|
|
26
|
+
it("rethrows code-bearing programming errors", () => {
|
|
27
|
+
const defect = new TypeError("path must be a string");
|
|
28
|
+
defect.code = "ERR_INVALID_ARG_TYPE";
|
|
29
|
+
expect(() => readlinkOrNull("/test/link", () => {
|
|
30
|
+
throw defect;
|
|
31
|
+
})).toThrow(defect);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=readlink-safe.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readlink-safe.test.js","sourceRoot":"","sources":["../../src/lib/readlink-safe.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,GAAG,IAAI,6BAA6B,CAA0B,CAAC;IACrF,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,cAAc,CAAC,0BAA0B,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC7E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,cAAc,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CACtE,uBAAuB,CACxB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,qBAAqB,EAAE,CAAC,IAAI,EAAE,EAAE;QACrE,MAAM,CAAC,cAAc,CAAC,YAAY,EAAE,GAAG,EAAE;YACvC,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACjB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;QAEnD,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,YAAY,EAAE,GAAG,EAAE;YAC7C,MAAM,MAAM,CAAC;QACf,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,uBAAuB,CAAiC,CAAC;QACtF,MAAM,CAAC,IAAI,GAAG,sBAAsB,CAAC;QAErC,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,YAAY,EAAE,GAAG,EAAE;YAC7C,MAAM,MAAM,CAAC;QACf,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -183,15 +183,20 @@ export async function executeCompanyFanout(options: {
|
|
|
183
183
|
...(event.errorCode ? { errorCode: event.errorCode } : {}),
|
|
184
184
|
});
|
|
185
185
|
} else if (event.type === "skip-junk-key-spelling") {
|
|
186
|
-
// Deliberate skip: forwarded for visibility, NEVER pushed into
|
|
187
|
-
// errors[] (policy hq-sync-deliberate-skip-not-fatal-error-exit2 —
|
|
188
|
-
// a benign recurring skip must not gate exit 2 / Sentry alerts).
|
|
189
186
|
options.emit({
|
|
190
187
|
type: "skip-junk-key-spelling",
|
|
191
188
|
company: companyLabel,
|
|
192
189
|
path: event.path,
|
|
193
190
|
journaledKey: event.journaledKey,
|
|
194
191
|
});
|
|
192
|
+
} else if (event.type === "not-shipped") {
|
|
193
|
+
options.emit({
|
|
194
|
+
type: "not-shipped",
|
|
195
|
+
company: companyLabel,
|
|
196
|
+
reason: event.reason,
|
|
197
|
+
count: event.count,
|
|
198
|
+
samplePaths: event.samplePaths,
|
|
199
|
+
});
|
|
195
200
|
}
|
|
196
201
|
};
|
|
197
202
|
|
|
@@ -208,6 +213,8 @@ export async function executeCompanyFanout(options: {
|
|
|
208
213
|
filesExcludedByPolicy: 0,
|
|
209
214
|
filesExcludedByScope: 0,
|
|
210
215
|
filesExcludedByIgnore: 0,
|
|
216
|
+
unreachablePaths: [],
|
|
217
|
+
linkedSubtreesNotShipped: [],
|
|
211
218
|
conflictPaths: [],
|
|
212
219
|
aborted: false,
|
|
213
220
|
};
|
|
@@ -288,6 +295,15 @@ export async function executeCompanyFanout(options: {
|
|
|
288
295
|
skipUnchanged: true,
|
|
289
296
|
propagateDeletes: true,
|
|
290
297
|
propagateDeletePolicy: options.resolveDeletePolicy(),
|
|
298
|
+
// `pushPaths` are INTERNAL walk roots (the company folder / the
|
|
299
|
+
// personal-vault dirs), never user input, so an unreachable entry
|
|
300
|
+
// here is a benign race — a directory removed between the scan and
|
|
301
|
+
// the push — and must not fail an unattended background sync. Record
|
|
302
|
+
// + report it (`not-shipped` event, `ShareResult.unreachablePaths`)
|
|
303
|
+
// and carry on. The interactive `hq sync push <path>` keeps the
|
|
304
|
+
// default "error" policy, which is where a user-typed path that
|
|
305
|
+
// silently ships nothing is a real bug (feedback_a51cb63d ask #2).
|
|
306
|
+
unreachablePathPolicy: "warn",
|
|
291
307
|
onEvent: tagAndEmit,
|
|
292
308
|
...(options.uploadAuthor ? { author: options.uploadAuthor } : {}),
|
|
293
309
|
...(target.personalMode !== undefined
|
|
@@ -157,6 +157,8 @@ function defaultShareResult(overrides: Partial<ShareResult> = {}): ShareResult {
|
|
|
157
157
|
filesExcludedByPolicy: 0,
|
|
158
158
|
filesExcludedByScope: 0,
|
|
159
159
|
filesExcludedByIgnore: 0,
|
|
160
|
+
unreachablePaths: [],
|
|
161
|
+
linkedSubtreesNotShipped: [],
|
|
160
162
|
conflictPaths: [],
|
|
161
163
|
aborted: false,
|
|
162
164
|
...overrides,
|
|
@@ -2025,6 +2027,58 @@ describe("per-company fanout", () => {
|
|
|
2025
2027
|
// ---------------------------------------------------------------------------
|
|
2026
2028
|
|
|
2027
2029
|
describe("all-complete aggregate", () => {
|
|
2030
|
+
it("reports every unreadable-link fallback by path without turning the company leg into PARTIAL_SYNC_EXIT", async () => {
|
|
2031
|
+
const unreadablePaths = [
|
|
2032
|
+
"policies/scope-invalid-tombstone",
|
|
2033
|
+
"docs/conflict-convergence-probe",
|
|
2034
|
+
"skills/downloaded-link-hash",
|
|
2035
|
+
];
|
|
2036
|
+
const deps = makeDeps({
|
|
2037
|
+
createVaultClient: () =>
|
|
2038
|
+
makeVaultStub({
|
|
2039
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
2040
|
+
entityGet: (uid: string) =>
|
|
2041
|
+
Promise.resolve({
|
|
2042
|
+
uid,
|
|
2043
|
+
type: "company",
|
|
2044
|
+
slug: "acme",
|
|
2045
|
+
bucketName: "hq-vault-acme-test",
|
|
2046
|
+
status: "active",
|
|
2047
|
+
} as unknown as EntityInfo),
|
|
2048
|
+
}),
|
|
2049
|
+
share: vi.fn(async (options: ShareOptions) => {
|
|
2050
|
+
for (const path of unreadablePaths) {
|
|
2051
|
+
options.onEvent?.({
|
|
2052
|
+
type: "not-shipped",
|
|
2053
|
+
reason: "unreadable-link",
|
|
2054
|
+
count: 1,
|
|
2055
|
+
samplePaths: [path],
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
return defaultShareResult();
|
|
2059
|
+
}),
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
const code = await runRunner(["--companies", "--direction", "push"], deps);
|
|
2063
|
+
|
|
2064
|
+
for (const path of unreadablePaths) {
|
|
2065
|
+
expect(deps.stdout.events()).toContainEqual({
|
|
2066
|
+
type: "not-shipped",
|
|
2067
|
+
company: "acme",
|
|
2068
|
+
reason: "unreadable-link",
|
|
2069
|
+
count: 1,
|
|
2070
|
+
samplePaths: [path],
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
const all = deps.stdout.events().find(
|
|
2074
|
+
(event): event is Extract<RunnerEvent, { type: "all-complete" }> =>
|
|
2075
|
+
event.type === "all-complete",
|
|
2076
|
+
);
|
|
2077
|
+
expect(all?.errors).toEqual([]);
|
|
2078
|
+
expect(all?.partial).toBe(false);
|
|
2079
|
+
expect(code).toBe(0);
|
|
2080
|
+
});
|
|
2081
|
+
|
|
2028
2082
|
it("emits all-complete when telemetry never resolves", async () => {
|
|
2029
2083
|
const deps = makeDeps({
|
|
2030
2084
|
createVaultClient: () =>
|
package/src/bin/sync-runner.ts
CHANGED
|
@@ -402,6 +402,10 @@ export type RunnerEvent =
|
|
|
402
402
|
Extract<SyncProgressEvent, { type: "skip-junk-key-spelling" }>,
|
|
403
403
|
"type"
|
|
404
404
|
>)
|
|
405
|
+
| ({ type: "not-shipped"; company: string } & Omit<
|
|
406
|
+
Extract<SyncProgressEvent, { type: "not-shipped" }>,
|
|
407
|
+
"type"
|
|
408
|
+
>)
|
|
405
409
|
| ({
|
|
406
410
|
type: "complete";
|
|
407
411
|
company: string;
|
package/src/cli/reindex.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { spawnSync } from "child_process";
|
|
|
22
22
|
import * as fs from "fs";
|
|
23
23
|
import * as os from "os";
|
|
24
24
|
import * as path from "path";
|
|
25
|
+
import { readlinkOrNull } from "../lib/readlink-safe.js";
|
|
25
26
|
import {
|
|
26
27
|
acquireOperationLock,
|
|
27
28
|
OperationLockedError,
|
|
@@ -126,15 +127,6 @@ function lstatOrNull(p: string): fs.Stats | null {
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
/** readlink without throwing (null when not a symlink / missing). */
|
|
130
|
-
function readlinkOrNull(p: string): string | null {
|
|
131
|
-
try {
|
|
132
|
-
return fs.readlinkSync(p);
|
|
133
|
-
} catch {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
130
|
/**
|
|
139
131
|
* Entries of `dir` matching a bash `dir/*` glob: dotfiles excluded (no
|
|
140
132
|
* dotglob), sorted lexicographically (bash glob order). Missing dir → [].
|