@indigoai-us/hq-cli 5.37.6 → 5.38.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/CHANGELOG.md +20 -0
- package/dist/commands/cloud.js +60 -19
- package/package.json +2 -2
- package/src/commands/cloud.status.test.ts +160 -0
- package/src/commands/cloud.ts +74 -22
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.38.0]
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`hq sync status` no longer reports "No sync journal yet" right after a
|
|
10
|
+
successful sync.** The command passed the HQ-root *path* where the engine
|
|
11
|
+
expects a journal *slug*, so it looked for `sync-journal._Users_<user>_hq.json`
|
|
12
|
+
— a slug the engine never writes — while the engine shards journals by slug
|
|
13
|
+
(`__hq_personal_vault__`, per-company). Status was blind to every real
|
|
14
|
+
journal. It now enumerates **all** shards via hq-cloud's `listJournals` and
|
|
15
|
+
prints a per-scope breakdown (personal vault + each company) plus an
|
|
16
|
+
all-scopes rollup; "No sync journal yet" appears only when none exist.
|
|
17
|
+
Tombstoned entries are excluded from tracked-file counts. Fixes
|
|
18
|
+
feedback_9fbf1f82 / DEV-1725 and feedback_46288b7b / DEV-1728.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Bumps `@indigoai-us/hq-cloud` to `^6.4.0` (was `^6.3.5`) for the new
|
|
23
|
+
`listJournals` / `getStateDir` enumeration API.
|
|
24
|
+
|
|
5
25
|
## [5.25.0] — 2026-05-26
|
|
6
26
|
|
|
7
27
|
### Added
|
package/dist/commands/cloud.js
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* hq sync status — show local journal summary
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
16
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="1164bdd1-84e4-5bc5-80a8-6888a6f4313c")}catch(e){}}();
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import * as fs from "fs";
|
|
19
19
|
import * as path from "path";
|
|
20
|
-
import { share, sync,
|
|
20
|
+
import { share, sync, getStateDir, listJournals, loadCachedTokens, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, } from "@indigoai-us/hq-cloud";
|
|
21
21
|
import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
22
22
|
import { emitNarrowHint, isStrictRefusal, resolveBannerLevel, } from "../lib/narrow-hint-banner.js";
|
|
23
23
|
/**
|
|
@@ -669,20 +669,20 @@ export function registerCloudCommands(program) {
|
|
|
669
669
|
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
670
670
|
.action((options) => {
|
|
671
671
|
try {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
672
|
+
// The engine SHARDS the journal by slug — the personal-vault fanout
|
|
673
|
+
// slot, one shard per cloud company, and the legacy "personal" shard.
|
|
674
|
+
// Enumerate EVERY shard the engine can write (via the engine's own
|
|
675
|
+
// `listJournals`) rather than reconstructing a single path: the old
|
|
676
|
+
// code passed the HQ-root PATH where a slug was expected, so it always
|
|
677
|
+
// looked at `sync-journal._Users_<user>_hq.json` — a slug the engine
|
|
678
|
+
// never writes — and printed "No sync journal yet" right after a
|
|
679
|
+
// successful sync (feedback_9fbf1f82 / feedback_46288b7b).
|
|
680
|
+
const journals = listJournals();
|
|
681
|
+
if (journals.length === 0) {
|
|
682
|
+
console.log(chalk.dim("No sync journal yet — run `hq sync now`, `hq sync push`, or `hq sync pull` to create one."));
|
|
683
|
+
console.log(chalk.dim(` Looked in: ${getStateDir()} (sync-journal.*.json)`));
|
|
676
684
|
return;
|
|
677
685
|
}
|
|
678
|
-
const journal = readJournal(options.hqRoot);
|
|
679
|
-
const entries = Object.entries(journal.files ?? {});
|
|
680
|
-
const lastSyncTimes = entries
|
|
681
|
-
.map(([, entry]) => entry.syncedAt)
|
|
682
|
-
.filter((t) => typeof t === "string")
|
|
683
|
-
.sort();
|
|
684
|
-
const lastSync = lastSyncTimes.at(-1) ?? "never";
|
|
685
|
-
const totalBytes = entries.reduce((acc, [, entry]) => acc + (entry.size ?? 0), 0);
|
|
686
686
|
const configPath = path.join(options.hqRoot, ".hq", "config.json");
|
|
687
687
|
let activeCompany;
|
|
688
688
|
if (fs.existsSync(configPath)) {
|
|
@@ -694,13 +694,42 @@ export function registerCloudCommands(program) {
|
|
|
694
694
|
// ignore
|
|
695
695
|
}
|
|
696
696
|
}
|
|
697
|
+
let grandFiles = 0;
|
|
698
|
+
let grandBytes = 0;
|
|
699
|
+
let grandLastSync = "";
|
|
697
700
|
console.log(chalk.bold("\nHQ Sync — Status"));
|
|
698
701
|
console.log(` HQ root: ${options.hqRoot}`);
|
|
699
702
|
console.log(` Active company: ${activeCompany ?? chalk.dim("(none)")}`);
|
|
700
|
-
console.log(`
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
703
|
+
console.log(` Journals: ${journals.length}`);
|
|
704
|
+
for (const j of journals) {
|
|
705
|
+
const entries = Object.entries(j.journal.files ?? {});
|
|
706
|
+
// Tombstones record removed files — exclude them from "tracked".
|
|
707
|
+
const live = entries.filter(([, entry]) => !entry.removedAt);
|
|
708
|
+
const bytes = live.reduce((acc, [, entry]) => acc + (entry.size ?? 0), 0);
|
|
709
|
+
const lastSync = j.journal.lastSync ||
|
|
710
|
+
live
|
|
711
|
+
.map(([, entry]) => entry.syncedAt)
|
|
712
|
+
.filter((t) => typeof t === "string")
|
|
713
|
+
.sort()
|
|
714
|
+
.at(-1) ||
|
|
715
|
+
"never";
|
|
716
|
+
grandFiles += live.length;
|
|
717
|
+
grandBytes += bytes;
|
|
718
|
+
if (lastSync !== "never" && lastSync > grandLastSync) {
|
|
719
|
+
grandLastSync = lastSync;
|
|
720
|
+
}
|
|
721
|
+
console.log(`\n • ${scopeLabel(j.slug)}`);
|
|
722
|
+
console.log(` Tracked files: ${live.length}`);
|
|
723
|
+
console.log(` Total size: ${formatBytes(bytes)}`);
|
|
724
|
+
console.log(` Last sync: ${lastSync}`);
|
|
725
|
+
console.log(chalk.dim(` Journal: ${j.path}`));
|
|
726
|
+
}
|
|
727
|
+
if (journals.length > 1) {
|
|
728
|
+
console.log(chalk.bold("\n All scopes"));
|
|
729
|
+
console.log(` Tracked files: ${grandFiles}`);
|
|
730
|
+
console.log(` Total size: ${formatBytes(grandBytes)}`);
|
|
731
|
+
console.log(` Last sync: ${grandLastSync || "never"}`);
|
|
732
|
+
}
|
|
704
733
|
}
|
|
705
734
|
catch (err) {
|
|
706
735
|
console.error(chalk.red("✗ Status failed:"), err instanceof Error ? err.message : String(err));
|
|
@@ -1113,6 +1142,18 @@ function readActiveCompanySlug(hqRoot) {
|
|
|
1113
1142
|
return undefined;
|
|
1114
1143
|
}
|
|
1115
1144
|
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Human-friendly label for a journal slug in `hq sync status`. The
|
|
1147
|
+
* personal-vault fanout slot and the legacy `companies/personal` company both
|
|
1148
|
+
* carry opaque slugs; everything else is a real company slug shown as-is.
|
|
1149
|
+
*/
|
|
1150
|
+
function scopeLabel(slug) {
|
|
1151
|
+
if (slug === PERSONAL_VAULT_JOURNAL_SLUG)
|
|
1152
|
+
return "personal vault";
|
|
1153
|
+
if (slug === "personal")
|
|
1154
|
+
return "personal (companies/personal)";
|
|
1155
|
+
return slug;
|
|
1156
|
+
}
|
|
1116
1157
|
function formatBytes(bytes) {
|
|
1117
1158
|
if (bytes === 0)
|
|
1118
1159
|
return "0 B";
|
|
@@ -1168,4 +1209,4 @@ function resolveUploadAuthorFromCache() {
|
|
|
1168
1209
|
}
|
|
1169
1210
|
}
|
|
1170
1211
|
//# sourceMappingURL=cloud.js.map
|
|
1171
|
-
//# debugId=
|
|
1212
|
+
//# debugId=1164bdd1-84e4-5bc5-80a8-6888a6f4313c
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.38.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"clean": "rm -rf dist"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@indigoai-us/hq-cloud": "^6.
|
|
18
|
+
"@indigoai-us/hq-cloud": "^6.4.0",
|
|
19
19
|
"@indigoai-us/hq-onboarding": "^0.1.0",
|
|
20
20
|
"@sentry/node": "^10.49.0",
|
|
21
21
|
"chalk": "^5.3.0",
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for `hq sync status`.
|
|
3
|
+
*
|
|
4
|
+
* Bug (feedback_9fbf1f82 / DEV-1725, feedback_46288b7b / DEV-1728): status
|
|
5
|
+
* reported "No sync journal yet" immediately after a SUCCESSFUL sync. Root
|
|
6
|
+
* cause — the action passed the HQ-root PATH where the engine expects a journal
|
|
7
|
+
* SLUG, so it looked for `sync-journal._Users_<user>_hq.json`, a slug the
|
|
8
|
+
* engine never writes, while the engine shards journals by slug
|
|
9
|
+
* (`__hq_personal_vault__`, per-company). The fix enumerates EVERY shard via
|
|
10
|
+
* the engine's `listJournals` and only reports "no journal" when none exist.
|
|
11
|
+
*
|
|
12
|
+
* These tests drive the real registered `status` command against real journal
|
|
13
|
+
* shards under a temp `HQ_STATE_DIR`, capturing stdout.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
17
|
+
import { Command } from "commander";
|
|
18
|
+
import * as fs from "fs";
|
|
19
|
+
import * as os from "os";
|
|
20
|
+
import * as path from "path";
|
|
21
|
+
import {
|
|
22
|
+
writeJournal,
|
|
23
|
+
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
24
|
+
} from "@indigoai-us/hq-cloud";
|
|
25
|
+
|
|
26
|
+
import { registerCloudCommands } from "./cloud.js";
|
|
27
|
+
|
|
28
|
+
/** Build a fresh program with the cloud commands registered. */
|
|
29
|
+
function makeProgram(): Command {
|
|
30
|
+
const program = new Command();
|
|
31
|
+
program.exitOverride(); // throw instead of process.exit on parse errors
|
|
32
|
+
registerCloudCommands(program);
|
|
33
|
+
return program;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Run `status` and return everything written to console.log. */
|
|
37
|
+
async function runStatus(stateDir: string): Promise<string> {
|
|
38
|
+
const lines: string[] = [];
|
|
39
|
+
const spy = vi
|
|
40
|
+
.spyOn(console, "log")
|
|
41
|
+
.mockImplementation((...args: unknown[]) => {
|
|
42
|
+
lines.push(args.map(String).join(" "));
|
|
43
|
+
});
|
|
44
|
+
try {
|
|
45
|
+
await makeProgram().parseAsync(
|
|
46
|
+
["status", "--hq-root", "/home/fake/hq"],
|
|
47
|
+
{ from: "user" },
|
|
48
|
+
);
|
|
49
|
+
} finally {
|
|
50
|
+
spy.mockRestore();
|
|
51
|
+
}
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("hq sync status", () => {
|
|
56
|
+
let stateDir: string;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-status-test-"));
|
|
60
|
+
process.env.HQ_STATE_DIR = stateDir;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
65
|
+
delete process.env.HQ_STATE_DIR;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function seed(slug: string, files: Record<string, unknown>): void {
|
|
69
|
+
writeJournal(slug, {
|
|
70
|
+
version: "2",
|
|
71
|
+
lastSync: "",
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
+
files: files as any,
|
|
74
|
+
pulls: [],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
it("reports 'No sync journal yet' only when no shard exists", async () => {
|
|
79
|
+
const out = await runStatus(stateDir);
|
|
80
|
+
expect(out).toContain("No sync journal yet");
|
|
81
|
+
expect(out).toContain("sync-journal.*.json");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("REGRESSION: shows the personal-vault journal after a `--personal` sync", async () => {
|
|
85
|
+
// This is the exact scenario both reporters hit: a successful personal
|
|
86
|
+
// sync, then status. It must NOT say "No sync journal yet".
|
|
87
|
+
seed(PERSONAL_VAULT_JOURNAL_SLUG, {
|
|
88
|
+
".claude/skills/x/SKILL.md": {
|
|
89
|
+
hash: "a",
|
|
90
|
+
size: 1234,
|
|
91
|
+
syncedAt: "2026-06-08T10:00:00.000Z",
|
|
92
|
+
direction: "up",
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const out = await runStatus(stateDir);
|
|
97
|
+
expect(out).not.toContain("No sync journal yet");
|
|
98
|
+
expect(out).toContain("personal vault");
|
|
99
|
+
expect(out).toContain("Tracked files: 1");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("enumerates every shard with a per-scope breakdown + rollup", async () => {
|
|
103
|
+
seed(PERSONAL_VAULT_JOURNAL_SLUG, {
|
|
104
|
+
".claude/a": {
|
|
105
|
+
hash: "a",
|
|
106
|
+
size: 1000,
|
|
107
|
+
syncedAt: "2026-06-08T10:00:00.000Z",
|
|
108
|
+
direction: "up",
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
seed("marshalops", {
|
|
112
|
+
"k/a.md": {
|
|
113
|
+
hash: "b",
|
|
114
|
+
size: 5000,
|
|
115
|
+
syncedAt: "2026-06-08T11:00:00.000Z",
|
|
116
|
+
direction: "down",
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
seed("glazeymedia", {
|
|
120
|
+
"k/b.md": {
|
|
121
|
+
hash: "c",
|
|
122
|
+
size: 777,
|
|
123
|
+
syncedAt: "2026-06-09T09:30:00.000Z",
|
|
124
|
+
direction: "down",
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const out = await runStatus(stateDir);
|
|
129
|
+
expect(out).toContain("Journals: 3");
|
|
130
|
+
expect(out).toContain("personal vault");
|
|
131
|
+
expect(out).toContain("marshalops");
|
|
132
|
+
expect(out).toContain("glazeymedia");
|
|
133
|
+
expect(out).toContain("All scopes");
|
|
134
|
+
// Rollup last-sync is the max across shards.
|
|
135
|
+
expect(out).toContain("2026-06-09T09:30:00.000Z");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("excludes tombstoned entries from tracked counts", async () => {
|
|
139
|
+
seed("glazeymedia", {
|
|
140
|
+
"k/live.md": {
|
|
141
|
+
hash: "c",
|
|
142
|
+
size: 100,
|
|
143
|
+
syncedAt: "2026-06-09T09:30:00.000Z",
|
|
144
|
+
direction: "down",
|
|
145
|
+
},
|
|
146
|
+
"k/removed.md": {
|
|
147
|
+
hash: "old",
|
|
148
|
+
size: 999,
|
|
149
|
+
syncedAt: "2026-05-01T00:00:00.000Z",
|
|
150
|
+
direction: "down",
|
|
151
|
+
removedAt: "2026-05-02T00:00:00.000Z",
|
|
152
|
+
removedReason: "scope_shrink",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const out = await runStatus(stateDir);
|
|
157
|
+
// One live file, not two — the tombstone is not "tracked".
|
|
158
|
+
expect(out).toContain("Tracked files: 1");
|
|
159
|
+
});
|
|
160
|
+
});
|
package/src/commands/cloud.ts
CHANGED
|
@@ -21,8 +21,8 @@ import * as path from "path";
|
|
|
21
21
|
import {
|
|
22
22
|
share,
|
|
23
23
|
sync,
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
getStateDir,
|
|
25
|
+
listJournals,
|
|
26
26
|
loadCachedTokens,
|
|
27
27
|
VaultClient,
|
|
28
28
|
computePersonalVaultPaths,
|
|
@@ -1102,25 +1102,28 @@ export function registerCloudCommands(program: Command): void {
|
|
|
1102
1102
|
)
|
|
1103
1103
|
.action((options: { hqRoot: string }) => {
|
|
1104
1104
|
try {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1105
|
+
// The engine SHARDS the journal by slug — the personal-vault fanout
|
|
1106
|
+
// slot, one shard per cloud company, and the legacy "personal" shard.
|
|
1107
|
+
// Enumerate EVERY shard the engine can write (via the engine's own
|
|
1108
|
+
// `listJournals`) rather than reconstructing a single path: the old
|
|
1109
|
+
// code passed the HQ-root PATH where a slug was expected, so it always
|
|
1110
|
+
// looked at `sync-journal._Users_<user>_hq.json` — a slug the engine
|
|
1111
|
+
// never writes — and printed "No sync journal yet" right after a
|
|
1112
|
+
// successful sync (feedback_9fbf1f82 / feedback_46288b7b).
|
|
1113
|
+
const journals = listJournals();
|
|
1114
|
+
|
|
1115
|
+
if (journals.length === 0) {
|
|
1116
|
+
console.log(
|
|
1117
|
+
chalk.dim(
|
|
1118
|
+
"No sync journal yet — run `hq sync now`, `hq sync push`, or `hq sync pull` to create one.",
|
|
1119
|
+
),
|
|
1120
|
+
);
|
|
1121
|
+
console.log(
|
|
1122
|
+
chalk.dim(` Looked in: ${getStateDir()} (sync-journal.*.json)`),
|
|
1123
|
+
);
|
|
1109
1124
|
return;
|
|
1110
1125
|
}
|
|
1111
1126
|
|
|
1112
|
-
const journal = readJournal(options.hqRoot);
|
|
1113
|
-
const entries = Object.entries(journal.files ?? {});
|
|
1114
|
-
const lastSyncTimes = entries
|
|
1115
|
-
.map(([, entry]) => entry.syncedAt)
|
|
1116
|
-
.filter((t): t is string => typeof t === "string")
|
|
1117
|
-
.sort();
|
|
1118
|
-
const lastSync = lastSyncTimes.at(-1) ?? "never";
|
|
1119
|
-
const totalBytes = entries.reduce(
|
|
1120
|
-
(acc, [, entry]) => acc + (entry.size ?? 0),
|
|
1121
|
-
0,
|
|
1122
|
-
);
|
|
1123
|
-
|
|
1124
1127
|
const configPath = path.join(options.hqRoot, ".hq", "config.json");
|
|
1125
1128
|
let activeCompany: string | undefined;
|
|
1126
1129
|
if (fs.existsSync(configPath)) {
|
|
@@ -1132,13 +1135,51 @@ export function registerCloudCommands(program: Command): void {
|
|
|
1132
1135
|
}
|
|
1133
1136
|
}
|
|
1134
1137
|
|
|
1138
|
+
let grandFiles = 0;
|
|
1139
|
+
let grandBytes = 0;
|
|
1140
|
+
let grandLastSync = "";
|
|
1141
|
+
|
|
1135
1142
|
console.log(chalk.bold("\nHQ Sync — Status"));
|
|
1136
1143
|
console.log(` HQ root: ${options.hqRoot}`);
|
|
1137
1144
|
console.log(` Active company: ${activeCompany ?? chalk.dim("(none)")}`);
|
|
1138
|
-
console.log(`
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1145
|
+
console.log(` Journals: ${journals.length}`);
|
|
1146
|
+
|
|
1147
|
+
for (const j of journals) {
|
|
1148
|
+
const entries = Object.entries(j.journal.files ?? {});
|
|
1149
|
+
// Tombstones record removed files — exclude them from "tracked".
|
|
1150
|
+
const live = entries.filter(([, entry]) => !entry.removedAt);
|
|
1151
|
+
const bytes = live.reduce(
|
|
1152
|
+
(acc, [, entry]) => acc + (entry.size ?? 0),
|
|
1153
|
+
0,
|
|
1154
|
+
);
|
|
1155
|
+
const lastSync =
|
|
1156
|
+
j.journal.lastSync ||
|
|
1157
|
+
live
|
|
1158
|
+
.map(([, entry]) => entry.syncedAt)
|
|
1159
|
+
.filter((t): t is string => typeof t === "string")
|
|
1160
|
+
.sort()
|
|
1161
|
+
.at(-1) ||
|
|
1162
|
+
"never";
|
|
1163
|
+
|
|
1164
|
+
grandFiles += live.length;
|
|
1165
|
+
grandBytes += bytes;
|
|
1166
|
+
if (lastSync !== "never" && lastSync > grandLastSync) {
|
|
1167
|
+
grandLastSync = lastSync;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
console.log(`\n • ${scopeLabel(j.slug)}`);
|
|
1171
|
+
console.log(` Tracked files: ${live.length}`);
|
|
1172
|
+
console.log(` Total size: ${formatBytes(bytes)}`);
|
|
1173
|
+
console.log(` Last sync: ${lastSync}`);
|
|
1174
|
+
console.log(chalk.dim(` Journal: ${j.path}`));
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (journals.length > 1) {
|
|
1178
|
+
console.log(chalk.bold("\n All scopes"));
|
|
1179
|
+
console.log(` Tracked files: ${grandFiles}`);
|
|
1180
|
+
console.log(` Total size: ${formatBytes(grandBytes)}`);
|
|
1181
|
+
console.log(` Last sync: ${grandLastSync || "never"}`);
|
|
1182
|
+
}
|
|
1142
1183
|
} catch (err) {
|
|
1143
1184
|
console.error(
|
|
1144
1185
|
chalk.red("✗ Status failed:"),
|
|
@@ -1702,6 +1743,17 @@ function readActiveCompanySlug(hqRoot: string): string | undefined {
|
|
|
1702
1743
|
}
|
|
1703
1744
|
}
|
|
1704
1745
|
|
|
1746
|
+
/**
|
|
1747
|
+
* Human-friendly label for a journal slug in `hq sync status`. The
|
|
1748
|
+
* personal-vault fanout slot and the legacy `companies/personal` company both
|
|
1749
|
+
* carry opaque slugs; everything else is a real company slug shown as-is.
|
|
1750
|
+
*/
|
|
1751
|
+
function scopeLabel(slug: string): string {
|
|
1752
|
+
if (slug === PERSONAL_VAULT_JOURNAL_SLUG) return "personal vault";
|
|
1753
|
+
if (slug === "personal") return "personal (companies/personal)";
|
|
1754
|
+
return slug;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1705
1757
|
function formatBytes(bytes: number): string {
|
|
1706
1758
|
if (bytes === 0) return "0 B";
|
|
1707
1759
|
const units = ["B", "KB", "MB", "GB"];
|