@indigoai-us/hq-cli 5.271.0 → 5.273.0-alpha.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 +16 -0
- package/dist/command-catalog.generated.d.ts +141 -0
- package/dist/command-catalog.generated.js +179 -0
- package/dist/commands/core-ontology.d.ts +16 -0
- package/dist/commands/core-ontology.js +86 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.js +173 -1
- package/dist/commands/files.d.ts +16 -0
- package/dist/commands/files.js +1 -1
- package/dist/commands/sync-mode.d.ts +11 -0
- package/dist/commands/sync-mode.js +30 -0
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +116 -0
- package/dist/lib/checkpoint-ontology/merge-copies.js +521 -0
- package/dist/lib/daemon/catalog.d.ts +18 -2
- package/dist/lib/daemon/catalog.js +18 -2
- package/dist/lib/daemon/config.d.ts +6 -0
- package/dist/lib/daemon/config.js +15 -2
- package/dist/lib/daemon/requests.d.ts +4 -0
- package/dist/lib/daemon/requests.js +2 -0
- package/dist/lib/daemon/run.js +78 -19
- package/dist/lib/daemon/supervisor.d.ts +8 -0
- package/dist/lib/daemon/supervisor.js +101 -1
- package/dist/lib/daemon/sync-runner.d.ts +58 -0
- package/dist/lib/daemon/sync-runner.js +277 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/package.json +2 -2
package/dist/commands/daemon.js
CHANGED
|
@@ -18,7 +18,7 @@ import * as os from "node:os";
|
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
import { major as semverMajor, valid as validSemver } from "semver";
|
|
20
20
|
import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits } from "../lib/daemon/catalog.js";
|
|
21
|
-
import { applyServiceToggle, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
|
|
21
|
+
import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
|
|
22
22
|
import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
|
|
23
23
|
import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
|
|
24
24
|
import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
|
|
@@ -27,7 +27,10 @@ import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
|
|
|
27
27
|
import { bundledHqCloud } from "../lib/daemon/hq-cloud.js";
|
|
28
28
|
import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "../lib/daemon/machine-type.js";
|
|
29
29
|
import { daemonPaths } from "../lib/daemon/paths.js";
|
|
30
|
+
import { processCommandLine } from "../lib/daemon/lock.js";
|
|
30
31
|
import { writeControlRequest } from "../lib/daemon/requests.js";
|
|
32
|
+
import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
|
|
33
|
+
import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
|
|
31
34
|
import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
|
|
32
35
|
class DaemonCommandError extends Error {
|
|
33
36
|
}
|
|
@@ -96,6 +99,7 @@ async function loadDefaults(over) {
|
|
|
96
99
|
windowsUser: over.windowsUser ?? (platform === "win32" ? mesh.resolveWindowsUser(env) : ""),
|
|
97
100
|
windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
|
|
98
101
|
systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
|
|
102
|
+
syncModeClient: over.syncModeClient,
|
|
99
103
|
};
|
|
100
104
|
}
|
|
101
105
|
function detect(d, flag, config) {
|
|
@@ -372,6 +376,135 @@ function queueRequest(d, request, what) {
|
|
|
372
376
|
const running = runningDaemonPid(paths.lockPath, d.daemonAlive) !== null;
|
|
373
377
|
d.out(running ? `Asked the daemon to ${what}.` : `Queued "${what}"; the daemon is not running, so it applies when it starts.`);
|
|
374
378
|
}
|
|
379
|
+
function daemonSyncModeClient(d) {
|
|
380
|
+
if (d.syncModeClient)
|
|
381
|
+
return d.syncModeClient();
|
|
382
|
+
return (async () => {
|
|
383
|
+
const [{ VaultClient }, { buildVaultConfig, ensureCognitoToken }] = await Promise.all([
|
|
384
|
+
import("@indigoai-us/hq-cloud"),
|
|
385
|
+
import("../utils/cognito-session.js"),
|
|
386
|
+
]);
|
|
387
|
+
const token = await ensureCognitoToken();
|
|
388
|
+
return new VaultClient(buildVaultConfig(token));
|
|
389
|
+
})();
|
|
390
|
+
}
|
|
391
|
+
function daemonSyncStatus(d) {
|
|
392
|
+
const host = currentHost(d);
|
|
393
|
+
const { paths } = host;
|
|
394
|
+
const config = readDaemonConfig(paths.configPath);
|
|
395
|
+
const daemonPid = runningDaemonPid(paths.lockPath, d.daemonAlive);
|
|
396
|
+
const daemonState = daemonPid === null ? null : readState(paths);
|
|
397
|
+
const syncUnit = daemonState?.units.find((unit) => unit.id === "sync");
|
|
398
|
+
const hqRoot = config.hqRoot ?? d.env.HQ_ROOT?.trim();
|
|
399
|
+
let ownerStatusCandidate = null;
|
|
400
|
+
if (hqRoot) {
|
|
401
|
+
try {
|
|
402
|
+
ownerStatusCandidate = readWatchRunnerOwnerStatus(watchRunnerOwnerStatusPath(hqRoot, d.home, d.env));
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
406
|
+
d.err(`Could not read sync runner owner status; treating it as unavailable: ${detail}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const ownerCommandLine = ownerStatusCandidate
|
|
410
|
+
? processCommandLine(ownerStatusCandidate.pid, { platform: d.platform, run: d.run })
|
|
411
|
+
: null;
|
|
412
|
+
const ownerStatus = ownerCommandLine && isSyncRunnerCommandLine(ownerCommandLine)
|
|
413
|
+
? ownerStatusCandidate
|
|
414
|
+
: null;
|
|
415
|
+
return {
|
|
416
|
+
running: syncUnit?.status === "running",
|
|
417
|
+
paused: config.syncPaused === true,
|
|
418
|
+
syncOwner: effectiveSyncOwner(config, host.detected.type),
|
|
419
|
+
owner: ownerStatus?.owner ?? (syncUnit?.status === "running" ? "hq-daemon" : null),
|
|
420
|
+
runnerPid: ownerStatus?.pid ?? syncUnit?.pid ?? null,
|
|
421
|
+
hqCloudVersion: ownerStatus?.version ?? null,
|
|
422
|
+
lastHeartbeat: syncUnit?.lastHeartbeatAt ?? ownerStatus?.heartbeatAt ?? null,
|
|
423
|
+
lastPassResult: syncUnit?.lastPassResult ?? null,
|
|
424
|
+
unitStatus: syncUnit?.status ?? "not-running",
|
|
425
|
+
reason: syncUnit?.reason ?? null,
|
|
426
|
+
logPath: paths.unitLogPath("sync"),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async function runDaemonSyncStatus(d, opts) {
|
|
430
|
+
const status = daemonSyncStatus(d);
|
|
431
|
+
if (opts.json) {
|
|
432
|
+
d.out(JSON.stringify(status, null, 2));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
d.out(`Sync ${status.running ? "running" : status.unitStatus}${status.paused ? " (paused)" : ""}.`);
|
|
436
|
+
d.out(`Configured sync owner: ${status.syncOwner}.`);
|
|
437
|
+
d.out(`Owner: ${status.owner ?? "none"}; runner pid: ${status.runnerPid ?? "none"}; hq-cloud: ${status.hqCloudVersion ?? "unknown"}.`);
|
|
438
|
+
if (status.reason)
|
|
439
|
+
d.out(`Reason: ${status.reason}`);
|
|
440
|
+
d.out(`Log: ${status.logPath}`);
|
|
441
|
+
}
|
|
442
|
+
function queueDaemonSyncControl(d, request) {
|
|
443
|
+
const host = currentHost(d);
|
|
444
|
+
writeControlRequest(host.paths.requestsDir, request);
|
|
445
|
+
handOver(d, host);
|
|
446
|
+
return runningDaemonPid(host.paths.lockPath, d.daemonAlive) !== null;
|
|
447
|
+
}
|
|
448
|
+
async function runDaemonSyncNow(d, opts) {
|
|
449
|
+
const host = currentHost(d);
|
|
450
|
+
const config = readDaemonConfig(host.paths.configPath);
|
|
451
|
+
if (config.syncPaused)
|
|
452
|
+
throw new DaemonCommandError("sync is paused; run `hq daemon sync resume` first");
|
|
453
|
+
if (!enabledServices(host.detected.type, config).includes("sync")) {
|
|
454
|
+
throw new DaemonCommandError("sync is disabled for this machine; run `hq daemon enable sync` first");
|
|
455
|
+
}
|
|
456
|
+
const running = queueDaemonSyncControl(d, { action: "restart", unit: "sync" });
|
|
457
|
+
const result = { accepted: true, action: "restart-sync-runner", daemonRunning: running };
|
|
458
|
+
d.out(opts.json ? JSON.stringify(result, null, 2) : running ? "Asked the daemon to start a fresh sync pass." : "Queued a fresh sync pass for the next daemon start.");
|
|
459
|
+
}
|
|
460
|
+
async function runDaemonSyncPause(d, paused, opts) {
|
|
461
|
+
const host = currentHost(d);
|
|
462
|
+
const config = readDaemonConfig(host.paths.configPath);
|
|
463
|
+
if (config.syncPaused !== paused)
|
|
464
|
+
writeDaemonConfig(host.paths.configPath, { ...config, syncPaused: paused });
|
|
465
|
+
const daemonRunning = queueDaemonSyncControl(d, { action: "reconcile" });
|
|
466
|
+
const result = { paused, persisted: true, daemonRunning };
|
|
467
|
+
d.out(opts.json ? JSON.stringify(result, null, 2) : `Sync ${paused ? "paused" : "resumed"}${daemonRunning ? "" : "; the setting will apply when the daemon starts"}.`);
|
|
468
|
+
}
|
|
469
|
+
async function runDaemonSyncTakeOwnership(d, opts) {
|
|
470
|
+
const host = currentHost(d);
|
|
471
|
+
const config = readDaemonConfig(host.paths.configPath);
|
|
472
|
+
if (config.syncOwner !== "daemon")
|
|
473
|
+
writeDaemonConfig(host.paths.configPath, { ...config, syncOwner: "daemon" });
|
|
474
|
+
const daemonRunning = queueDaemonSyncControl(d, { action: "reconcile" });
|
|
475
|
+
const result = { syncOwner: "daemon", persisted: true, daemonRunning };
|
|
476
|
+
d.out(opts.json ? JSON.stringify(result, null, 2) : `Daemon sync ownership persisted${daemonRunning ? "; asked the daemon to reconcile" : "; it applies when the daemon starts"}.`);
|
|
477
|
+
}
|
|
478
|
+
async function runDaemonSyncModeGet(d, companySlug, opts) {
|
|
479
|
+
const result = await getSyncModeForCompany({ companySlug, vaultClient: await daemonSyncModeClient(d) });
|
|
480
|
+
const value = {
|
|
481
|
+
companySlug: result.companySlug,
|
|
482
|
+
membershipId: result.membershipId,
|
|
483
|
+
mode: result.config.syncMode,
|
|
484
|
+
isDefault: result.config.isDefault,
|
|
485
|
+
...(result.config.updatedAt ? { updatedAt: result.config.updatedAt } : {}),
|
|
486
|
+
};
|
|
487
|
+
d.out(opts.json ? JSON.stringify(value, null, 2) : `${value.companySlug}: ${value.mode}${value.isDefault ? " (default)" : ""}`);
|
|
488
|
+
}
|
|
489
|
+
async function runDaemonSyncModeSet(d, companySlug, mode, opts) {
|
|
490
|
+
if (mode !== "all" && mode !== "shared") {
|
|
491
|
+
throw new DaemonCommandError(`Invalid sync mode '${mode}'. Use all or shared.`);
|
|
492
|
+
}
|
|
493
|
+
const result = await setSyncMode({
|
|
494
|
+
mode,
|
|
495
|
+
companySlug,
|
|
496
|
+
vaultClient: await daemonSyncModeClient(d),
|
|
497
|
+
});
|
|
498
|
+
const value = {
|
|
499
|
+
companySlug: result.companySlug,
|
|
500
|
+
membershipId: result.membershipId,
|
|
501
|
+
previousMode: result.previousMode,
|
|
502
|
+
mode: result.newMode,
|
|
503
|
+
isDefault: result.newConfig.isDefault,
|
|
504
|
+
...(result.newConfig.updatedAt ? { updatedAt: result.newConfig.updatedAt } : {}),
|
|
505
|
+
};
|
|
506
|
+
d.out(opts.json ? JSON.stringify(value, null, 2) : `${value.companySlug}: ${value.previousMode} → ${value.mode}`);
|
|
507
|
+
}
|
|
375
508
|
async function runToggle(d, action, services) {
|
|
376
509
|
const host = currentHost(d);
|
|
377
510
|
const { paths } = host;
|
|
@@ -603,6 +736,45 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
603
736
|
.description("Show the machine type and each service's state")
|
|
604
737
|
.option("--json", "Print machine-readable JSON")
|
|
605
738
|
.action(wrap((d, opts) => runStatus(d, opts)));
|
|
739
|
+
const sync = daemon
|
|
740
|
+
.command("sync")
|
|
741
|
+
.description("Inspect and control the daemon's company sync runner");
|
|
742
|
+
sync
|
|
743
|
+
.command("status")
|
|
744
|
+
.description("Show sync ownership, heartbeat, last pass and log path")
|
|
745
|
+
.option("--json", "Print machine-readable JSON")
|
|
746
|
+
.action(wrap((d, opts) => runDaemonSyncStatus(d, opts)));
|
|
747
|
+
sync
|
|
748
|
+
.command("now")
|
|
749
|
+
.description("Start a fresh sync pass immediately")
|
|
750
|
+
.option("--json", "Print machine-readable JSON")
|
|
751
|
+
.action(wrap((d, opts) => runDaemonSyncNow(d, opts)));
|
|
752
|
+
sync
|
|
753
|
+
.command("pause")
|
|
754
|
+
.description("Pause daemon sync until resumed")
|
|
755
|
+
.option("--json", "Print machine-readable JSON")
|
|
756
|
+
.action(wrap((d, opts) => runDaemonSyncPause(d, true, opts)));
|
|
757
|
+
sync
|
|
758
|
+
.command("resume")
|
|
759
|
+
.description("Resume daemon sync after a pause")
|
|
760
|
+
.option("--json", "Print machine-readable JSON")
|
|
761
|
+
.action(wrap((d, opts) => runDaemonSyncPause(d, false, opts)));
|
|
762
|
+
sync
|
|
763
|
+
.command("take-ownership")
|
|
764
|
+
.description("Persist daemon ownership of desktop sync and reconcile")
|
|
765
|
+
.option("--json", "Print machine-readable JSON")
|
|
766
|
+
.action(wrap((d, opts) => runDaemonSyncTakeOwnership(d, opts)));
|
|
767
|
+
const mode = sync.command("mode").description("Read or change a company's sync footprint");
|
|
768
|
+
mode
|
|
769
|
+
.command("get <company>")
|
|
770
|
+
.description("Read a company's current all/shared sync mode")
|
|
771
|
+
.option("--json", "Print machine-readable JSON")
|
|
772
|
+
.action(wrap((d, company, opts) => runDaemonSyncModeGet(d, company, opts)));
|
|
773
|
+
mode
|
|
774
|
+
.command("set <company> <mode>")
|
|
775
|
+
.description("Set a company's sync mode to all or shared")
|
|
776
|
+
.option("--json", "Print machine-readable JSON")
|
|
777
|
+
.action(wrap((d, company, mode, opts) => runDaemonSyncModeSet(d, company, mode, opts)));
|
|
606
778
|
daemon
|
|
607
779
|
.command("logs [unit]")
|
|
608
780
|
.description("Print the supervisor log, or one service's log")
|
package/dist/commands/files.d.ts
CHANGED
|
@@ -62,6 +62,22 @@ export declare class FilesDeleteHttpError extends Error {
|
|
|
62
62
|
export declare function formatFilesDeleteError(err: FilesDeleteHttpError, prefix: string): string;
|
|
63
63
|
/** Injectable yes/no confirmation seam (stubbed in tests). */
|
|
64
64
|
export type ConfirmFn = (message: string) => Promise<boolean>;
|
|
65
|
+
/**
|
|
66
|
+
* POST /v1/files/delete. Throws FilesDeleteHttpError on any non-2xx so the one
|
|
67
|
+
* caller renders a single consistent error path.
|
|
68
|
+
*
|
|
69
|
+
* Scope is EITHER a company vault (`companyUid` set) OR the caller's personal
|
|
70
|
+
* vault (`personal: true`). For the personal case the server resolves the target
|
|
71
|
+
* person + bucket from the authenticated caller — we send NO uid, just the
|
|
72
|
+
* `personal` flag — so there is nothing for the client to get wrong or spoof.
|
|
73
|
+
*/
|
|
74
|
+
export declare function callDeleteEndpoint(params: {
|
|
75
|
+
token: string;
|
|
76
|
+
companyUid?: string;
|
|
77
|
+
personal?: boolean;
|
|
78
|
+
prefix: string;
|
|
79
|
+
dryRun: boolean;
|
|
80
|
+
}): Promise<FilesDeleteResponse>;
|
|
65
81
|
interface RunFilesDeleteParams {
|
|
66
82
|
prefix: string;
|
|
67
83
|
dryRun: boolean;
|
package/dist/commands/files.js
CHANGED
|
@@ -879,7 +879,7 @@ function realConfirm(message) {
|
|
|
879
879
|
* person + bucket from the authenticated caller — we send NO uid, just the
|
|
880
880
|
* `personal` flag — so there is nothing for the client to get wrong or spoof.
|
|
881
881
|
*/
|
|
882
|
-
async function callDeleteEndpoint(params) {
|
|
882
|
+
export async function callDeleteEndpoint(params) {
|
|
883
883
|
const res = await vaultApiFetch({
|
|
884
884
|
token: params.token,
|
|
885
885
|
path: "/v1/files/delete",
|
|
@@ -70,6 +70,15 @@ export interface SetSyncModeResult {
|
|
|
70
70
|
export interface ShowSyncModesOptions {
|
|
71
71
|
vaultClient: SyncModeVaultClient;
|
|
72
72
|
}
|
|
73
|
+
export interface GetSyncModeOptions {
|
|
74
|
+
companySlug: string;
|
|
75
|
+
vaultClient: SyncModeVaultClient;
|
|
76
|
+
}
|
|
77
|
+
export interface GetSyncModeResult {
|
|
78
|
+
membershipId: string;
|
|
79
|
+
companySlug: string;
|
|
80
|
+
config: MembershipSyncConfig;
|
|
81
|
+
}
|
|
73
82
|
export interface ShowSyncModesRow {
|
|
74
83
|
companySlug: string;
|
|
75
84
|
companyName?: string;
|
|
@@ -100,6 +109,8 @@ export declare function setSyncMode(options: SetSyncModeOptions): Promise<SetSyn
|
|
|
100
109
|
* stable table output.
|
|
101
110
|
*/
|
|
102
111
|
export declare function showSyncModes(options: ShowSyncModesOptions): Promise<ShowSyncModesRow[]>;
|
|
112
|
+
/** Read one company's effective sync mode without hiding API read failures. */
|
|
113
|
+
export declare function getSyncModeForCompany(options: GetSyncModeOptions): Promise<GetSyncModeResult>;
|
|
103
114
|
/**
|
|
104
115
|
* Render the `--show` table as plain text. No external dep — hq-cli doesn't
|
|
105
116
|
* use cli-table3, so we hand-format columns to match the existing chalk +
|
|
@@ -163,6 +163,36 @@ export async function showSyncModes(options) {
|
|
|
163
163
|
rows.sort((a, b) => a.companySlug.localeCompare(b.companySlug));
|
|
164
164
|
return rows;
|
|
165
165
|
}
|
|
166
|
+
/** Read one company's effective sync mode without hiding API read failures. */
|
|
167
|
+
export async function getSyncModeForCompany(options) {
|
|
168
|
+
const { companySlug, vaultClient } = options;
|
|
169
|
+
const memberships = await vaultClient.listMyMemberships();
|
|
170
|
+
const enriched = await Promise.all(memberships.map(async (membership) => {
|
|
171
|
+
try {
|
|
172
|
+
const entity = await vaultClient.entity.get(membership.companyUid);
|
|
173
|
+
return { ok: true, membership, slug: entity.slug };
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
return { ok: false, membership, error };
|
|
177
|
+
}
|
|
178
|
+
}));
|
|
179
|
+
const match = enriched.find((row) => row.ok && row.slug === companySlug);
|
|
180
|
+
if (!match) {
|
|
181
|
+
const failedLookup = enriched.find((row) => !row.ok);
|
|
182
|
+
if (failedLookup && !failedLookup.ok)
|
|
183
|
+
throw failedLookup.error;
|
|
184
|
+
throw new Error(`No membership found for company '${companySlug}'.`);
|
|
185
|
+
}
|
|
186
|
+
for (const failedLookup of enriched) {
|
|
187
|
+
if (!failedLookup.ok) {
|
|
188
|
+
const detail = failedLookup.error instanceof Error ? failedLookup.error.message : String(failedLookup.error);
|
|
189
|
+
console.error(`getSyncModeForCompany: entity lookup failed for membership ${failedLookup.membership.membershipKey} while resolving '${companySlug}': ${detail}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const membershipId = match.membership.membershipKey;
|
|
193
|
+
const config = await vaultClient.getMembershipSyncConfig(membershipId);
|
|
194
|
+
return { membershipId, companySlug, config };
|
|
195
|
+
}
|
|
166
196
|
// ── Table rendering (used by CLI action, separable for tests) ──────────────
|
|
167
197
|
/**
|
|
168
198
|
* Render the `--show` table as plain text. No external dep — hq-cli doesn't
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge numbered entity copies (`<slug> 2.md`) into their original file and
|
|
3
|
+
* delete them (CO-54, owner decision CO-D-007 option 2).
|
|
4
|
+
*
|
|
5
|
+
* Gated by `checkpoint.ontology.merge-copies`. A copy is a file named
|
|
6
|
+
* `<slug> <n>.md` next to `<slug>.md` of the same type, with the same `id`,
|
|
7
|
+
* in one duplicate group. Other members of a group are never touched.
|
|
8
|
+
*
|
|
9
|
+
* Two steps, run separately so sync can push the merged original before its
|
|
10
|
+
* copy goes away:
|
|
11
|
+
*
|
|
12
|
+
* - merge: the copy's fresh aliases, back-references and relationship lines
|
|
13
|
+
* are added to the original, `signal_count` takes the higher value, and
|
|
14
|
+
* entity files that link to the copy are re-pointed at the original. Every
|
|
15
|
+
* rewritten file is recorded in the outbox before it is written, so sync
|
|
16
|
+
* pushes it. Keys the checkpoint does not own keep the original's value.
|
|
17
|
+
* - delete: a copy is deleted from the vault, by exact key, only when merging
|
|
18
|
+
* it again would change nothing and no entity file or signal links to it.
|
|
19
|
+
* The vault keeps every old version. The local file goes after the vault
|
|
20
|
+
* delete succeeds, or when the vault no longer has the key.
|
|
21
|
+
*
|
|
22
|
+
* Copies of one original merge in path order, each into the result of the
|
|
23
|
+
* previous one, and none of them counts as holding a name the others move.
|
|
24
|
+
* A copy whose body has lines the merged original lacks (prose the merge does
|
|
25
|
+
* not carry) is merged but never deleted. A copy linked from any file the
|
|
26
|
+
* merge cannot rewrite (a copy, a knowledge file, a signal) is held.
|
|
27
|
+
*
|
|
28
|
+
* A signal whose `entity_refs` names a copy holds that copy: signals are
|
|
29
|
+
* server-owned, so the ref cannot be re-pointed from here.
|
|
30
|
+
*/
|
|
31
|
+
import { type OntologyCompany } from "./company.js";
|
|
32
|
+
import { type EntityIndex } from "./tree.js";
|
|
33
|
+
export declare const MERGE_COPIES_FLAG = "checkpoint.ontology.merge-copies";
|
|
34
|
+
export type CopySkipReason = "no_original" | "original_invalid" | "identity_differs" | "not_in_duplicate_group";
|
|
35
|
+
export interface CopySkip {
|
|
36
|
+
copy: string;
|
|
37
|
+
reason: CopySkipReason;
|
|
38
|
+
}
|
|
39
|
+
export interface CopyPair {
|
|
40
|
+
type: string;
|
|
41
|
+
/** Company-relative path of the copy. */
|
|
42
|
+
copy: string;
|
|
43
|
+
/** Company-relative path of the original it merges into. */
|
|
44
|
+
original: string;
|
|
45
|
+
}
|
|
46
|
+
export interface CopyPlan extends CopyPair {
|
|
47
|
+
add_aliases: string[];
|
|
48
|
+
/** Copy names the original lacks that another entity already holds, so they do not move. */
|
|
49
|
+
dropped_aliases: string[];
|
|
50
|
+
add_backrefs: number;
|
|
51
|
+
add_relationship_lines: number;
|
|
52
|
+
/** The copy's higher `signal_count`, when it is higher. */
|
|
53
|
+
raise_signal_count?: number;
|
|
54
|
+
/** Keys the checkpoint does not own whose values differ; the original's value stays. */
|
|
55
|
+
kept_original_keys: string[];
|
|
56
|
+
/** Entity files that link to the copy. */
|
|
57
|
+
inbound_links: string[];
|
|
58
|
+
/** Signal files whose `entity_refs` name the copy. */
|
|
59
|
+
signal_refs: string[];
|
|
60
|
+
/** Why the copy cannot be merged or deleted from here. */
|
|
61
|
+
held?: "signal_refs" | "linked_from_unwritable" | "merge_invalid";
|
|
62
|
+
detail?: string;
|
|
63
|
+
/** Body lines of the copy the merged original lacks; a copy with any is never deleted. */
|
|
64
|
+
unmerged_body_lines: number;
|
|
65
|
+
}
|
|
66
|
+
export interface MergeCopiesReport {
|
|
67
|
+
company: string;
|
|
68
|
+
mode: "dry_run" | "merge" | "delete";
|
|
69
|
+
duplicate_groups_before: number;
|
|
70
|
+
duplicate_groups_after: number;
|
|
71
|
+
copies_found: number;
|
|
72
|
+
plans: CopyPlan[];
|
|
73
|
+
skipped: CopySkip[];
|
|
74
|
+
merged: string[];
|
|
75
|
+
relinked: string[];
|
|
76
|
+
deleted: string[];
|
|
77
|
+
/** Deleted copies the vault no longer had (a resumed run, or never synced). */
|
|
78
|
+
vault_already_absent: string[];
|
|
79
|
+
/** Copies still carrying content, links or refs, so not deleted this run. */
|
|
80
|
+
not_ready: {
|
|
81
|
+
copy: string;
|
|
82
|
+
reason: string;
|
|
83
|
+
}[];
|
|
84
|
+
errors: {
|
|
85
|
+
path: string;
|
|
86
|
+
error: string;
|
|
87
|
+
}[];
|
|
88
|
+
/** Entity-file links whose target file does not exist. */
|
|
89
|
+
broken_links_before: number;
|
|
90
|
+
broken_links_after: number;
|
|
91
|
+
/** Links in `company-brief.md` to a copy; the brief is re-rendered by the next checkpoint. */
|
|
92
|
+
brief_links_to_copies: string[];
|
|
93
|
+
}
|
|
94
|
+
/** Deletes one exact vault key; resolves to the number of objects deleted. */
|
|
95
|
+
export type DeleteVaultKey = (key: string) => Promise<number>;
|
|
96
|
+
export interface MergeCopiesOptions {
|
|
97
|
+
hqRoot: string;
|
|
98
|
+
company: OntologyCompany;
|
|
99
|
+
mode: "dry_run" | "merge" | "delete";
|
|
100
|
+
/** Handle at most this many copies (sorted by path). */
|
|
101
|
+
limit?: number;
|
|
102
|
+
now: Date;
|
|
103
|
+
deleteVaultKey?: DeleteVaultKey;
|
|
104
|
+
}
|
|
105
|
+
/** Copies in scope, and the ones skipped with a reason. Read-only. */
|
|
106
|
+
export declare function findCopyPairs(index: EntityIndex): {
|
|
107
|
+
pairs: CopyPair[];
|
|
108
|
+
skipped: CopySkip[];
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Plan, merge or delete numbered copies for one company. The dry run writes
|
|
112
|
+
* nothing; merge writes entity files only; delete removes vault keys and
|
|
113
|
+
* local copy files only.
|
|
114
|
+
*/
|
|
115
|
+
export declare function mergeCopies(options: MergeCopiesOptions): Promise<MergeCopiesReport>;
|
|
116
|
+
//# sourceMappingURL=merge-copies.d.ts.map
|