@indigoai-us/hq-cloud 5.11.3 → 5.12.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/bin/sync-runner.d.ts +11 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +65 -4
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +118 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts +17 -0
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +11 -3
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +104 -0
- package/dist/cli/share.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner.test.ts +152 -0
- package/src/bin/sync-runner.ts +66 -4
- package/src/cli/share.test.ts +122 -0
- package/src/cli/share.ts +28 -3
package/src/cli/share.test.ts
CHANGED
|
@@ -1082,4 +1082,126 @@ describe("share", () => {
|
|
|
1082
1082
|
const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
|
|
1083
1083
|
expect(journal.files["flaky.md"]).toBeDefined();
|
|
1084
1084
|
});
|
|
1085
|
+
|
|
1086
|
+
// ── personalMode ───────────────────────────────────────────────────────────
|
|
1087
|
+
//
|
|
1088
|
+
// The personal vault (slug "personal" in the runner's fanout plan) shares
|
|
1089
|
+
// files from hqRoot DIRECTLY — not from hqRoot/companies/<slug>/. Mirrors
|
|
1090
|
+
// the Rust hq-sync first-push contract in src-tauri/src/commands/personal.rs:
|
|
1091
|
+
// syncRoot = hqRoot, journal slug = "personal", remote keys are hq-root-
|
|
1092
|
+
// relative (e.g. ".claude/skills/foo.md", "knowledge/notes.md"). The
|
|
1093
|
+
// exclusion list itself is enforced by the runner (sync-runner.ts) by only
|
|
1094
|
+
// passing in the allowed top-level directories — share() trusts its
|
|
1095
|
+
// `paths` input.
|
|
1096
|
+
describe("personalMode", () => {
|
|
1097
|
+
it("personalMode=true keys files hq-root-relative, not companies/{slug}/-relative", async () => {
|
|
1098
|
+
fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
|
|
1099
|
+
fs.mkdirSync(path.join(tmpDir, "knowledge"), { recursive: true });
|
|
1100
|
+
fs.writeFileSync(path.join(tmpDir, ".claude", "skills", "foo.md"), "skill");
|
|
1101
|
+
fs.writeFileSync(path.join(tmpDir, "knowledge", "notes.md"), "note");
|
|
1102
|
+
|
|
1103
|
+
const result = await share({
|
|
1104
|
+
paths: [
|
|
1105
|
+
path.join(tmpDir, ".claude"),
|
|
1106
|
+
path.join(tmpDir, "knowledge"),
|
|
1107
|
+
],
|
|
1108
|
+
company: "acme",
|
|
1109
|
+
vaultConfig: mockConfig,
|
|
1110
|
+
hqRoot: tmpDir,
|
|
1111
|
+
personalMode: true,
|
|
1112
|
+
journalSlug: "personal",
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
expect(result.filesUploaded).toBe(2);
|
|
1116
|
+
// Remote keys must be hq-root-relative, NOT prefixed with companies/personal/
|
|
1117
|
+
const keys = vi.mocked(uploadFile).mock.calls.map((c) => c[2]);
|
|
1118
|
+
expect(keys.sort()).toEqual([".claude/skills/foo.md", "knowledge/notes.md"]);
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
it("personalMode=true writes journal under the personal journalSlug", async () => {
|
|
1122
|
+
fs.mkdirSync(path.join(tmpDir, "knowledge"), { recursive: true });
|
|
1123
|
+
fs.writeFileSync(path.join(tmpDir, "knowledge", "notes.md"), "note");
|
|
1124
|
+
|
|
1125
|
+
await share({
|
|
1126
|
+
paths: [path.join(tmpDir, "knowledge")],
|
|
1127
|
+
company: "acme",
|
|
1128
|
+
vaultConfig: mockConfig,
|
|
1129
|
+
hqRoot: tmpDir,
|
|
1130
|
+
personalMode: true,
|
|
1131
|
+
journalSlug: "personal",
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
// Personal journal is keyed "personal", NOT the company's ctx.slug ("acme")
|
|
1135
|
+
const personalJournalPath = path.join(stateDir, "sync-journal.personal.json");
|
|
1136
|
+
const acmeJournalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
1137
|
+
expect(fs.existsSync(personalJournalPath)).toBe(true);
|
|
1138
|
+
expect(fs.existsSync(acmeJournalPath)).toBe(false);
|
|
1139
|
+
|
|
1140
|
+
const journal = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
|
|
1141
|
+
expect(journal.files["knowledge/notes.md"]).toBeDefined();
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
it("personalMode=true accepts files outside companies/<slug>/ (companion to the company-folder rejection)", async () => {
|
|
1145
|
+
// Same fixture as the "skips files outside the company folder" test
|
|
1146
|
+
// above — file at hqRoot root, NOT under companies/acme/. Without
|
|
1147
|
+
// personalMode this is rejected with a "outside company folder" warning;
|
|
1148
|
+
// with personalMode=true the file IS uploaded because syncRoot=hqRoot.
|
|
1149
|
+
const outsideFile = path.join(tmpDir, "stray.md");
|
|
1150
|
+
fs.writeFileSync(outsideFile, "stray");
|
|
1151
|
+
|
|
1152
|
+
const result = await share({
|
|
1153
|
+
paths: [outsideFile],
|
|
1154
|
+
company: "acme",
|
|
1155
|
+
vaultConfig: mockConfig,
|
|
1156
|
+
hqRoot: tmpDir,
|
|
1157
|
+
personalMode: true,
|
|
1158
|
+
journalSlug: "personal",
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
expect(result.filesUploaded).toBe(1);
|
|
1162
|
+
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), outsideFile, "stray.md");
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
it("personalMode=true + skipUnchanged honors the personal-journal hash", async () => {
|
|
1166
|
+
fs.mkdirSync(path.join(tmpDir, "knowledge"), { recursive: true });
|
|
1167
|
+
const testFile = path.join(tmpDir, "knowledge", "stable.md");
|
|
1168
|
+
fs.writeFileSync(testFile, "stable content");
|
|
1169
|
+
|
|
1170
|
+
const { hashFile } = await import("../journal.js");
|
|
1171
|
+
const hash = hashFile(testFile);
|
|
1172
|
+
|
|
1173
|
+
// Pre-seed the PERSONAL journal (not the per-company one) so the
|
|
1174
|
+
// skipUnchanged short-circuit fires for the right slug.
|
|
1175
|
+
const personalJournalPath = path.join(stateDir, "sync-journal.personal.json");
|
|
1176
|
+
fs.writeFileSync(
|
|
1177
|
+
personalJournalPath,
|
|
1178
|
+
JSON.stringify({
|
|
1179
|
+
version: "1",
|
|
1180
|
+
lastSync: new Date().toISOString(),
|
|
1181
|
+
files: {
|
|
1182
|
+
"knowledge/stable.md": {
|
|
1183
|
+
hash,
|
|
1184
|
+
size: 15,
|
|
1185
|
+
syncedAt: new Date().toISOString(),
|
|
1186
|
+
direction: "up",
|
|
1187
|
+
},
|
|
1188
|
+
},
|
|
1189
|
+
}),
|
|
1190
|
+
);
|
|
1191
|
+
|
|
1192
|
+
const result = await share({
|
|
1193
|
+
paths: [path.join(tmpDir, "knowledge")],
|
|
1194
|
+
company: "acme",
|
|
1195
|
+
vaultConfig: mockConfig,
|
|
1196
|
+
hqRoot: tmpDir,
|
|
1197
|
+
personalMode: true,
|
|
1198
|
+
journalSlug: "personal",
|
|
1199
|
+
skipUnchanged: true,
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
expect(result.filesUploaded).toBe(0);
|
|
1203
|
+
expect(result.filesSkipped).toBe(1);
|
|
1204
|
+
expect(uploadFile).not.toHaveBeenCalled();
|
|
1205
|
+
});
|
|
1206
|
+
});
|
|
1085
1207
|
});
|
package/src/cli/share.ts
CHANGED
|
@@ -193,6 +193,23 @@ export interface ShareOptions {
|
|
|
193
193
|
* this engine. The runner pipes Cognito idToken claims through here.
|
|
194
194
|
*/
|
|
195
195
|
author?: UploadAuthor;
|
|
196
|
+
/**
|
|
197
|
+
* When true, share() targets the caller's person-entity bucket: syncRoot
|
|
198
|
+
* is `hqRoot` itself (NOT `hqRoot/companies/<slug>/`), so remote keys are
|
|
199
|
+
* hq-root-relative (e.g. ".claude/skills/foo.md", "knowledge/notes.md") to
|
|
200
|
+
* match the Rust hq-sync first-push contract in
|
|
201
|
+
* `src-tauri/src/commands/personal.rs`. The exclusion of top-level dirs
|
|
202
|
+
* (.git, companies, core, data, personal, repos, workspace) is enforced
|
|
203
|
+
* by the runner — share() trusts its `paths` input.
|
|
204
|
+
*/
|
|
205
|
+
personalMode?: boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Override for the per-slug journal file name. Defaults to `ctx.slug`. The
|
|
208
|
+
* runner passes `journalSlug: "personal"` for the personal slot so the TS
|
|
209
|
+
* push and the Rust personal first-push share idempotency state under one
|
|
210
|
+
* `sync-journal.personal.json` file.
|
|
211
|
+
*/
|
|
212
|
+
journalSlug?: string;
|
|
196
213
|
}
|
|
197
214
|
|
|
198
215
|
export interface ShareResult {
|
|
@@ -263,9 +280,17 @@ export async function share(options: ShareOptions): Promise<ShareResult> {
|
|
|
263
280
|
// Remote keys are company-relative; the on-disk scoping prefix is
|
|
264
281
|
// companies/{slug}/. Anything outside this folder gets skipped to avoid
|
|
265
282
|
// leaking cross-company state into the vault.
|
|
266
|
-
|
|
283
|
+
//
|
|
284
|
+
// In personalMode the syncRoot is `hqRoot` itself — remote keys are
|
|
285
|
+
// hq-root-relative to match the Rust personal first-push (which uploads
|
|
286
|
+
// every non-excluded top-level dir under ~/HQ). The exclusion list is
|
|
287
|
+
// enforced upstream by the runner; share() just trusts `paths`.
|
|
288
|
+
const syncRoot = options.personalMode === true
|
|
289
|
+
? hqRoot
|
|
290
|
+
: path.join(hqRoot, "companies", ctx.slug);
|
|
267
291
|
const shouldSync = createIgnoreFilter(hqRoot);
|
|
268
|
-
const
|
|
292
|
+
const journalSlug = options.journalSlug ?? ctx.slug;
|
|
293
|
+
const journal = readJournal(journalSlug);
|
|
269
294
|
|
|
270
295
|
let filesUploaded = 0;
|
|
271
296
|
let bytesUploaded = 0;
|
|
@@ -457,7 +482,7 @@ export async function share(options: ShareOptions): Promise<ShareResult> {
|
|
|
457
482
|
// See cli/sync.ts: stamp lastSync on completion so a no-op share still
|
|
458
483
|
// ticks the "Last sync" indicator.
|
|
459
484
|
journal.lastSync = new Date().toISOString();
|
|
460
|
-
writeJournal(
|
|
485
|
+
writeJournal(journalSlug, journal);
|
|
461
486
|
|
|
462
487
|
return {
|
|
463
488
|
filesUploaded,
|