@indigoai-us/hq-cloud 5.15.0 → 5.17.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 +2 -12
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +6 -57
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/cli/share.d.ts +26 -0
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +41 -9
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +122 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/personal-vault.d.ts +44 -0
- package/dist/personal-vault.d.ts.map +1 -0
- package/dist/personal-vault.js +62 -0
- package/dist/personal-vault.js.map +1 -0
- package/package.json +1 -1
- package/src/bin/sync-runner.ts +9 -57
- package/src/cli/share.test.ts +145 -0
- package/src/cli/share.ts +72 -8
- package/src/index.ts +7 -1
- package/src/personal-vault.ts +63 -0
package/src/cli/share.ts
CHANGED
|
@@ -185,6 +185,32 @@ export interface ShareOptions {
|
|
|
185
185
|
* full-tree bidirectional runner opts in.
|
|
186
186
|
*/
|
|
187
187
|
propagateDeletes?: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Policy for which journal entries `propagateDeletes` is willing to
|
|
190
|
+
* convert into remote `DeleteObject` calls. Only consulted when
|
|
191
|
+
* `propagateDeletes === true`.
|
|
192
|
+
*
|
|
193
|
+
* - `"owned-only"` (default, safer): only entries whose journal
|
|
194
|
+
* `direction === "up"` are eligible. That is, only files this
|
|
195
|
+
* machine previously uploaded can be remotely deleted on its
|
|
196
|
+
* behalf. Entries the journal records as pulled from elsewhere
|
|
197
|
+
* (`direction === "down"`) are never delete-propagated — the
|
|
198
|
+
* local absence may just be an unpulled state or a filter
|
|
199
|
+
* mismatch, both of which previously caused this machine to
|
|
200
|
+
* erase other machines' uploads.
|
|
201
|
+
* - `"all"`: legacy behaviour — every in-scope journal entry whose
|
|
202
|
+
* local file is missing is eligible (regardless of direction). The
|
|
203
|
+
* bidirectional runner's first-push and any tool that wants to
|
|
204
|
+
* mirror a destructive local checkout opts in here explicitly.
|
|
205
|
+
*
|
|
206
|
+
* Independently of this policy, an entry is also dropped from the
|
|
207
|
+
* plan when `shouldSync(localPath, false) === false` — i.e. the
|
|
208
|
+
* current ignore filter would have skipped the path on pull. That
|
|
209
|
+
* symmetry blocks the failure mode where a path was filtered locally
|
|
210
|
+
* but lived in the vault (and the journal) from an older HQ layout
|
|
211
|
+
* or a different machine, causing the next push to erase it.
|
|
212
|
+
*/
|
|
213
|
+
propagateDeletePolicy?: "owned-only" | "all";
|
|
188
214
|
/**
|
|
189
215
|
* Identity stamped onto each uploaded object's S3 user metadata
|
|
190
216
|
* (`created-by`, `created-by-sub`, `created-at`). The hq-console vault UI
|
|
@@ -236,6 +262,14 @@ export interface ShareResult {
|
|
|
236
262
|
*/
|
|
237
263
|
export async function share(options: ShareOptions): Promise<ShareResult> {
|
|
238
264
|
const { paths, company, message, onConflict, vaultConfig, entityContext, hqRoot, skipUnchanged, propagateDeletes } = options;
|
|
265
|
+
// Default to the safer "owned-only" policy when delete-propagation is on
|
|
266
|
+
// but the caller hasn't pinned a policy. Pre-existing callers that passed
|
|
267
|
+
// `propagateDeletes: true` (the `sync now` push leg, the runner's
|
|
268
|
+
// bidirectional sync, the `--all` fanout) thereby flip to the safer
|
|
269
|
+
// semantics automatically. Set `propagateDeletePolicy: "all"` explicitly
|
|
270
|
+
// to opt back into the legacy any-missing-file-deletes behaviour.
|
|
271
|
+
const propagateDeletePolicy: "owned-only" | "all" =
|
|
272
|
+
options.propagateDeletePolicy ?? "owned-only";
|
|
239
273
|
const emit = options.onEvent ?? defaultConsoleLogger;
|
|
240
274
|
|
|
241
275
|
// Exactly-one-of contract: either we vend (vaultConfig) or the caller did
|
|
@@ -316,7 +350,13 @@ export async function share(options: ShareOptions): Promise<ShareResult> {
|
|
|
316
350
|
? resolveDeleteScopeRoots(paths, hqRoot, syncRoot)
|
|
317
351
|
: [];
|
|
318
352
|
const deletePlan = propagateDeletes === true
|
|
319
|
-
? computeDeletePlan(
|
|
353
|
+
? computeDeletePlan(
|
|
354
|
+
journal,
|
|
355
|
+
syncRoot,
|
|
356
|
+
deleteScopeRoots,
|
|
357
|
+
shouldSync,
|
|
358
|
+
propagateDeletePolicy,
|
|
359
|
+
)
|
|
320
360
|
: [];
|
|
321
361
|
|
|
322
362
|
emit({
|
|
@@ -681,18 +721,37 @@ function resolveDeleteScopeRoots(
|
|
|
681
721
|
|
|
682
722
|
/**
|
|
683
723
|
* Walk every journal key in `scopeRoots` whose local file is missing from
|
|
684
|
-
* disk
|
|
685
|
-
*
|
|
686
|
-
*
|
|
724
|
+
* disk and return the keys eligible for a remote `DeleteObject`. An entry
|
|
725
|
+
* is in the plan only when ALL of the following hold:
|
|
726
|
+
*
|
|
727
|
+
* 1. Its key matches (or sits beneath) one of the `scopeRoots` prefixes.
|
|
728
|
+
* 2. Its local file is missing from disk.
|
|
729
|
+
* 3. The current ignore filter (`shouldSync`) accepts the key — so paths
|
|
730
|
+
* filtered out by `.hqignore` / `.gitignore` / `DEFAULT_IGNORES` are
|
|
731
|
+
* never delete-propagated. This blocks the failure mode where a path
|
|
732
|
+
* lives in the vault (and the journal) but the local walk skips it
|
|
733
|
+
* because of asymmetric ignore rules; without this guard the push
|
|
734
|
+
* leg would erase it.
|
|
735
|
+
* 4. When `policy === "owned-only"`: the journal entry's `direction`
|
|
736
|
+
* is `"up"` (i.e. this machine previously uploaded the file). This
|
|
737
|
+
* blocks the failure mode where a behind machine's first `sync now`
|
|
738
|
+
* push leg would otherwise erase recent uploads from peers, since
|
|
739
|
+
* those entries are recorded as `direction: "down"` (pulled) or
|
|
740
|
+
* absent (never seen). Set `policy: "all"` to opt back into the
|
|
741
|
+
* legacy any-missing-file-deletes behaviour.
|
|
742
|
+
*
|
|
743
|
+
* Empty `scopeRoots` ⇒ empty plan (caller didn't opt in).
|
|
687
744
|
*/
|
|
688
745
|
function computeDeletePlan(
|
|
689
746
|
journal: SyncJournal,
|
|
690
747
|
syncRoot: string,
|
|
691
748
|
scopeRoots: string[],
|
|
749
|
+
shouldSync: (filePath: string, isDir?: boolean) => boolean,
|
|
750
|
+
policy: "owned-only" | "all",
|
|
692
751
|
): string[] {
|
|
693
752
|
if (scopeRoots.length === 0) return [];
|
|
694
753
|
const out: string[] = [];
|
|
695
|
-
for (const relativeKey of Object.
|
|
754
|
+
for (const [relativeKey, entry] of Object.entries(journal.files)) {
|
|
696
755
|
const inScope = scopeRoots.some(
|
|
697
756
|
(root) =>
|
|
698
757
|
root === "" ||
|
|
@@ -701,9 +760,14 @@ function computeDeletePlan(
|
|
|
701
760
|
);
|
|
702
761
|
if (!inScope) continue;
|
|
703
762
|
const localPath = path.join(syncRoot, relativeKey);
|
|
704
|
-
if (
|
|
705
|
-
|
|
706
|
-
|
|
763
|
+
if (fs.existsSync(localPath)) continue;
|
|
764
|
+
// (3) Symmetric filter guard. `shouldSync` is constructed from the same
|
|
765
|
+
// hqRoot the pull leg uses, so a key the pull would have skipped
|
|
766
|
+
// ("ignored") is also one we must not delete-propagate.
|
|
767
|
+
if (!shouldSync(localPath, false)) continue;
|
|
768
|
+
// (4) Direction guard under "owned-only" policy.
|
|
769
|
+
if (policy === "owned-only" && entry.direction !== "up") continue;
|
|
770
|
+
out.push(relativeKey);
|
|
707
771
|
}
|
|
708
772
|
return out;
|
|
709
773
|
}
|
package/src/index.ts
CHANGED
|
@@ -50,8 +50,14 @@ export {
|
|
|
50
50
|
} from "./cognito-auth.js";
|
|
51
51
|
export type { CognitoAuthConfig, CognitoTokens } from "./cognito-auth.js";
|
|
52
52
|
|
|
53
|
+
// Personal-vault scope helpers — shared between hq-sync-runner and `hq sync`
|
|
54
|
+
export {
|
|
55
|
+
PERSONAL_VAULT_EXCLUDED_TOP_LEVEL,
|
|
56
|
+
computePersonalVaultPaths,
|
|
57
|
+
} from "./personal-vault.js";
|
|
58
|
+
|
|
53
59
|
// VaultClient SDK (VLT-7)
|
|
54
|
-
export { VaultClient } from "./vault-client.js";
|
|
60
|
+
export { VaultClient, pickCanonicalPersonEntity } from "./vault-client.js";
|
|
55
61
|
export {
|
|
56
62
|
VaultClientError,
|
|
57
63
|
VaultAuthError,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Personal-vault scope helpers — shared between the menubar runner
|
|
3
|
+
* (`hq-sync-runner`) and the `hq sync` CLI so every composer of a personal
|
|
4
|
+
* push uses the same exclusion list.
|
|
5
|
+
*
|
|
6
|
+
* The exclusion list mirrors the Rust constant of the same name in
|
|
7
|
+
* `hq-sync/src-tauri/src/commands/personal.rs` so the Tauri menubar's
|
|
8
|
+
* first-push and this Node engine's steady-state push enforce identical
|
|
9
|
+
* scope. Every other top-level entry under hq_root (e.g. `.claude/`,
|
|
10
|
+
* `knowledge/`, `modules/`, `README.md`, `.codex/`, `core/`, `data/`,
|
|
11
|
+
* `personal/`) is included, subject to the usual `.hqignore` filter.
|
|
12
|
+
*
|
|
13
|
+
* Excluded entries (and why):
|
|
14
|
+
* - `.git`: a git repo's own metadata is hostile to multi-machine
|
|
15
|
+
* sync; .gitignore alone doesn't cover `.git/` because it's the repo
|
|
16
|
+
* itself, not a tracked path.
|
|
17
|
+
* - `companies/`: synced separately by the runner's per-membership
|
|
18
|
+
* fanout; do not double-write into the personal vault.
|
|
19
|
+
* - `repos/`, `workspace/`: per user directive — heavy local-only
|
|
20
|
+
* content (cloned remotes, session threads) that has no business in
|
|
21
|
+
* the personal vault.
|
|
22
|
+
*
|
|
23
|
+
* Note: `core/`, `data/`, and `personal/` were previously excluded but are
|
|
24
|
+
* INCLUDED as of user directive 2026-05-13. `core/` ships the hq-core
|
|
25
|
+
* scaffold — policies/, settings/, skills/, workers/, plus the rules
|
|
26
|
+
* manifest at core/core.yaml. `data/` and `personal/` carry per-user data,
|
|
27
|
+
* policies, hooks, and skills that follow the user across machines. The
|
|
28
|
+
* hq-root identity marker `core.yaml` (at hq_root, distinct from
|
|
29
|
+
* `core/core.yaml`) is filtered separately by the root-anchored
|
|
30
|
+
* `/core.yaml` DEFAULT_IGNORES rule.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import * as fs from "fs";
|
|
34
|
+
import * as path from "path";
|
|
35
|
+
|
|
36
|
+
export const PERSONAL_VAULT_EXCLUDED_TOP_LEVEL: readonly string[] = [
|
|
37
|
+
".git",
|
|
38
|
+
"companies",
|
|
39
|
+
"repos",
|
|
40
|
+
"workspace",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compute absolute paths to share for the personal vault: every top-level
|
|
45
|
+
* entry under `hqRoot` whose basename is NOT in
|
|
46
|
+
* `PERSONAL_VAULT_EXCLUDED_TOP_LEVEL`. Mirrors the Rust
|
|
47
|
+
* `is_personal_vault_path` predicate (just hoisted to the top-level step).
|
|
48
|
+
* Order is whatever `fs.readdirSync` returns — share() doesn't care, and
|
|
49
|
+
* the per-file walk inside share() handles recursion uniformly. Missing
|
|
50
|
+
* hqRoot returns []; callers treat that as "no personal content to push"
|
|
51
|
+
* rather than a hard error.
|
|
52
|
+
*/
|
|
53
|
+
export function computePersonalVaultPaths(hqRoot: string): string[] {
|
|
54
|
+
let entries: string[];
|
|
55
|
+
try {
|
|
56
|
+
entries = fs.readdirSync(hqRoot);
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return entries
|
|
61
|
+
.filter((name) => !PERSONAL_VAULT_EXCLUDED_TOP_LEVEL.includes(name))
|
|
62
|
+
.map((name) => path.join(hqRoot, name));
|
|
63
|
+
}
|