@git-stunts/git-warp 10.8.0 → 11.2.1
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/README.md +53 -32
- package/SECURITY.md +64 -0
- package/bin/cli/commands/check.js +168 -0
- package/bin/cli/commands/doctor/checks.js +422 -0
- package/bin/cli/commands/doctor/codes.js +46 -0
- package/bin/cli/commands/doctor/index.js +239 -0
- package/bin/cli/commands/doctor/types.js +89 -0
- package/bin/cli/commands/history.js +73 -0
- package/bin/cli/commands/info.js +139 -0
- package/bin/cli/commands/install-hooks.js +128 -0
- package/bin/cli/commands/materialize.js +99 -0
- package/bin/cli/commands/path.js +88 -0
- package/bin/cli/commands/query.js +194 -0
- package/bin/cli/commands/registry.js +28 -0
- package/bin/cli/commands/seek.js +592 -0
- package/bin/cli/commands/trust.js +154 -0
- package/bin/cli/commands/verify-audit.js +113 -0
- package/bin/cli/commands/view.js +45 -0
- package/bin/cli/infrastructure.js +336 -0
- package/bin/cli/schemas.js +177 -0
- package/bin/cli/shared.js +244 -0
- package/bin/cli/types.js +85 -0
- package/bin/presenters/index.js +6 -0
- package/bin/presenters/text.js +136 -0
- package/bin/warp-graph.js +5 -2346
- package/index.d.ts +32 -2
- package/index.js +2 -0
- package/package.json +8 -7
- package/src/domain/WarpGraph.js +106 -3252
- package/src/domain/errors/QueryError.js +2 -2
- package/src/domain/errors/TrustError.js +29 -0
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AuditMessageCodec.js +137 -0
- package/src/domain/services/AuditReceiptService.js +471 -0
- package/src/domain/services/AuditVerifierService.js +693 -0
- package/src/domain/services/HttpSyncServer.js +36 -22
- package/src/domain/services/MessageCodecInternal.js +3 -0
- package/src/domain/services/MessageSchemaDetector.js +2 -2
- package/src/domain/services/SyncAuthService.js +69 -3
- package/src/domain/services/WarpMessageCodec.js +4 -1
- package/src/domain/trust/TrustCanonical.js +42 -0
- package/src/domain/trust/TrustCrypto.js +111 -0
- package/src/domain/trust/TrustEvaluator.js +180 -0
- package/src/domain/trust/TrustRecordService.js +274 -0
- package/src/domain/trust/TrustStateBuilder.js +209 -0
- package/src/domain/trust/canonical.js +68 -0
- package/src/domain/trust/reasonCodes.js +64 -0
- package/src/domain/trust/schemas.js +160 -0
- package/src/domain/trust/verdict.js +42 -0
- package/src/domain/types/git-cas.d.ts +20 -0
- package/src/domain/utils/RefLayout.js +59 -0
- package/src/domain/warp/PatchSession.js +18 -0
- package/src/domain/warp/Writer.js +18 -3
- package/src/domain/warp/_internal.js +26 -0
- package/src/domain/warp/_wire.js +58 -0
- package/src/domain/warp/_wiredMethods.d.ts +100 -0
- package/src/domain/warp/checkpoint.methods.js +397 -0
- package/src/domain/warp/fork.methods.js +323 -0
- package/src/domain/warp/materialize.methods.js +188 -0
- package/src/domain/warp/materializeAdvanced.methods.js +339 -0
- package/src/domain/warp/patch.methods.js +529 -0
- package/src/domain/warp/provenance.methods.js +284 -0
- package/src/domain/warp/query.methods.js +279 -0
- package/src/domain/warp/subscribe.methods.js +272 -0
- package/src/domain/warp/sync.methods.js +549 -0
- package/src/infrastructure/adapters/GitGraphAdapter.js +67 -1
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
- package/src/ports/CommitPort.js +10 -0
- package/src/ports/RefPort.js +17 -0
- package/src/hooks/post-merge.sh +0 -60
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the `doctor` command.
|
|
3
|
+
*
|
|
4
|
+
* @module cli/commands/doctor/types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ── JSON-safe recursive value type ──────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/** @typedef {null | boolean | number | string | Array<*> | {[k:string]: *}} JsonValue */ // TODO(ts-cleanup): recursive type
|
|
10
|
+
|
|
11
|
+
/** @typedef {{[k:string]: JsonValue}} FindingEvidence */
|
|
12
|
+
|
|
13
|
+
// ── Finding ─────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} DoctorFinding
|
|
17
|
+
* @property {string} id - Check identifier (e.g. 'repo-accessible')
|
|
18
|
+
* @property {'ok'|'warn'|'fail'} status
|
|
19
|
+
* @property {string} code - Machine-readable code from CODES registry
|
|
20
|
+
* @property {'data_integrity'|'security'|'operability'|'hygiene'} impact
|
|
21
|
+
* @property {string} message - Human-readable summary
|
|
22
|
+
* @property {string} [fix] - Suggested remediation command or instruction
|
|
23
|
+
* @property {string} [helpUrl] - Stable documentation anchor
|
|
24
|
+
* @property {FindingEvidence} [evidence] - JSON-safe supporting data
|
|
25
|
+
* @property {number} [durationMs] - Time spent on this check
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// ── Policy ──────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} DoctorPolicy
|
|
32
|
+
* @property {boolean} strict
|
|
33
|
+
* @property {number} clockSkewMs
|
|
34
|
+
* @property {number} checkpointMaxAgeHours
|
|
35
|
+
* @property {number} globalDeadlineMs
|
|
36
|
+
* @property {{[checkId:string]: number}} checkTimeouts
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// ── Payload ─────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} DoctorPayload
|
|
43
|
+
* @property {1} doctorVersion
|
|
44
|
+
* @property {string} repo
|
|
45
|
+
* @property {string} graph
|
|
46
|
+
* @property {string} checkedAt - ISO 8601 timestamp
|
|
47
|
+
* @property {'ok'|'degraded'|'failed'} health
|
|
48
|
+
* @property {DoctorPolicy} policy
|
|
49
|
+
* @property {DoctorSummary} summary
|
|
50
|
+
* @property {DoctorFinding[]} findings
|
|
51
|
+
* @property {number} durationMs
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {Object} DoctorSummary
|
|
56
|
+
* @property {number} checksRun
|
|
57
|
+
* @property {number} findingsTotal
|
|
58
|
+
* @property {number} ok
|
|
59
|
+
* @property {number} warn
|
|
60
|
+
* @property {number} fail
|
|
61
|
+
* @property {string[]} priorityActions
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
// ── Context passed to each check ────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {Object} DoctorContext
|
|
68
|
+
* @property {import('../../types.js').Persistence} persistence
|
|
69
|
+
* @property {string} graphName
|
|
70
|
+
* @property {Array<{writerId: string, sha: string|null, ref: string}>} writerHeads
|
|
71
|
+
* @property {DoctorPolicy} policy
|
|
72
|
+
* @property {string} repoPath
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @callback DoctorCheck
|
|
77
|
+
* @param {DoctorContext} ctx
|
|
78
|
+
* @returns {Promise<DoctorFinding|DoctorFinding[]|null>}
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
// ── Exit codes ──────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
export const DOCTOR_EXIT_CODES = {
|
|
84
|
+
OK: 0,
|
|
85
|
+
FINDINGS: 3,
|
|
86
|
+
STRICT_FINDINGS: 4,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { summarizeOps } from '../../../src/visualization/renderers/ascii/history.js';
|
|
2
|
+
import { EXIT_CODES, notFoundError, parseCommandArgs } from '../infrastructure.js';
|
|
3
|
+
import { historySchema } from '../schemas.js';
|
|
4
|
+
import { openGraph, applyCursorCeiling, emitCursorWarning } from '../shared.js';
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('../types.js').CliOptions} CliOptions */
|
|
7
|
+
|
|
8
|
+
const HISTORY_OPTIONS = {
|
|
9
|
+
node: { type: 'string' },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** @param {string[]} args */
|
|
13
|
+
function parseHistoryArgs(args) {
|
|
14
|
+
const { values } = parseCommandArgs(args, HISTORY_OPTIONS, historySchema);
|
|
15
|
+
return { node: values.node ?? null };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {*} patch
|
|
20
|
+
* @param {string} nodeId
|
|
21
|
+
*/
|
|
22
|
+
function patchTouchesNode(patch, nodeId) {
|
|
23
|
+
const ops = Array.isArray(patch?.ops) ? patch.ops : [];
|
|
24
|
+
for (const op of ops) {
|
|
25
|
+
if (op.node === nodeId) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (op.from === nodeId || op.to === nodeId) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Handles the `history` command: shows patch history for a writer.
|
|
37
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
38
|
+
* @returns {Promise<{payload: *, exitCode: number}>}
|
|
39
|
+
*/
|
|
40
|
+
export default async function handleHistory({ options, args }) {
|
|
41
|
+
const historyOptions = parseHistoryArgs(args);
|
|
42
|
+
const { graph, graphName, persistence } = await openGraph(options);
|
|
43
|
+
const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
|
|
44
|
+
emitCursorWarning(cursorInfo, null);
|
|
45
|
+
|
|
46
|
+
const writerId = options.writer;
|
|
47
|
+
let patches = await graph.getWriterPatches(writerId);
|
|
48
|
+
if (cursorInfo.active) {
|
|
49
|
+
patches = patches.filter((/** @type {*} */ { patch }) => patch.lamport <= /** @type {number} */ (cursorInfo.tick)); // TODO(ts-cleanup): type CLI payload
|
|
50
|
+
}
|
|
51
|
+
if (patches.length === 0) {
|
|
52
|
+
throw notFoundError(`No patches found for writer: ${writerId}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const entries = patches
|
|
56
|
+
.filter((/** @type {*} */ { patch }) => !historyOptions.node || patchTouchesNode(patch, historyOptions.node)) // TODO(ts-cleanup): type CLI payload
|
|
57
|
+
.map((/** @type {*} */ { patch, sha }) => ({ // TODO(ts-cleanup): type CLI payload
|
|
58
|
+
sha,
|
|
59
|
+
schema: patch.schema,
|
|
60
|
+
lamport: patch.lamport,
|
|
61
|
+
opCount: Array.isArray(patch.ops) ? patch.ops.length : 0,
|
|
62
|
+
opSummary: Array.isArray(patch.ops) ? summarizeOps(patch.ops) : undefined,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
const payload = {
|
|
66
|
+
graph: graphName,
|
|
67
|
+
writer: writerId,
|
|
68
|
+
nodeFilter: historyOptions.node,
|
|
69
|
+
entries,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return { payload, exitCode: EXIT_CODES.OK };
|
|
73
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import WebCryptoAdapter from '../../../src/infrastructure/adapters/WebCryptoAdapter.js';
|
|
2
|
+
import WarpGraph from '../../../src/domain/WarpGraph.js';
|
|
3
|
+
import {
|
|
4
|
+
buildCheckpointRef,
|
|
5
|
+
buildCoverageRef,
|
|
6
|
+
buildWritersPrefix,
|
|
7
|
+
parseWriterIdFromRef,
|
|
8
|
+
} from '../../../src/domain/utils/RefLayout.js';
|
|
9
|
+
import { notFoundError } from '../infrastructure.js';
|
|
10
|
+
import { createPersistence, listGraphNames, readActiveCursor, readCheckpointDate } from '../shared.js';
|
|
11
|
+
|
|
12
|
+
/** @typedef {import('../types.js').CliOptions} CliOptions */
|
|
13
|
+
/** @typedef {import('../types.js').Persistence} Persistence */
|
|
14
|
+
/** @typedef {import('../types.js').GraphInfoResult} GraphInfoResult */
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Collects metadata about a single graph (writer count, refs, patches, checkpoint).
|
|
18
|
+
* @param {Persistence} persistence
|
|
19
|
+
* @param {string} graphName
|
|
20
|
+
* @param {Object} [options]
|
|
21
|
+
* @param {boolean} [options.includeWriterIds=false]
|
|
22
|
+
* @param {boolean} [options.includeRefs=false]
|
|
23
|
+
* @param {boolean} [options.includeWriterPatches=false]
|
|
24
|
+
* @param {boolean} [options.includeCheckpointDate=false]
|
|
25
|
+
* @returns {Promise<GraphInfoResult>}
|
|
26
|
+
*/
|
|
27
|
+
async function getGraphInfo(persistence, graphName, {
|
|
28
|
+
includeWriterIds = false,
|
|
29
|
+
includeRefs = false,
|
|
30
|
+
includeWriterPatches = false,
|
|
31
|
+
includeCheckpointDate = false,
|
|
32
|
+
} = {}) {
|
|
33
|
+
const writersPrefix = buildWritersPrefix(graphName);
|
|
34
|
+
const writerRefs = typeof persistence.listRefs === 'function'
|
|
35
|
+
? await persistence.listRefs(writersPrefix)
|
|
36
|
+
: [];
|
|
37
|
+
const writerIds = /** @type {string[]} */ (writerRefs
|
|
38
|
+
.map((ref) => parseWriterIdFromRef(ref))
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.sort());
|
|
41
|
+
|
|
42
|
+
/** @type {GraphInfoResult} */
|
|
43
|
+
const info = {
|
|
44
|
+
name: graphName,
|
|
45
|
+
writers: {
|
|
46
|
+
count: writerIds.length,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (includeWriterIds) {
|
|
51
|
+
info.writers.ids = writerIds;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (includeRefs || includeCheckpointDate) {
|
|
55
|
+
const checkpointRef = buildCheckpointRef(graphName);
|
|
56
|
+
const checkpointSha = await persistence.readRef(checkpointRef);
|
|
57
|
+
|
|
58
|
+
/** @type {{ref: string, sha: string|null, date?: string|null}} */
|
|
59
|
+
const checkpoint = { ref: checkpointRef, sha: checkpointSha || null };
|
|
60
|
+
|
|
61
|
+
if (includeCheckpointDate && checkpointSha) {
|
|
62
|
+
const checkpointDate = await readCheckpointDate(persistence, checkpointSha);
|
|
63
|
+
checkpoint.date = checkpointDate;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
info.checkpoint = checkpoint;
|
|
67
|
+
|
|
68
|
+
if (includeRefs) {
|
|
69
|
+
const coverageRef = buildCoverageRef(graphName);
|
|
70
|
+
const coverageSha = await persistence.readRef(coverageRef);
|
|
71
|
+
info.coverage = { ref: coverageRef, sha: coverageSha || null };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (includeWriterPatches && writerIds.length > 0) {
|
|
76
|
+
const graph = await WarpGraph.open({
|
|
77
|
+
persistence,
|
|
78
|
+
graphName,
|
|
79
|
+
writerId: 'cli',
|
|
80
|
+
crypto: new WebCryptoAdapter(),
|
|
81
|
+
});
|
|
82
|
+
/** @type {Record<string, number>} */
|
|
83
|
+
const writerPatches = {};
|
|
84
|
+
for (const writerId of writerIds) {
|
|
85
|
+
const patches = await graph.getWriterPatches(writerId);
|
|
86
|
+
writerPatches[/** @type {string} */ (writerId)] = patches.length;
|
|
87
|
+
}
|
|
88
|
+
info.writerPatches = writerPatches;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return info;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Handles the `info` command: summarizes graphs in the repository.
|
|
96
|
+
* @param {{options: CliOptions}} params
|
|
97
|
+
* @returns {Promise<{repo: string, graphs: GraphInfoResult[]}>}
|
|
98
|
+
*/
|
|
99
|
+
export default async function handleInfo({ options }) {
|
|
100
|
+
const { persistence } = await createPersistence(options.repo);
|
|
101
|
+
const graphNames = await listGraphNames(persistence);
|
|
102
|
+
|
|
103
|
+
if (options.graph && !graphNames.includes(options.graph)) {
|
|
104
|
+
throw notFoundError(`Graph not found: ${options.graph}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const detailGraphs = new Set();
|
|
108
|
+
if (options.graph) {
|
|
109
|
+
detailGraphs.add(options.graph);
|
|
110
|
+
} else if (graphNames.length === 1) {
|
|
111
|
+
detailGraphs.add(graphNames[0]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// In view mode, include extra data for visualization
|
|
115
|
+
const isViewMode = Boolean(options.view);
|
|
116
|
+
|
|
117
|
+
const graphs = [];
|
|
118
|
+
for (const name of graphNames) {
|
|
119
|
+
const includeDetails = detailGraphs.has(name);
|
|
120
|
+
const info = await getGraphInfo(persistence, name, {
|
|
121
|
+
includeWriterIds: includeDetails || isViewMode,
|
|
122
|
+
includeRefs: includeDetails || isViewMode,
|
|
123
|
+
includeWriterPatches: isViewMode,
|
|
124
|
+
includeCheckpointDate: isViewMode,
|
|
125
|
+
});
|
|
126
|
+
const activeCursor = await readActiveCursor(persistence, name);
|
|
127
|
+
if (activeCursor) {
|
|
128
|
+
info.cursor = { active: true, tick: activeCursor.tick, mode: activeCursor.mode };
|
|
129
|
+
} else {
|
|
130
|
+
info.cursor = { active: false };
|
|
131
|
+
}
|
|
132
|
+
graphs.push(info);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
repo: options.repo,
|
|
137
|
+
graphs,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { classifyExistingHook } from '../../../src/domain/services/HookInstaller.js';
|
|
4
|
+
import { EXIT_CODES, usageError, parseCommandArgs } from '../infrastructure.js';
|
|
5
|
+
import { installHooksSchema } from '../schemas.js';
|
|
6
|
+
import { createHookInstaller, isInteractive, promptUser } from '../shared.js';
|
|
7
|
+
|
|
8
|
+
/** @typedef {import('../types.js').CliOptions} CliOptions */
|
|
9
|
+
|
|
10
|
+
const INSTALL_HOOKS_OPTIONS = {
|
|
11
|
+
force: { type: 'boolean', default: false },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** @param {string[]} args */
|
|
15
|
+
function parseInstallHooksArgs(args) {
|
|
16
|
+
const { values } = parseCommandArgs(args, INSTALL_HOOKS_OPTIONS, installHooksSchema);
|
|
17
|
+
return values;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {*} classification
|
|
22
|
+
* @param {{force: boolean}} hookOptions
|
|
23
|
+
*/
|
|
24
|
+
async function resolveStrategy(classification, hookOptions) {
|
|
25
|
+
if (hookOptions.force) {
|
|
26
|
+
return 'replace';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (classification.kind === 'none') {
|
|
30
|
+
return 'install';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (classification.kind === 'ours') {
|
|
34
|
+
return await promptForOursStrategy(classification);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return await promptForForeignStrategy();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {*} classification */
|
|
41
|
+
async function promptForOursStrategy(classification) {
|
|
42
|
+
const installer = createHookInstaller();
|
|
43
|
+
if (classification.version === installer._version) {
|
|
44
|
+
return 'up-to-date';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!isInteractive()) {
|
|
48
|
+
throw usageError('Existing hook found. Use --force or run interactively.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const answer = await promptUser(
|
|
52
|
+
`Upgrade hook from v${classification.version} to v${installer._version}? [Y/n] `,
|
|
53
|
+
);
|
|
54
|
+
if (answer === '' || answer.toLowerCase() === 'y') {
|
|
55
|
+
return 'upgrade';
|
|
56
|
+
}
|
|
57
|
+
return 'skip';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function promptForForeignStrategy() {
|
|
61
|
+
if (!isInteractive()) {
|
|
62
|
+
throw usageError('Existing hook found. Use --force or run interactively.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
process.stderr.write('Existing post-merge hook found.\n');
|
|
66
|
+
process.stderr.write(' 1) Append (keep existing hook, add warp section)\n');
|
|
67
|
+
process.stderr.write(' 2) Replace (back up existing, install fresh)\n');
|
|
68
|
+
process.stderr.write(' 3) Skip\n');
|
|
69
|
+
const answer = await promptUser('Choose [1-3]: ');
|
|
70
|
+
|
|
71
|
+
if (answer === '1') {
|
|
72
|
+
return 'append';
|
|
73
|
+
}
|
|
74
|
+
if (answer === '2') {
|
|
75
|
+
return 'replace';
|
|
76
|
+
}
|
|
77
|
+
return 'skip';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @param {string} hookPath */
|
|
81
|
+
function readHookContent(hookPath) {
|
|
82
|
+
try {
|
|
83
|
+
return fs.readFileSync(hookPath, 'utf8');
|
|
84
|
+
} catch (/** @type {*} */ err) { // TODO(ts-cleanup): type fs error
|
|
85
|
+
if (err.code === 'ENOENT') {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Handles the `install-hooks` command.
|
|
94
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
95
|
+
* @returns {Promise<{payload: *, exitCode: number}>}
|
|
96
|
+
*/
|
|
97
|
+
export default async function handleInstallHooks({ options, args }) {
|
|
98
|
+
const hookOptions = parseInstallHooksArgs(args);
|
|
99
|
+
const installer = createHookInstaller();
|
|
100
|
+
const status = installer.getHookStatus(options.repo);
|
|
101
|
+
const content = readHookContent(status.hookPath);
|
|
102
|
+
const classification = classifyExistingHook(content);
|
|
103
|
+
const strategy = await resolveStrategy(classification, hookOptions);
|
|
104
|
+
|
|
105
|
+
if (strategy === 'up-to-date') {
|
|
106
|
+
return {
|
|
107
|
+
payload: {
|
|
108
|
+
action: 'up-to-date',
|
|
109
|
+
hookPath: status.hookPath,
|
|
110
|
+
version: installer._version,
|
|
111
|
+
},
|
|
112
|
+
exitCode: EXIT_CODES.OK,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (strategy === 'skip') {
|
|
117
|
+
return {
|
|
118
|
+
payload: { action: 'skipped' },
|
|
119
|
+
exitCode: EXIT_CODES.OK,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = installer.install(options.repo, { strategy });
|
|
124
|
+
return {
|
|
125
|
+
payload: result,
|
|
126
|
+
exitCode: EXIT_CODES.OK,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import WebCryptoAdapter from '../../../src/infrastructure/adapters/WebCryptoAdapter.js';
|
|
2
|
+
import WarpGraph from '../../../src/domain/WarpGraph.js';
|
|
3
|
+
import { EXIT_CODES, notFoundError } from '../infrastructure.js';
|
|
4
|
+
import { createPersistence, listGraphNames, readActiveCursor, emitCursorWarning } from '../shared.js';
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('../types.js').CliOptions} CliOptions */
|
|
7
|
+
/** @typedef {import('../types.js').Persistence} Persistence */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Materializes a single graph, creates a checkpoint, and returns summary stats.
|
|
11
|
+
* @param {{persistence: Persistence, graphName: string, writerId: string, ceiling?: number}} params
|
|
12
|
+
* @returns {Promise<{graph: string, nodes: number, edges: number, properties: number, checkpoint: string|null, writers: Record<string, number>, patchCount: number}>}
|
|
13
|
+
*/
|
|
14
|
+
async function materializeOneGraph({ persistence, graphName, writerId, ceiling }) {
|
|
15
|
+
const graph = await WarpGraph.open({ persistence, graphName, writerId, crypto: new WebCryptoAdapter() });
|
|
16
|
+
await graph.materialize(ceiling !== undefined ? { ceiling } : undefined);
|
|
17
|
+
const nodes = await graph.getNodes();
|
|
18
|
+
const edges = await graph.getEdges();
|
|
19
|
+
const checkpoint = ceiling !== undefined ? null : await graph.createCheckpoint();
|
|
20
|
+
const status = await graph.status();
|
|
21
|
+
|
|
22
|
+
// Build per-writer patch counts for the view renderer
|
|
23
|
+
/** @type {Record<string, number>} */
|
|
24
|
+
const writers = {};
|
|
25
|
+
let totalPatchCount = 0;
|
|
26
|
+
for (const wId of Object.keys(status.frontier)) {
|
|
27
|
+
const patches = await graph.getWriterPatches(wId);
|
|
28
|
+
writers[wId] = patches.length;
|
|
29
|
+
totalPatchCount += patches.length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const properties = await graph.getPropertyCount();
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
graph: graphName,
|
|
36
|
+
nodes: nodes.length,
|
|
37
|
+
edges: edges.length,
|
|
38
|
+
properties,
|
|
39
|
+
checkpoint,
|
|
40
|
+
writers,
|
|
41
|
+
patchCount: totalPatchCount,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Handles the `materialize` command: materializes and checkpoints all graphs.
|
|
47
|
+
* @param {{options: CliOptions}} params
|
|
48
|
+
* @returns {Promise<{payload: *, exitCode: number}>}
|
|
49
|
+
*/
|
|
50
|
+
export default async function handleMaterialize({ options }) {
|
|
51
|
+
const { persistence } = await createPersistence(options.repo);
|
|
52
|
+
const graphNames = await listGraphNames(persistence);
|
|
53
|
+
|
|
54
|
+
if (graphNames.length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
payload: { graphs: [] },
|
|
57
|
+
exitCode: EXIT_CODES.OK,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const targets = options.graph
|
|
62
|
+
? [options.graph]
|
|
63
|
+
: graphNames;
|
|
64
|
+
|
|
65
|
+
if (options.graph && !graphNames.includes(options.graph)) {
|
|
66
|
+
throw notFoundError(`Graph not found: ${options.graph}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const results = [];
|
|
70
|
+
let cursorWarningEmitted = false;
|
|
71
|
+
for (const name of targets) {
|
|
72
|
+
try {
|
|
73
|
+
const cursor = await readActiveCursor(persistence, name);
|
|
74
|
+
const ceiling = cursor ? cursor.tick : undefined;
|
|
75
|
+
if (cursor && !cursorWarningEmitted) {
|
|
76
|
+
emitCursorWarning({ active: true, tick: cursor.tick, maxTick: null }, null);
|
|
77
|
+
cursorWarningEmitted = true;
|
|
78
|
+
}
|
|
79
|
+
const result = await materializeOneGraph({
|
|
80
|
+
persistence,
|
|
81
|
+
graphName: name,
|
|
82
|
+
writerId: options.writer,
|
|
83
|
+
ceiling,
|
|
84
|
+
});
|
|
85
|
+
results.push(result);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
results.push({
|
|
88
|
+
graph: name,
|
|
89
|
+
error: error instanceof Error ? error.message : String(error),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const allFailed = results.every((r) => /** @type {*} */ (r).error); // TODO(ts-cleanup): type CLI payload
|
|
95
|
+
return {
|
|
96
|
+
payload: { graphs: results },
|
|
97
|
+
exitCode: allFailed ? EXIT_CODES.INTERNAL : EXIT_CODES.OK,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { renderSvg } from '../../../src/visualization/renderers/svg/index.js';
|
|
2
|
+
import { layoutGraph, pathResultToGraphData } from '../../../src/visualization/layouts/index.js';
|
|
3
|
+
import { EXIT_CODES, usageError, notFoundError, parseCommandArgs } from '../infrastructure.js';
|
|
4
|
+
import { openGraph, applyCursorCeiling, emitCursorWarning } from '../shared.js';
|
|
5
|
+
import { pathSchema } from '../schemas.js';
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('../types.js').CliOptions} CliOptions */
|
|
8
|
+
|
|
9
|
+
const PATH_OPTIONS = {
|
|
10
|
+
from: { type: 'string' },
|
|
11
|
+
to: { type: 'string' },
|
|
12
|
+
dir: { type: 'string' },
|
|
13
|
+
label: { type: 'string', multiple: true },
|
|
14
|
+
'max-depth': { type: 'string' },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** @param {string[]} args */
|
|
18
|
+
function parsePathArgs(args) {
|
|
19
|
+
const { values, positionals } = parseCommandArgs(args, PATH_OPTIONS, pathSchema, { allowPositionals: true });
|
|
20
|
+
|
|
21
|
+
// Positionals can supply from/to when flags are omitted
|
|
22
|
+
const from = values.from || positionals[0] || null;
|
|
23
|
+
const to = values.to || positionals[1] || null;
|
|
24
|
+
|
|
25
|
+
if (!from || !to) {
|
|
26
|
+
throw usageError('Path requires --from and --to (or two positional ids)');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Expand comma-separated labels
|
|
30
|
+
const labels = values.labels.flatMap((/** @type {string} */ l) => l.split(',').map((/** @type {string} */ s) => s.trim()).filter(Boolean));
|
|
31
|
+
|
|
32
|
+
/** @type {string|string[]|undefined} */
|
|
33
|
+
let labelFilter;
|
|
34
|
+
if (labels.length === 1) {
|
|
35
|
+
labelFilter = labels[0];
|
|
36
|
+
} else if (labels.length > 1) {
|
|
37
|
+
labelFilter = labels;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { from, to, dir: values.dir, labelFilter, maxDepth: values.maxDepth };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Handles the `path` command: finds a shortest path between two nodes.
|
|
45
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
46
|
+
* @returns {Promise<{payload: *, exitCode: number}>}
|
|
47
|
+
*/
|
|
48
|
+
export default async function handlePath({ options, args }) {
|
|
49
|
+
const pathOptions = parsePathArgs(args);
|
|
50
|
+
const { graph, graphName, persistence } = await openGraph(options);
|
|
51
|
+
const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
|
|
52
|
+
emitCursorWarning(cursorInfo, null);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const result = await graph.traverse.shortestPath(
|
|
56
|
+
pathOptions.from,
|
|
57
|
+
pathOptions.to,
|
|
58
|
+
{
|
|
59
|
+
dir: pathOptions.dir,
|
|
60
|
+
labelFilter: pathOptions.labelFilter,
|
|
61
|
+
maxDepth: pathOptions.maxDepth,
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const payload = {
|
|
66
|
+
graph: graphName,
|
|
67
|
+
from: pathOptions.from,
|
|
68
|
+
to: pathOptions.to,
|
|
69
|
+
...result,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (options.view && result.found && typeof options.view === 'string' && (options.view.startsWith('svg:') || options.view.startsWith('html:'))) {
|
|
73
|
+
const graphData = pathResultToGraphData(payload);
|
|
74
|
+
const positioned = await layoutGraph(graphData, { type: 'path' });
|
|
75
|
+
payload._renderedSvg = renderSvg(positioned, { title: `${graphName} path` });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
payload,
|
|
80
|
+
exitCode: result.found ? EXIT_CODES.OK : EXIT_CODES.NOT_FOUND,
|
|
81
|
+
};
|
|
82
|
+
} catch (/** @type {*} */ error) { // TODO(ts-cleanup): type error
|
|
83
|
+
if (error && error.code === 'NODE_NOT_FOUND') {
|
|
84
|
+
throw notFoundError(error.message);
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|