@indigoai-us/hq-cloud 6.3.6 → 6.5.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/cli/rescue-classify-ordering.test.d.ts +2 -0
- package/dist/cli/rescue-classify-ordering.test.d.ts.map +1 -0
- package/dist/cli/rescue-classify-ordering.test.js +241 -0
- package/dist/cli/rescue-classify-ordering.test.js.map +1 -0
- package/dist/cli/rescue-core.js +68 -22
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/journal.d.ts +31 -0
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +47 -0
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +102 -1
- package/dist/journal.test.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/rescue-classify-ordering.test.ts +263 -0
- package/src/cli/rescue-core.ts +82 -26
- package/src/index.ts +7 -0
- package/src/journal.test.ts +120 -0
- package/src/journal.ts +66 -0
package/src/journal.test.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
TOMBSTONE_TTL_MS,
|
|
28
28
|
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
29
29
|
migratePersonalVaultJournal,
|
|
30
|
+
listJournals,
|
|
30
31
|
} from "./journal.js";
|
|
31
32
|
import type { SyncJournal, PullRecord } from "./types.js";
|
|
32
33
|
|
|
@@ -643,4 +644,123 @@ describe("journal", () => {
|
|
|
643
644
|
expect(fs.existsSync(getJournalPath(PERSONAL_VAULT_JOURNAL_SLUG))).toBe(false);
|
|
644
645
|
});
|
|
645
646
|
});
|
|
647
|
+
|
|
648
|
+
// Regression: `hq sync status` reported "No sync journal yet" right after a
|
|
649
|
+
// successful sync because it read one path keyed off the HQ-ROOT, while the
|
|
650
|
+
// engine writes slug-sharded journals (personal vault + per-company).
|
|
651
|
+
// `listJournals` is the all-shard enumeration the status surface now uses.
|
|
652
|
+
describe("listJournals", () => {
|
|
653
|
+
function seed(slug: string, files: SyncJournal["files"]): void {
|
|
654
|
+
writeJournal(slug, {
|
|
655
|
+
version: "2",
|
|
656
|
+
lastSync: "",
|
|
657
|
+
files,
|
|
658
|
+
pulls: [],
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
it("returns [] when the state dir has no journals (truly-empty case)", () => {
|
|
663
|
+
expect(listJournals()).toEqual([]);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("returns [] when the state dir does not exist at all", () => {
|
|
667
|
+
process.env.HQ_STATE_DIR = path.join(stateDir, "does-not-exist");
|
|
668
|
+
expect(listJournals()).toEqual([]);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it("enumerates the personal-vault shard written by a `--personal` sync", () => {
|
|
672
|
+
seed(PERSONAL_VAULT_JOURNAL_SLUG, {
|
|
673
|
+
".claude/skills/x/SKILL.md": {
|
|
674
|
+
hash: "a",
|
|
675
|
+
size: 10,
|
|
676
|
+
syncedAt: "2026-06-08T10:00:00.000Z",
|
|
677
|
+
direction: "up",
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
const all = listJournals();
|
|
682
|
+
expect(all).toHaveLength(1);
|
|
683
|
+
expect(all[0]!.slug).toBe(PERSONAL_VAULT_JOURNAL_SLUG);
|
|
684
|
+
expect(all[0]!.path).toBe(getJournalPath(PERSONAL_VAULT_JOURNAL_SLUG));
|
|
685
|
+
expect(Object.keys(all[0]!.journal.files)).toContain(
|
|
686
|
+
".claude/skills/x/SKILL.md",
|
|
687
|
+
);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("enumerates a per-company shard written by a company sync", () => {
|
|
691
|
+
seed("marshalops", {
|
|
692
|
+
"knowledge/a.md": {
|
|
693
|
+
hash: "b",
|
|
694
|
+
size: 20,
|
|
695
|
+
syncedAt: "2026-06-08T11:00:00.000Z",
|
|
696
|
+
direction: "down",
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
const all = listJournals();
|
|
701
|
+
expect(all.map((j) => j.slug)).toEqual(["marshalops"]);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("enumerates ALL shards together, sorted by slug", () => {
|
|
705
|
+
seed(PERSONAL_VAULT_JOURNAL_SLUG, {
|
|
706
|
+
".claude/x": {
|
|
707
|
+
hash: "a",
|
|
708
|
+
size: 1,
|
|
709
|
+
syncedAt: "2026-06-08T10:00:00.000Z",
|
|
710
|
+
direction: "up",
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
seed("marshalops", {
|
|
714
|
+
"k/a": {
|
|
715
|
+
hash: "b",
|
|
716
|
+
size: 2,
|
|
717
|
+
syncedAt: "2026-06-08T11:00:00.000Z",
|
|
718
|
+
direction: "down",
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
seed("glazeymedia", {
|
|
722
|
+
"k/b": {
|
|
723
|
+
hash: "c",
|
|
724
|
+
size: 3,
|
|
725
|
+
syncedAt: "2026-06-08T12:00:00.000Z",
|
|
726
|
+
direction: "down",
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const slugs = listJournals().map((j) => j.slug);
|
|
731
|
+
// Sorted: "__hq_personal_vault__" < "glazeymedia" < "marshalops".
|
|
732
|
+
expect(slugs).toEqual([
|
|
733
|
+
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
734
|
+
"glazeymedia",
|
|
735
|
+
"marshalops",
|
|
736
|
+
]);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it("skips a corrupt shard but still returns the healthy ones", () => {
|
|
740
|
+
seed("marshalops", {
|
|
741
|
+
"k/a": {
|
|
742
|
+
hash: "b",
|
|
743
|
+
size: 2,
|
|
744
|
+
syncedAt: "2026-06-08T11:00:00.000Z",
|
|
745
|
+
direction: "down",
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
fs.writeFileSync(
|
|
749
|
+
path.join(stateDir, "sync-journal.broken.json"),
|
|
750
|
+
"{ not valid json",
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
const all = listJournals();
|
|
754
|
+
expect(all.map((j) => j.slug)).toEqual(["marshalops"]);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it("ignores unrelated files in the state dir", () => {
|
|
758
|
+
seed("marshalops", {});
|
|
759
|
+
fs.writeFileSync(path.join(stateDir, "config.json"), "{}");
|
|
760
|
+
fs.writeFileSync(path.join(stateDir, "sync-journal.json"), "{}"); // no slug
|
|
761
|
+
fs.writeFileSync(path.join(stateDir, "notes.txt"), "hi");
|
|
762
|
+
|
|
763
|
+
expect(listJournals().map((j) => j.slug)).toEqual(["marshalops"]);
|
|
764
|
+
});
|
|
765
|
+
});
|
|
646
766
|
});
|
package/src/journal.ts
CHANGED
|
@@ -135,6 +135,72 @@ export function readJournal(slug: string): SyncJournal {
|
|
|
135
135
|
return { version: JOURNAL_VERSION_CURRENT, lastSync: "", files: {}, pulls: [] };
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/** One enumerated journal shard: its recovered slug, on-disk path, contents. */
|
|
139
|
+
export interface JournalSummary {
|
|
140
|
+
/**
|
|
141
|
+
* Slug recovered from the `sync-journal.<slug>.json` filename — the
|
|
142
|
+
* sanitized form the engine wrote (e.g. a company slug,
|
|
143
|
+
* `PERSONAL_VAULT_JOURNAL_SLUG`, or the legacy `"personal"`).
|
|
144
|
+
*/
|
|
145
|
+
slug: string;
|
|
146
|
+
/** Absolute path to the journal file. */
|
|
147
|
+
path: string;
|
|
148
|
+
/** Parsed journal contents. */
|
|
149
|
+
journal: SyncJournal;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Enumerate every sync journal present in the state dir.
|
|
154
|
+
*
|
|
155
|
+
* The engine SHARDS journals by slug (ADR-0001 Phase 5): the personal-vault
|
|
156
|
+
* fanout slot under `PERSONAL_VAULT_JOURNAL_SLUG`, one shard per cloud company,
|
|
157
|
+
* and the legacy `"personal"` shard. A caller that reads a single fixed path
|
|
158
|
+
* therefore only ever sees one scope — and a caller that mistakes a non-slug
|
|
159
|
+
* value for a slug (e.g. `hq sync status` passing the HQ-root PATH, which
|
|
160
|
+
* `sanitizeSlug` mangles into `_Users_<user>_hq`) sees a slug the engine never
|
|
161
|
+
* writes, and reports "no journal" right after a successful sync. Any surface
|
|
162
|
+
* that wants the COMPLETE local sync picture must read ALL shards via this
|
|
163
|
+
* helper rather than reconstructing a path.
|
|
164
|
+
*
|
|
165
|
+
* Slugs are recovered from each filename. A shard that fails to read or parse
|
|
166
|
+
* is skipped rather than thrown — one corrupt shard must not blind the caller
|
|
167
|
+
* to the healthy ones. Results are sorted by slug for deterministic output.
|
|
168
|
+
*/
|
|
169
|
+
export function listJournals(): JournalSummary[] {
|
|
170
|
+
const dir = getStateDir();
|
|
171
|
+
let names: string[];
|
|
172
|
+
try {
|
|
173
|
+
names = fs.readdirSync(dir);
|
|
174
|
+
} catch {
|
|
175
|
+
return []; // state dir absent → no journals yet
|
|
176
|
+
}
|
|
177
|
+
const out: JournalSummary[] = [];
|
|
178
|
+
for (const name of names) {
|
|
179
|
+
if (
|
|
180
|
+
!name.startsWith(JOURNAL_FILE_PREFIX) ||
|
|
181
|
+
!name.endsWith(JOURNAL_FILE_SUFFIX)
|
|
182
|
+
) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const slug = name.slice(
|
|
186
|
+
JOURNAL_FILE_PREFIX.length,
|
|
187
|
+
name.length - JOURNAL_FILE_SUFFIX.length,
|
|
188
|
+
);
|
|
189
|
+
if (!slug) continue; // guard against a stray "sync-journal..json"
|
|
190
|
+
const filePath = path.join(dir, name);
|
|
191
|
+
try {
|
|
192
|
+
const journal = JSON.parse(
|
|
193
|
+
fs.readFileSync(filePath, "utf-8"),
|
|
194
|
+
) as SyncJournal;
|
|
195
|
+
out.push({ slug, path: filePath, journal });
|
|
196
|
+
} catch {
|
|
197
|
+
// Corrupt/unreadable shard — skip; don't blind the caller to the rest.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
out.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
138
204
|
/**
|
|
139
205
|
* Defuse the pre-5.47.2 Windows backslash-key landmine in a journal's `files`
|
|
140
206
|
* map. Such clients stamped keys with the OS path separator ("\\"), e.g.
|