@indigoai-us/hq-cloud 6.14.22 → 6.14.24
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 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-events.test.js +22 -0
- package/dist/bin/sync-runner-events.test.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts +11 -0
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +37 -2
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.js +1 -0
- package/dist/bin/sync-runner-planning.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +17 -1
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +33 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +28 -3
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +206 -2
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/doctor.d.ts +119 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +485 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/doctor.test.d.ts +13 -0
- package/dist/cli/doctor.test.d.ts.map +1 -0
- package/dist/cli/doctor.test.js +485 -0
- package/dist/cli/doctor.test.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/share.js +6 -1
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +1 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +54 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +223 -3
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +394 -1
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cognito-auth.test.js +7 -0
- package/dist/cognito-auth.test.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/conflict-file.d.ts.map +1 -1
- package/dist/lib/conflict-file.js +5 -1
- package/dist/lib/conflict-file.js.map +1 -1
- package/dist/lib/conflict-index.d.ts +7 -0
- package/dist/lib/conflict-index.d.ts.map +1 -1
- package/dist/lib/conflict-index.js +10 -2
- package/dist/lib/conflict-index.js.map +1 -1
- package/dist/lib/conflict.test.js +31 -0
- package/dist/lib/conflict.test.js.map +1 -1
- package/dist/lib/machine-id.test.js +14 -0
- package/dist/lib/machine-id.test.js.map +1 -1
- package/dist/local-path-codec.d.ts +18 -0
- package/dist/local-path-codec.d.ts.map +1 -1
- package/dist/local-path-codec.js +63 -0
- package/dist/local-path-codec.js.map +1 -1
- package/dist/local-path-codec.test.d.ts +14 -0
- package/dist/local-path-codec.test.d.ts.map +1 -0
- package/dist/local-path-codec.test.js +102 -0
- package/dist/local-path-codec.test.js.map +1 -0
- package/dist/machine-auth.test.js +7 -0
- package/dist/machine-auth.test.js.map +1 -1
- package/dist/s3.d.ts +5 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +129 -3
- package/dist/s3.js.map +1 -1
- package/dist/s3.symlink-materialize.test.d.ts +18 -0
- package/dist/s3.symlink-materialize.test.d.ts.map +1 -0
- package/dist/s3.symlink-materialize.test.js +394 -0
- package/dist/s3.symlink-materialize.test.js.map +1 -0
- package/dist/s3.test.js +19 -0
- package/dist/s3.test.js.map +1 -1
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +23 -2
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +17 -0
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +10 -1
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +28 -0
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-company.ts +18 -0
- package/src/bin/sync-runner-events.test.ts +31 -0
- package/src/bin/sync-runner-planning.test.ts +1 -0
- package/src/bin/sync-runner-planning.ts +46 -3
- package/src/bin/sync-runner-watch-loop.ts +19 -1
- package/src/bin/sync-runner.test.ts +236 -1
- package/src/bin/sync-runner.ts +57 -2
- package/src/cli/doctor.test.ts +581 -0
- package/src/cli/doctor.ts +640 -0
- package/src/cli/index.ts +14 -0
- package/src/cli/share.test.ts +1 -0
- package/src/cli/share.ts +6 -1
- package/src/cli/sync.test.ts +473 -1
- package/src/cli/sync.ts +310 -3
- package/src/cognito-auth.test.ts +5 -0
- package/src/index.ts +10 -0
- package/src/lib/conflict-file.ts +6 -1
- package/src/lib/conflict-index.ts +18 -2
- package/src/lib/conflict.test.ts +33 -0
- package/src/lib/machine-id.test.ts +10 -0
- package/src/local-path-codec.test.ts +138 -0
- package/src/local-path-codec.ts +66 -0
- package/src/machine-auth.test.ts +5 -0
- package/src/s3.symlink-materialize.test.ts +492 -0
- package/src/s3.test.ts +24 -0
- package/src/s3.ts +148 -4
- package/src/skill-telemetry.test.ts +19 -0
- package/src/skill-telemetry.ts +31 -2
- package/src/watcher.test.ts +33 -0
- package/src/watcher.ts +10 -1
package/src/cli/sync.ts
CHANGED
|
@@ -19,7 +19,12 @@ import {
|
|
|
19
19
|
} from "../telemetry-events.js";
|
|
20
20
|
import { resolveEntityContext, isExpiringSoon, refreshEntityContext } from "../context.js";
|
|
21
21
|
import { createSyncProgressRecorder } from "../sync-progress.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
canonicalVaultKeySpelling,
|
|
24
|
+
localPathForVaultKey,
|
|
25
|
+
vaultKeyForLocalPath,
|
|
26
|
+
} from "../local-path-codec.js";
|
|
27
|
+
import { SKILLS_KEY_PREFIX } from "./doctor.js";
|
|
23
28
|
import {
|
|
24
29
|
downloadFile,
|
|
25
30
|
listRemoteFiles,
|
|
@@ -307,6 +312,32 @@ export type SyncProgressEvent =
|
|
|
307
312
|
count: number;
|
|
308
313
|
samplePaths: string[];
|
|
309
314
|
}
|
|
315
|
+
| {
|
|
316
|
+
/**
|
|
317
|
+
* Emitted at most ONCE per PULL leg when the leg ran under a
|
|
318
|
+
* membership-scoped `syncMode` (`"shared"` or `"custom"` — never `"all"`)
|
|
319
|
+
* AND one or more remote keys were skipped as out-of-scope
|
|
320
|
+
* (`filesOutOfScope > 0`). This turns the silent shared-vs-all gap into a
|
|
321
|
+
* VISIBLE, actionable surface. The reporter of feedback_d2082110 lost
|
|
322
|
+
* files across devices precisely because four memberships defaulted to
|
|
323
|
+
* `shared` rather than `all`, so not all vault content materialized on the
|
|
324
|
+
* second device — and nothing told them; they had to notice the gap and
|
|
325
|
+
* flip each membership to `all` by hand. This event names the gap and the
|
|
326
|
+
* lever (raise the membership's access level to `all`) so FULL
|
|
327
|
+
* materialization is a deliberate, visible choice rather than a silent
|
|
328
|
+
* omission.
|
|
329
|
+
*
|
|
330
|
+
* `count` is the number of remote keys skipped as out-of-scope on this
|
|
331
|
+
* leg; `samplePaths` carries up to 10 company-relative keys for display;
|
|
332
|
+
* `syncMode` is the active scoped mode. Distinct from the push-side
|
|
333
|
+
* `scope-excluded` (which reports what a grantee could not PUSH). Not
|
|
334
|
+
* emitted in `all` mode or when `count === 0` — no gap, no noise.
|
|
335
|
+
*/
|
|
336
|
+
type: "scope-materialization-gap";
|
|
337
|
+
count: number;
|
|
338
|
+
samplePaths: string[];
|
|
339
|
+
syncMode: SyncMode;
|
|
340
|
+
}
|
|
310
341
|
| {
|
|
311
342
|
/**
|
|
312
343
|
* Emitted at most ONCE per `share()` push leg when the base ignore
|
|
@@ -375,6 +406,27 @@ export type SyncProgressEvent =
|
|
|
375
406
|
path: string;
|
|
376
407
|
/** Server-compatible invalid-key code, such as INVALID_KEY_CONTROL_CHARS. */
|
|
377
408
|
errorCode?: string;
|
|
409
|
+
}
|
|
410
|
+
| {
|
|
411
|
+
/**
|
|
412
|
+
* Emitted by the PULL leg once per remote key skipped because its
|
|
413
|
+
* spelling percent-decodes to the same logical path as an
|
|
414
|
+
* already-journaled key with a DIFFERENT spelling — the mixed-version
|
|
415
|
+
* amplifier (a pre-codec peer minting `%3A`/`%253A` twins of a
|
|
416
|
+
* canonical `:` key). Downloading the twin would materialize a junk
|
|
417
|
+
* local dir and fork the journal into a second key family for one
|
|
418
|
+
* logical path, so the established spelling wins and the twin is
|
|
419
|
+
* skipped. Like `skip-size-limit`, this is deliberately NOT
|
|
420
|
+
* `type: "error"` (policy hq-sync-deliberate-skip-not-fatal-error-exit2:
|
|
421
|
+
* a recurring benign per-file skip emitted as an error lands in the
|
|
422
|
+
* runner's `errors[]`, forces exit 2 on every pass, and the menubar
|
|
423
|
+
* Sentry-alerts each time). The US-003 vault doctor is the cleanup
|
|
424
|
+
* path that collapses the junk family itself.
|
|
425
|
+
*/
|
|
426
|
+
type: "skip-junk-key-spelling";
|
|
427
|
+
path: string;
|
|
428
|
+
/** The already-journaled spelling that owns this logical path. */
|
|
429
|
+
journaledKey: string;
|
|
378
430
|
};
|
|
379
431
|
|
|
380
432
|
export interface SyncOptions {
|
|
@@ -856,6 +908,8 @@ async function syncWithOperationLockHeld(
|
|
|
856
908
|
await verifyPlannedJournalTombstones(run, plan);
|
|
857
909
|
executeJournalTombstoneDeletes(run, plan, counters);
|
|
858
910
|
|
|
911
|
+
emitScopeMaterializationGap(run, plan, counters);
|
|
912
|
+
|
|
859
913
|
return finalizePullRun(run, plan, scopeRun, counters);
|
|
860
914
|
}
|
|
861
915
|
|
|
@@ -1170,7 +1224,8 @@ async function executeConflictExecutor(
|
|
|
1170
1224
|
item.action === "skip-personal-mode" ||
|
|
1171
1225
|
item.action === "skip-unchanged" ||
|
|
1172
1226
|
item.action === "skip-local-only" ||
|
|
1173
|
-
item.action === "skip-stale-overlay-marker"
|
|
1227
|
+
item.action === "skip-stale-overlay-marker" ||
|
|
1228
|
+
item.action === "skip-overlay-marker-with-children"
|
|
1174
1229
|
) {
|
|
1175
1230
|
counters.filesSkipped++;
|
|
1176
1231
|
continue;
|
|
@@ -1182,6 +1237,18 @@ async function executeConflictExecutor(
|
|
|
1182
1237
|
counters.filesOutOfScope++;
|
|
1183
1238
|
continue;
|
|
1184
1239
|
}
|
|
1240
|
+
if (item.action === "skip-junk-key-spelling") {
|
|
1241
|
+
// Deliberate, recoverable skip — counted as skipped and surfaced via a
|
|
1242
|
+
// dedicated warning event, never `type: "error"` (see the event doc /
|
|
1243
|
+
// policy hq-sync-deliberate-skip-not-fatal-error-exit2).
|
|
1244
|
+
counters.filesSkipped++;
|
|
1245
|
+
run.emit({
|
|
1246
|
+
type: "skip-junk-key-spelling",
|
|
1247
|
+
path: item.remoteFile.key,
|
|
1248
|
+
journaledKey: item.journaledKey,
|
|
1249
|
+
});
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1185
1252
|
if (item.action === "tombstone-delete") {
|
|
1186
1253
|
executeFileTombstoneDelete(run, item, counters);
|
|
1187
1254
|
continue;
|
|
@@ -1844,6 +1911,37 @@ function finalizePullRun(
|
|
|
1844
1911
|
};
|
|
1845
1912
|
}
|
|
1846
1913
|
|
|
1914
|
+
/**
|
|
1915
|
+
* Surface the shared-vs-all materialization gap (feedback_d2082110). When a
|
|
1916
|
+
* pull leg ran under a membership-scoped `syncMode` and skipped one or more
|
|
1917
|
+
* remote keys as out-of-scope, emit a single summary event so the operator
|
|
1918
|
+
* SEES that not all vault content materialized on this device and knows the
|
|
1919
|
+
* lever — raise the membership's access level to `all`. Silent in `all` mode
|
|
1920
|
+
* (nothing is scoped away) and when nothing fell out of scope: no gap, no
|
|
1921
|
+
* noise. Deliberately AFTER the transfer executors, so `filesOutOfScope` is
|
|
1922
|
+
* final; sample keys are read off the plan's `skip-out-of-scope` items (pure,
|
|
1923
|
+
* no I/O). This is the visible complement to the previously-silent
|
|
1924
|
+
* `SyncResult.filesOutOfScope` count.
|
|
1925
|
+
*/
|
|
1926
|
+
function emitScopeMaterializationGap(
|
|
1927
|
+
run: PullRunContext,
|
|
1928
|
+
plan: PullPlan,
|
|
1929
|
+
counters: PullCounters,
|
|
1930
|
+
): void {
|
|
1931
|
+
if (run.syncMode === "all") return;
|
|
1932
|
+
if (counters.filesOutOfScope <= 0) return;
|
|
1933
|
+
const samplePaths = plan.items
|
|
1934
|
+
.filter((item) => item.action === "skip-out-of-scope")
|
|
1935
|
+
.slice(0, 10)
|
|
1936
|
+
.map((item) => item.remoteFile.key);
|
|
1937
|
+
run.emit({
|
|
1938
|
+
type: "scope-materialization-gap",
|
|
1939
|
+
count: counters.filesOutOfScope,
|
|
1940
|
+
samplePaths,
|
|
1941
|
+
syncMode: run.syncMode,
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1847
1945
|
/**
|
|
1848
1946
|
* Decide whether a remote object present in the LIST is a GENUINE RE-CREATE
|
|
1849
1947
|
* written AFTER a FILE_TOMBSTONE — in which case the tombstone is stale and the
|
|
@@ -2034,6 +2132,15 @@ type PullPlanItem =
|
|
|
2034
2132
|
// no-action-needed case is never surfaced with the alarming "rm -rf the
|
|
2035
2133
|
// local directory" reconciliation advice.
|
|
2036
2134
|
| { action: "skip-stale-overlay-marker"; remoteFile: RemoteFile; localPath: string }
|
|
2135
|
+
// A directory-overlay marker (a single vault object at a key that ALSO has
|
|
2136
|
+
// child objects under it in the same LIST — a symlink record for a directory
|
|
2137
|
+
// mirrored from another tree) colliding with a local REAL directory whose
|
|
2138
|
+
// contents are already materialized. The marker can never be renamed over the
|
|
2139
|
+
// directory; its children carry the real content and materialize via their own
|
|
2140
|
+
// plan items. A benign, no-action skip — distinct from skip-local-only so the
|
|
2141
|
+
// case is never surfaced with the alarming (and, for a materialized directory,
|
|
2142
|
+
// data-destroying) "rm -rf the local directory to pull" advice. feedback_d2082110.
|
|
2143
|
+
| { action: "skip-overlay-marker-with-children"; remoteFile: RemoteFile; localPath: string }
|
|
2037
2144
|
// Remote keys refused by ephemeral-mirror policy. The push walker has
|
|
2038
2145
|
// refused to upload these since 5.33.0; the pull walker now refuses to
|
|
2039
2146
|
// download them so legacy litter in cloud staging drains naturally.
|
|
@@ -2042,6 +2149,14 @@ type PullPlanItem =
|
|
|
2042
2149
|
// the remote LIST (and accessible per STS) but deliberately not downloaded
|
|
2043
2150
|
// because the membership's sync scope doesn't cover them.
|
|
2044
2151
|
| { action: "skip-out-of-scope"; remoteFile: RemoteFile; localPath: string }
|
|
2152
|
+
| {
|
|
2153
|
+
// Mixed-version amplifier guard — see the `skip-junk-key-spelling`
|
|
2154
|
+
// event doc. Carries the journaled spelling for the warning surface.
|
|
2155
|
+
action: "skip-junk-key-spelling";
|
|
2156
|
+
remoteFile: RemoteFile;
|
|
2157
|
+
localPath: string;
|
|
2158
|
+
journaledKey: string;
|
|
2159
|
+
}
|
|
2045
2160
|
// Remote key present in the LIST but carrying a FILE_TOMBSTONE that marks it
|
|
2046
2161
|
// intentionally deleted (and the remote object is NOT a newer re-create). The
|
|
2047
2162
|
// executor deletes any local copy and drops the journal entry — the
|
|
@@ -2139,6 +2254,39 @@ interface PullPlan {
|
|
|
2139
2254
|
const PULL_INTENTIONAL_DELETE_MIN_ABS = 10;
|
|
2140
2255
|
const PULL_INTENTIONAL_DELETE_RATIO = 0.1;
|
|
2141
2256
|
|
|
2257
|
+
/**
|
|
2258
|
+
* win32 only: true when an existing local symlink's TARGET exists but the
|
|
2259
|
+
* link cannot be traversed as that target's type — the file-flavor-link-at-
|
|
2260
|
+
* a-directory shape minted while the target was absent (or by an older
|
|
2261
|
+
* client). A dangling link is NOT broken-flavored (there is nothing to
|
|
2262
|
+
* repair until the target appears). POSIX links are flavorless: always
|
|
2263
|
+
* false off win32.
|
|
2264
|
+
*/
|
|
2265
|
+
export function win32SymlinkFlavorBroken(
|
|
2266
|
+
localPath: string,
|
|
2267
|
+
win32: boolean = process.platform === "win32",
|
|
2268
|
+
): boolean {
|
|
2269
|
+
if (!win32) return false;
|
|
2270
|
+
let target: string;
|
|
2271
|
+
try {
|
|
2272
|
+
target = fs.readlinkSync(localPath);
|
|
2273
|
+
} catch {
|
|
2274
|
+
return false;
|
|
2275
|
+
}
|
|
2276
|
+
const resolved = path.resolve(path.dirname(localPath), target);
|
|
2277
|
+
let targetIsDir: boolean;
|
|
2278
|
+
try {
|
|
2279
|
+
targetIsDir = fs.statSync(resolved).isDirectory();
|
|
2280
|
+
} catch {
|
|
2281
|
+
return false; // dangling target — nothing to repair yet
|
|
2282
|
+
}
|
|
2283
|
+
try {
|
|
2284
|
+
return fs.statSync(localPath).isDirectory() !== targetIsDir;
|
|
2285
|
+
} catch {
|
|
2286
|
+
return true; // target exists but the link cannot traverse: wrong flavor
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2142
2290
|
function computePullPlan(
|
|
2143
2291
|
remoteFiles: RemoteFile[],
|
|
2144
2292
|
journal: SyncJournal,
|
|
@@ -2176,6 +2324,46 @@ function computePullPlan(
|
|
|
2176
2324
|
localPath: string;
|
|
2177
2325
|
}> = [];
|
|
2178
2326
|
|
|
2327
|
+
// Junk-spelling guard (mixed-version amplifier): index every journaled key
|
|
2328
|
+
// by its canonical (percent-decode fixpoint) form so a remote twin minted
|
|
2329
|
+
// under a different spelling is recognized before it materializes. When
|
|
2330
|
+
// several journaled spellings already share one canonical form (a
|
|
2331
|
+
// pre-guard fork), the exactly-canonical spelling is preferred as the
|
|
2332
|
+
// representative; entries journaled under their own exact spelling are
|
|
2333
|
+
// never gated by this map (see the guard below), so pre-existing families
|
|
2334
|
+
// keep syncing untouched until the vault doctor (US-003) collapses them.
|
|
2335
|
+
const journaledKeyByCanonical = new Map<string, string>();
|
|
2336
|
+
for (const journaledKey of Object.keys(journal.files)) {
|
|
2337
|
+
if (!journaledKey.startsWith(SKILLS_KEY_PREFIX)) continue;
|
|
2338
|
+
const canonical = canonicalVaultKeySpelling(journaledKey);
|
|
2339
|
+
const existing = journaledKeyByCanonical.get(canonical);
|
|
2340
|
+
if (existing === undefined || journaledKey === canonical) {
|
|
2341
|
+
journaledKeyByCanonical.set(canonical, journaledKey);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// Remote keys that ALSO appear as an ancestor of another remote key — i.e.
|
|
2346
|
+
// keys that carry child objects under `${key}/…` in this same LIST. The
|
|
2347
|
+
// vault stores a directory OVERLAY (a symlink into another tree, e.g. a
|
|
2348
|
+
// company skill mirrored from a shared repo) as a symlink RECORD at the
|
|
2349
|
+
// directory's own key AND stores the directory's contents as separate child
|
|
2350
|
+
// objects beneath it. A REAL directory, by contrast, only ever syncs as
|
|
2351
|
+
// those child objects — never as a single object AT its own key. So a single
|
|
2352
|
+
// remote object sitting at a key that also has children is unambiguously a
|
|
2353
|
+
// directory-overlay marker, not a regular file. Precomputed once (pure path
|
|
2354
|
+
// derivation, no I/O — the planner is synchronous) so the dir-vs-object
|
|
2355
|
+
// collision branch can tell an inert overlay marker apart from a genuine
|
|
2356
|
+
// file-vs-directory structural collision. feedback_d2082110.
|
|
2357
|
+
const remoteKeysWithChildren = new Set<string>();
|
|
2358
|
+
for (const rf of remoteFiles) {
|
|
2359
|
+
const key = toPosixKey(rf.key);
|
|
2360
|
+
let slash = key.indexOf("/");
|
|
2361
|
+
while (slash !== -1) {
|
|
2362
|
+
remoteKeysWithChildren.add(key.slice(0, slash));
|
|
2363
|
+
slash = key.indexOf("/", slash + 1);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2179
2367
|
for (const remoteFile of remoteFiles) {
|
|
2180
2368
|
const invalidKey = classifyVaultKey(
|
|
2181
2369
|
remoteFile.key,
|
|
@@ -2315,6 +2503,43 @@ function computePullPlan(
|
|
|
2315
2503
|
continue;
|
|
2316
2504
|
}
|
|
2317
2505
|
|
|
2506
|
+
// Junk-spelling guard — SKILL-FARM KEYS ONLY. The amplifier is specific
|
|
2507
|
+
// to `.claude/skills/<ns>:<skill>` wrapper spellings; outside that prefix,
|
|
2508
|
+
// keys whose percent-decode fixpoints collide (`notes/a b.md` vs a genuine
|
|
2509
|
+
// `notes/a%20b.md`) are DISTINCT files that must both pull.
|
|
2510
|
+
// Within the farm: a remote key that is NOT journaled under its own
|
|
2511
|
+
// spelling but whose percent-decode fixpoint matches an already-journaled
|
|
2512
|
+
// key with a different spelling is a mixed-version twin (`%3A`/`%253A`
|
|
2513
|
+
// generations of a canonical `:` key). Materializing it would mint junk
|
|
2514
|
+
// local dirs and fork the journal into a second key family for the same
|
|
2515
|
+
// logical path — skip it (loudly, never fatally; see the
|
|
2516
|
+
// `skip-junk-key-spelling` event doc) and leave the journaled spelling
|
|
2517
|
+
// authoritative for this machine.
|
|
2518
|
+
if (
|
|
2519
|
+
journalEntry === undefined &&
|
|
2520
|
+
remoteFile.key.startsWith(SKILLS_KEY_PREFIX) &&
|
|
2521
|
+
// Gate NON-canonical remote spellings only. A remote key that already
|
|
2522
|
+
// IS the canonical spelling must always be downloadable — after the
|
|
2523
|
+
// doctor collapses the vault, a machine whose journal still holds only
|
|
2524
|
+
// the junk spelling would otherwise skip the canonical object forever
|
|
2525
|
+
// (and the delete-resync twin protection keeps that stale junk entry),
|
|
2526
|
+
// deadlocking it out of every future update.
|
|
2527
|
+
canonicalVaultKeySpelling(remoteFile.key) !== remoteFile.key
|
|
2528
|
+
) {
|
|
2529
|
+
const journaledTwin = journaledKeyByCanonical.get(
|
|
2530
|
+
canonicalVaultKeySpelling(remoteFile.key),
|
|
2531
|
+
);
|
|
2532
|
+
if (journaledTwin !== undefined && journaledTwin !== remoteFile.key) {
|
|
2533
|
+
items.push({
|
|
2534
|
+
action: "skip-junk-key-spelling",
|
|
2535
|
+
remoteFile,
|
|
2536
|
+
localPath,
|
|
2537
|
+
journaledKey: journaledTwin,
|
|
2538
|
+
});
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2318
2543
|
// ── FILE_TOMBSTONE consult (delete-resync) ───────────────────────────────
|
|
2319
2544
|
// A remote object present in the LIST may be an intentionally-deleted key
|
|
2320
2545
|
// (a peer re-pushed it, or its delete-marker hasn't propagated to this
|
|
@@ -2437,6 +2662,30 @@ function computePullPlan(
|
|
|
2437
2662
|
});
|
|
2438
2663
|
continue;
|
|
2439
2664
|
}
|
|
2665
|
+
// Directory-overlay marker vs. a locally-materialized directory:
|
|
2666
|
+
// the remote object at this key carries CHILD objects under it in the
|
|
2667
|
+
// same LIST (`remoteKeysWithChildren`), so it is a symlink/overlay
|
|
2668
|
+
// record for a directory — e.g. the DPP vault's `skills/cody-copywriter`
|
|
2669
|
+
// overlay whose 13 child objects are already materialized locally. The
|
|
2670
|
+
// marker can never be renamed over the real directory, and its children
|
|
2671
|
+
// carry the actual content and materialize via their own plan items, so
|
|
2672
|
+
// the marker is a benign, no-action skip. The generic "rm -rf the local
|
|
2673
|
+
// directory to pull" advice below would tell the operator to DELETE the
|
|
2674
|
+
// materialized (and possibly locally-authored) contents to install an
|
|
2675
|
+
// inert link, and it recurred on EVERY sync cycle. feedback_d2082110.
|
|
2676
|
+
if (remoteKeysWithChildren.has(toPosixKey(remoteFile.key))) {
|
|
2677
|
+
console.error(
|
|
2678
|
+
` Note: ${remoteFile.key} is a directory-overlay marker whose ` +
|
|
2679
|
+
`contents already exist locally as a directory; keeping the ` +
|
|
2680
|
+
`local directory and ignoring the marker (no action needed).`,
|
|
2681
|
+
);
|
|
2682
|
+
items.push({
|
|
2683
|
+
action: "skip-overlay-marker-with-children",
|
|
2684
|
+
remoteFile,
|
|
2685
|
+
localPath,
|
|
2686
|
+
});
|
|
2687
|
+
continue;
|
|
2688
|
+
}
|
|
2440
2689
|
console.error(
|
|
2441
2690
|
` Warning: ${remoteFile.key} exists locally as a directory; ` +
|
|
2442
2691
|
`cloud has a single object at this key. Skipping; manual ` +
|
|
@@ -2537,6 +2786,25 @@ function computePullPlan(
|
|
|
2537
2786
|
continue;
|
|
2538
2787
|
}
|
|
2539
2788
|
if (journalEntry && !localChanged && !remoteChanged) {
|
|
2789
|
+
// Flavor-health repair (win32 skill farm): a link journaled while its
|
|
2790
|
+
// target was absent was minted file-flavor; once the target directory
|
|
2791
|
+
// exists the hashes still match (hash = sha256(target)) so this
|
|
2792
|
+
// branch would skip it forever, leaving the wrapper broken. Re-download
|
|
2793
|
+
// instead — the materializer's flavor-aware path recreates it 'dir'.
|
|
2794
|
+
if (
|
|
2795
|
+
isLocalSymlink &&
|
|
2796
|
+
remoteFile.key.startsWith(SKILLS_KEY_PREFIX) &&
|
|
2797
|
+
win32SymlinkFlavorBroken(localPath)
|
|
2798
|
+
) {
|
|
2799
|
+
items.push({
|
|
2800
|
+
action: "download",
|
|
2801
|
+
remoteFile,
|
|
2802
|
+
localPath,
|
|
2803
|
+
isNew: false,
|
|
2804
|
+
localSnapshot: plannedLocalSnapshot,
|
|
2805
|
+
});
|
|
2806
|
+
continue;
|
|
2807
|
+
}
|
|
2540
2808
|
items.push({ action: "skip-unchanged", remoteFile, localPath });
|
|
2541
2809
|
continue;
|
|
2542
2810
|
}
|
|
@@ -2730,7 +2998,20 @@ function computePullPlan(
|
|
|
2730
2998
|
// as a local delete + journal removal. Symmetric to the push side's
|
|
2731
2999
|
// `propagateDeletes` plan in share.ts.
|
|
2732
3000
|
const remoteKeySet = new Set<string>();
|
|
2733
|
-
|
|
3001
|
+
// Skill-farm twin protection (companion to the junk-spelling pull guard):
|
|
3002
|
+
// when the LIST holds only a junk spelling of a journaled canonical key,
|
|
3003
|
+
// the logical object is still remotely present — a raw has() would class
|
|
3004
|
+
// the canonical journal entry as remote-deleted, unlink the good local
|
|
3005
|
+
// wrapper, and drop the journal entry (after which the next pull would
|
|
3006
|
+
// materialize the junk spelling the guard just refused). Track canonical
|
|
3007
|
+
// forms of listed skill keys and consult them below.
|
|
3008
|
+
const remoteCanonicalSkillKeys = new Set<string>();
|
|
3009
|
+
for (const rf of remoteFiles) {
|
|
3010
|
+
remoteKeySet.add(rf.key);
|
|
3011
|
+
if (rf.key.startsWith(SKILLS_KEY_PREFIX)) {
|
|
3012
|
+
remoteCanonicalSkillKeys.add(canonicalVaultKeySpelling(rf.key));
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
2734
3015
|
const tombstones: PullPlan["tombstones"] = [];
|
|
2735
3016
|
for (const key of Object.keys(journal.files)) {
|
|
2736
3017
|
// Compare membership in POSIX space. A pre-5.47.2 Windows journal key can
|
|
@@ -2740,6 +3021,18 @@ function computePullPlan(
|
|
|
2740
3021
|
// POSIX compare is defense-in-depth (ridge data-loss, feedback_b8d09d0f).
|
|
2741
3022
|
const posixKey = toPosixKey(key);
|
|
2742
3023
|
if (remoteKeySet.has(posixKey)) continue;
|
|
3024
|
+
if (
|
|
3025
|
+
posixKey.startsWith(SKILLS_KEY_PREFIX) &&
|
|
3026
|
+
remoteCanonicalSkillKeys.has(canonicalVaultKeySpelling(posixKey)) &&
|
|
3027
|
+
// An EXACT FILE_TOMBSTONE for the journaled key is an authoritative
|
|
3028
|
+
// delete and wins over the twin protection — a lingering junk
|
|
3029
|
+
// spelling in the LIST must not keep a deliberately-deleted wrapper
|
|
3030
|
+
// alive forever. (The key is absent from the LIST here, so there is
|
|
3031
|
+
// no newer re-create to defer to.)
|
|
3032
|
+
fileTombstones.get(posixKey) === undefined
|
|
3033
|
+
) {
|
|
3034
|
+
continue;
|
|
3035
|
+
}
|
|
2743
3036
|
const localPath = resolveContainedVaultPath(companyRoot, key);
|
|
2744
3037
|
if (localPath === null) continue;
|
|
2745
3038
|
// PersonalMode key gating — mirror the download branch. Local (non-cloud)
|
|
@@ -2896,6 +3189,16 @@ function defaultConsoleLogger(event: SyncProgressEvent): void {
|
|
|
2896
3189
|
if (event.count > event.samplePaths.length) {
|
|
2897
3190
|
console.log(` ... and ${event.count - event.samplePaths.length} more`);
|
|
2898
3191
|
}
|
|
3192
|
+
} else if (event.type === "scope-materialization-gap") {
|
|
3193
|
+
console.warn(
|
|
3194
|
+
` ! ${event.count} item${event.count === 1 ? "" : "s"} in this vault did NOT sync to this device — your access level is "${event.syncMode}", so only shared paths materialize. To pull everything you have access to, set this membership's access level to "all":`,
|
|
3195
|
+
);
|
|
3196
|
+
for (const p of event.samplePaths) {
|
|
3197
|
+
console.warn(` · ${p}`);
|
|
3198
|
+
}
|
|
3199
|
+
if (event.count > event.samplePaths.length) {
|
|
3200
|
+
console.warn(` ... and ${event.count - event.samplePaths.length} more`);
|
|
3201
|
+
}
|
|
2899
3202
|
} else if (event.type === "ignore-excluded") {
|
|
2900
3203
|
console.warn(
|
|
2901
3204
|
` ! ${event.count} path${event.count === 1 ? "" : "s"} were EXCLUDED from sync by an ignore rule and did NOT reach the vault (review in case this is unintended):`,
|
|
@@ -2915,5 +3218,9 @@ function defaultConsoleLogger(event: SyncProgressEvent): void {
|
|
|
2915
3218
|
console.warn(
|
|
2916
3219
|
` ! ${event.path} skipped — invalid vault key (${event.errorCode ?? "INVALID_KEY_COMPANIES_SCOPED"})`,
|
|
2917
3220
|
);
|
|
3221
|
+
} else if (event.type === "skip-junk-key-spelling") {
|
|
3222
|
+
console.warn(
|
|
3223
|
+
` ! ${event.path} skipped — junk key spelling; this logical path is already synced as ${event.journaledKey} (mixed-version peer artifact; the vault doctor collapses the family)`,
|
|
3224
|
+
);
|
|
2918
3225
|
}
|
|
2919
3226
|
}
|
package/src/cognito-auth.test.ts
CHANGED
|
@@ -14,18 +14,23 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
14
14
|
// Sandbox HOME *before* importing the module — it reads os.homedir() at load
|
|
15
15
|
// time to compute the cache file path.
|
|
16
16
|
let originalHome: string | undefined;
|
|
17
|
+
let originalUserProfile: string | undefined;
|
|
17
18
|
let tmpHome: string;
|
|
18
19
|
|
|
19
20
|
beforeEach(() => {
|
|
20
21
|
originalHome = process.env.HOME;
|
|
22
|
+
originalUserProfile = process.env.USERPROFILE;
|
|
21
23
|
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "hq-cognito-auth-test-"));
|
|
22
24
|
process.env.HOME = tmpHome;
|
|
25
|
+
process.env.USERPROFILE = tmpHome;
|
|
23
26
|
vi.resetModules();
|
|
24
27
|
});
|
|
25
28
|
|
|
26
29
|
afterEach(() => {
|
|
27
30
|
if (originalHome === undefined) delete process.env.HOME;
|
|
28
31
|
else process.env.HOME = originalHome;
|
|
32
|
+
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
|
33
|
+
else process.env.USERPROFILE = originalUserProfile;
|
|
29
34
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
30
35
|
vi.unstubAllGlobals();
|
|
31
36
|
vi.restoreAllMocks();
|
package/src/index.ts
CHANGED
|
@@ -300,6 +300,16 @@ export type { ReindexOptions, ReindexResult } from "./cli/index.js";
|
|
|
300
300
|
export { rescue, buildRescueArgs } from "./cli/index.js";
|
|
301
301
|
export type { RescueOptions, RescueResult } from "./cli/index.js";
|
|
302
302
|
|
|
303
|
+
// `hq sync doctor` — skill-key dedupe + local farm GC (US-003). Consumed by
|
|
304
|
+
// @indigoai-us/hq-cli's `sync doctor` subcommand.
|
|
305
|
+
export { syncDoctor, SKILLS_KEY_PREFIX } from "./cli/index.js";
|
|
306
|
+
export type {
|
|
307
|
+
SyncDoctorOptions,
|
|
308
|
+
SyncDoctorResult,
|
|
309
|
+
SyncDoctorPlan,
|
|
310
|
+
DoctorStore,
|
|
311
|
+
} from "./cli/index.js";
|
|
312
|
+
|
|
303
313
|
export type {
|
|
304
314
|
EntityContext,
|
|
305
315
|
VaultCredentials,
|
package/src/lib/conflict-file.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
import * as fs from "fs";
|
|
19
19
|
import * as path from "path";
|
|
20
20
|
|
|
21
|
+
import { localPathForVaultKey } from "../local-path-codec.js";
|
|
22
|
+
|
|
21
23
|
export { readShortMachineId, getOrCreateMachineId } from "./machine-id.js";
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -58,7 +60,10 @@ export function writeConflictFile(
|
|
|
58
60
|
conflictRelative: string,
|
|
59
61
|
contents: Buffer,
|
|
60
62
|
): void {
|
|
61
|
-
|
|
63
|
+
// Codec boundary: conflict paths are canonical (hq-root-relative,
|
|
64
|
+
// forward-slash) keys, so the mirror must land at the encoded local name
|
|
65
|
+
// on win32 — a raw join of a colon key can never be created there.
|
|
66
|
+
const abs = localPathForVaultKey(hqRoot, conflictRelative);
|
|
62
67
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
63
68
|
fs.writeFileSync(abs, contents);
|
|
64
69
|
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import * as crypto from "crypto";
|
|
23
23
|
import * as fs from "fs";
|
|
24
24
|
import * as path from "path";
|
|
25
|
+
import { localPathForVaultKey } from "../local-path-codec.js";
|
|
25
26
|
import type { ConflictIndex, ConflictIndexEntry } from "../types.js";
|
|
26
27
|
|
|
27
28
|
const CONFLICTS_DIR = ".hq-conflicts";
|
|
@@ -136,6 +137,13 @@ export interface PruneConflictIndexResult {
|
|
|
136
137
|
removedMirrors: number;
|
|
137
138
|
/** Rows kept (genuine divergence, or unprovable — fail-safe retained). */
|
|
138
139
|
kept: number;
|
|
140
|
+
/**
|
|
141
|
+
* Up to 10 ORIGINAL (non-mirror) paths of the kept rows, for a
|
|
142
|
+
* post-sync reconcile surface (`conflicts-remaining`). Empty when
|
|
143
|
+
* `kept === 0`. Lets a caller name the preserved conflict variants a human
|
|
144
|
+
* still has to resolve without re-reading the index. feedback_d2082110.
|
|
145
|
+
*/
|
|
146
|
+
keptSamplePaths: string[];
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
/**
|
|
@@ -198,6 +206,7 @@ export function pruneConflictIndex(hqRoot: string): PruneConflictIndexResult {
|
|
|
198
206
|
prunedIdentical: 0,
|
|
199
207
|
removedMirrors: 0,
|
|
200
208
|
kept: 0,
|
|
209
|
+
keptSamplePaths: [],
|
|
201
210
|
};
|
|
202
211
|
|
|
203
212
|
const index = readConflictIndex(hqRoot);
|
|
@@ -207,8 +216,12 @@ export function pruneConflictIndex(hqRoot: string): PruneConflictIndexResult {
|
|
|
207
216
|
const mirrorsToRemove: string[] = [];
|
|
208
217
|
|
|
209
218
|
for (const entry of index.conflicts) {
|
|
210
|
-
|
|
211
|
-
|
|
219
|
+
// Codec boundary: index rows store canonical (hq-root-relative) keys, so
|
|
220
|
+
// both probes must resolve through the codec — a raw join of a win32
|
|
221
|
+
// colon key always lstat-fails, which would mis-classify a live encoded
|
|
222
|
+
// mirror as an orphaned row and silently drop it.
|
|
223
|
+
const mirrorAbs = localPathForVaultKey(hqRoot, entry.conflictPath);
|
|
224
|
+
const originalAbs = localPathForVaultKey(hqRoot, entry.originalPath);
|
|
212
225
|
|
|
213
226
|
let mirrorStat: fs.Stats;
|
|
214
227
|
try {
|
|
@@ -254,6 +267,9 @@ export function pruneConflictIndex(hqRoot: string): PruneConflictIndexResult {
|
|
|
254
267
|
}
|
|
255
268
|
}
|
|
256
269
|
|
|
270
|
+
// Name the preserved divergences for the post-sync reconcile surface.
|
|
271
|
+
result.keptSamplePaths = kept.slice(0, 10).map((e) => e.originalPath);
|
|
272
|
+
|
|
257
273
|
// No row dropped → leave the file (and its mtime) untouched.
|
|
258
274
|
if (kept.length === index.conflicts.length) return result;
|
|
259
275
|
|
package/src/lib/conflict.test.ts
CHANGED
|
@@ -194,6 +194,7 @@ describe("pruneConflictIndex", () => {
|
|
|
194
194
|
prunedIdentical: 0,
|
|
195
195
|
removedMirrors: 0,
|
|
196
196
|
kept: 0,
|
|
197
|
+
keptSamplePaths: [],
|
|
197
198
|
});
|
|
198
199
|
expect(fs.existsSync(getConflictIndexPath(tmpHq))).toBe(false);
|
|
199
200
|
});
|
|
@@ -292,6 +293,7 @@ describe("pruneConflictIndex", () => {
|
|
|
292
293
|
prunedIdentical: 1,
|
|
293
294
|
removedMirrors: 1,
|
|
294
295
|
kept: 1,
|
|
296
|
+
keptSamplePaths: ["dir/real.md"],
|
|
295
297
|
});
|
|
296
298
|
expect(readConflictIndex(tmpHq).conflicts.map((c) => c.id)).toEqual(["real"]);
|
|
297
299
|
});
|
|
@@ -312,4 +314,35 @@ describe("pruneConflictIndex", () => {
|
|
|
312
314
|
pruneConflictIndex(tmpHq);
|
|
313
315
|
expect(fs.statSync(indexPath).mtimeMs).toBe(before);
|
|
314
316
|
});
|
|
317
|
+
|
|
318
|
+
it("names the preserved conflict variants (keptSamplePaths, capped at 10) for the reconcile surface (feedback_d2082110)", () => {
|
|
319
|
+
// Reporter feedback_d2082110 finished a full sync with "20 older preserved
|
|
320
|
+
// conflict entries" still on disk and NO signal they were there. The prune
|
|
321
|
+
// self-heals the ledger but conservatively KEEPS genuine divergences; those
|
|
322
|
+
// were silent. keptSamplePaths surfaces the original (non-mirror) paths so
|
|
323
|
+
// the runner can emit a `conflicts-remaining` reconcile line pointing the
|
|
324
|
+
// operator at `/resolve-conflicts`.
|
|
325
|
+
const rows: ConflictIndexEntry[] = [];
|
|
326
|
+
for (let i = 0; i < 12; i++) {
|
|
327
|
+
const id = `real-${String(i).padStart(2, "0")}`;
|
|
328
|
+
const row = rowFor(id);
|
|
329
|
+
put(row.originalPath, `local-${i}`);
|
|
330
|
+
put(row.conflictPath, `remote-${i}`);
|
|
331
|
+
rows.push(row);
|
|
332
|
+
}
|
|
333
|
+
writeConflictIndex(tmpHq, { version: 1, conflicts: rows });
|
|
334
|
+
|
|
335
|
+
const res = pruneConflictIndex(tmpHq);
|
|
336
|
+
expect(res.kept).toBe(12);
|
|
337
|
+
// Capped at 10 samples, drawn from the kept (detectedAt-sorted) rows.
|
|
338
|
+
expect(res.keptSamplePaths).toHaveLength(10);
|
|
339
|
+
expect(res.keptSamplePaths.every((p) => p.startsWith("dir/real-"))).toBe(true);
|
|
340
|
+
// Orphaned/identical rows never contribute to the surfaced sample.
|
|
341
|
+
const orphan = rowFor("orphan");
|
|
342
|
+
put(orphan.originalPath, "x"); // mirror missing → dropped, not sampled
|
|
343
|
+
writeConflictIndex(tmpHq, { version: 1, conflicts: [orphan] });
|
|
344
|
+
const res2 = pruneConflictIndex(tmpHq);
|
|
345
|
+
expect(res2.kept).toBe(0);
|
|
346
|
+
expect(res2.keptSamplePaths).toEqual([]);
|
|
347
|
+
});
|
|
315
348
|
});
|
|
@@ -17,22 +17,27 @@ function freshTmp(prefix: string): string {
|
|
|
17
17
|
|
|
18
18
|
describe("getOrCreateMachineId (four-tier resolver)", () => {
|
|
19
19
|
let originalHome: string | undefined;
|
|
20
|
+
let originalUserProfile: string | undefined;
|
|
20
21
|
let originalEnvId: string | undefined;
|
|
21
22
|
let tmpHome: string;
|
|
22
23
|
let tmpHqRoot: string;
|
|
23
24
|
|
|
24
25
|
beforeEach(() => {
|
|
25
26
|
originalHome = process.env.HOME;
|
|
27
|
+
originalUserProfile = process.env.USERPROFILE;
|
|
26
28
|
originalEnvId = process.env.HQ_MACHINE_ID;
|
|
27
29
|
delete process.env.HQ_MACHINE_ID;
|
|
28
30
|
tmpHome = freshTmp("hq-machineid-home-");
|
|
29
31
|
tmpHqRoot = freshTmp("hq-machineid-root-");
|
|
30
32
|
process.env.HOME = tmpHome;
|
|
33
|
+
process.env.USERPROFILE = tmpHome;
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
afterEach(() => {
|
|
34
37
|
if (originalHome) process.env.HOME = originalHome;
|
|
35
38
|
else delete process.env.HOME;
|
|
39
|
+
if (originalUserProfile) process.env.USERPROFILE = originalUserProfile;
|
|
40
|
+
else delete process.env.USERPROFILE;
|
|
36
41
|
if (originalEnvId !== undefined) process.env.HQ_MACHINE_ID = originalEnvId;
|
|
37
42
|
else delete process.env.HQ_MACHINE_ID;
|
|
38
43
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
@@ -135,22 +140,27 @@ describe("getOrCreateMachineId (four-tier resolver)", () => {
|
|
|
135
140
|
|
|
136
141
|
describe("readShortMachineId", () => {
|
|
137
142
|
let originalHome: string | undefined;
|
|
143
|
+
let originalUserProfile: string | undefined;
|
|
138
144
|
let originalEnvId: string | undefined;
|
|
139
145
|
let tmpHome: string;
|
|
140
146
|
let tmpHqRoot: string;
|
|
141
147
|
|
|
142
148
|
beforeEach(() => {
|
|
143
149
|
originalHome = process.env.HOME;
|
|
150
|
+
originalUserProfile = process.env.USERPROFILE;
|
|
144
151
|
originalEnvId = process.env.HQ_MACHINE_ID;
|
|
145
152
|
delete process.env.HQ_MACHINE_ID;
|
|
146
153
|
tmpHome = freshTmp("hq-machineid-short-home-");
|
|
147
154
|
tmpHqRoot = freshTmp("hq-machineid-short-root-");
|
|
148
155
|
process.env.HOME = tmpHome;
|
|
156
|
+
process.env.USERPROFILE = tmpHome;
|
|
149
157
|
});
|
|
150
158
|
|
|
151
159
|
afterEach(() => {
|
|
152
160
|
if (originalHome) process.env.HOME = originalHome;
|
|
153
161
|
else delete process.env.HOME;
|
|
162
|
+
if (originalUserProfile) process.env.USERPROFILE = originalUserProfile;
|
|
163
|
+
else delete process.env.USERPROFILE;
|
|
154
164
|
if (originalEnvId !== undefined) process.env.HQ_MACHINE_ID = originalEnvId;
|
|
155
165
|
else delete process.env.HQ_MACHINE_ID;
|
|
156
166
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|