@git-stunts/git-warp 10.8.0 → 11.3.3
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 +80 -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/patch.js +142 -0
- package/bin/cli/commands/path.js +88 -0
- package/bin/cli/commands/query.js +235 -0
- package/bin/cli/commands/registry.js +32 -0
- package/bin/cli/commands/seek.js +598 -0
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +154 -0
- package/bin/cli/commands/verify-audit.js +114 -0
- package/bin/cli/commands/view.js +46 -0
- package/bin/cli/infrastructure.js +350 -0
- package/bin/cli/schemas.js +177 -0
- package/bin/cli/shared.js +244 -0
- package/bin/cli/types.js +96 -0
- package/bin/presenters/index.js +41 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +286 -28
- package/bin/warp-graph.js +5 -2346
- package/index.d.ts +111 -21
- package/index.js +2 -0
- package/package.json +10 -8
- package/src/domain/WarpGraph.js +109 -3252
- package/src/domain/crdt/ORSet.js +8 -8
- package/src/domain/errors/EmptyMessageError.js +2 -2
- package/src/domain/errors/ForkError.js +1 -1
- package/src/domain/errors/IndexError.js +1 -1
- package/src/domain/errors/OperationAbortedError.js +1 -1
- package/src/domain/errors/QueryError.js +3 -3
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/ShardCorruptionError.js +2 -2
- package/src/domain/errors/ShardLoadError.js +2 -2
- package/src/domain/errors/ShardValidationError.js +4 -4
- package/src/domain/errors/StorageError.js +2 -2
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +29 -0
- package/src/domain/errors/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- 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 +707 -0
- package/src/domain/services/BitmapIndexBuilder.js +3 -3
- package/src/domain/services/BitmapIndexReader.js +28 -19
- package/src/domain/services/BoundaryTransitionRecord.js +18 -17
- package/src/domain/services/CheckpointSerializerV5.js +17 -16
- package/src/domain/services/CheckpointService.js +2 -2
- package/src/domain/services/CommitDagTraversalService.js +13 -13
- package/src/domain/services/DagPathFinding.js +7 -7
- package/src/domain/services/DagTopology.js +1 -1
- package/src/domain/services/DagTraversal.js +1 -1
- package/src/domain/services/HealthCheckService.js +1 -1
- package/src/domain/services/HookInstaller.js +1 -1
- package/src/domain/services/HttpSyncServer.js +120 -55
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +11 -11
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +4 -1
- package/src/domain/services/MessageSchemaDetector.js +2 -2
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +42 -26
- package/src/domain/services/ProvenanceIndex.js +1 -1
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -3
- package/src/domain/services/StateDiff.js +14 -11
- package/src/domain/services/StateSerializerV5.js +2 -2
- package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
- package/src/domain/services/SyncAuthService.js +71 -4
- package/src/domain/services/SyncProtocol.js +25 -11
- package/src/domain/services/TemporalQuery.js +9 -6
- package/src/domain/services/TranslationCost.js +7 -5
- package/src/domain/services/WarpMessageCodec.js +4 -1
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +42 -0
- package/src/domain/trust/TrustCrypto.js +111 -0
- package/src/domain/trust/TrustEvaluator.js +195 -0
- package/src/domain/trust/TrustRecordService.js +281 -0
- package/src/domain/trust/TrustStateBuilder.js +222 -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/TickReceipt.js +1 -1
- package/src/domain/types/WarpErrors.js +45 -0
- package/src/domain/types/WarpOptions.js +29 -0
- package/src/domain/types/WarpPersistence.js +41 -0
- package/src/domain/types/WarpTypes.js +2 -2
- package/src/domain/types/WarpTypesV2.js +2 -2
- package/src/domain/types/git-cas.d.ts +20 -0
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/RefLayout.js +59 -0
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +26 -17
- 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 +254 -0
- package/src/domain/warp/checkpoint.methods.js +401 -0
- package/src/domain/warp/fork.methods.js +323 -0
- package/src/domain/warp/materialize.methods.js +238 -0
- package/src/domain/warp/materializeAdvanced.methods.js +350 -0
- package/src/domain/warp/patch.methods.js +554 -0
- package/src/domain/warp/provenance.methods.js +286 -0
- package/src/domain/warp/query.methods.js +280 -0
- package/src/domain/warp/subscribe.methods.js +272 -0
- package/src/domain/warp/sync.methods.js +554 -0
- package/src/globals.d.ts +64 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
- package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
- package/src/infrastructure/adapters/GitGraphAdapter.js +79 -11
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/ports/CommitPort.js +10 -0
- package/src/ports/RefPort.js +17 -0
- package/src/visualization/layouts/converters.js +2 -2
- package/src/visualization/layouts/elkAdapter.js +1 -1
- package/src/visualization/layouts/elkLayout.js +10 -7
- package/src/visualization/layouts/index.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +16 -6
- package/src/visualization/renderers/svg/index.js +1 -1
- 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<unknown> | {[k:string]: unknown}} JsonValue */
|
|
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,80 @@
|
|
|
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
|
+
/** @typedef {{patch: {schema?: number, lamport: number, ops?: Array<{type: string, node?: string, from?: string, to?: string}>}, sha: string}} PatchEntry */
|
|
8
|
+
|
|
9
|
+
const HISTORY_OPTIONS = {
|
|
10
|
+
node: { type: 'string' },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** @param {string[]} args */
|
|
14
|
+
function parseHistoryArgs(args) {
|
|
15
|
+
const { values } = parseCommandArgs(args, HISTORY_OPTIONS, historySchema);
|
|
16
|
+
return { node: values.node ?? null };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{ops?: Array<{node?: string, from?: string, to?: string}>}} patch
|
|
21
|
+
* @param {string} nodeId
|
|
22
|
+
*/
|
|
23
|
+
function patchTouchesNode(patch, nodeId) {
|
|
24
|
+
const ops = Array.isArray(patch?.ops) ? patch.ops : [];
|
|
25
|
+
for (const op of ops) {
|
|
26
|
+
if (op.node === nodeId) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (op.from === nodeId || op.to === nodeId) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Handles the `history` command: shows patch history for a writer.
|
|
38
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
39
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
40
|
+
*/
|
|
41
|
+
export default async function handleHistory({ options, args }) {
|
|
42
|
+
const historyOptions = parseHistoryArgs(args);
|
|
43
|
+
const { graph, graphName, persistence } = await openGraph(options);
|
|
44
|
+
const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
|
|
45
|
+
emitCursorWarning(cursorInfo, null);
|
|
46
|
+
|
|
47
|
+
const writerId = options.writer;
|
|
48
|
+
let patches = await graph.getWriterPatches(writerId);
|
|
49
|
+
if (cursorInfo.active) {
|
|
50
|
+
patches = patches.filter((/** @type {PatchEntry} */ { patch }) => patch.lamport <= /** @type {number} */ (cursorInfo.tick));
|
|
51
|
+
}
|
|
52
|
+
if (patches.length === 0) {
|
|
53
|
+
const knownWriters = await graph.discoverWriters();
|
|
54
|
+
if (knownWriters.length > 0) {
|
|
55
|
+
throw notFoundError(
|
|
56
|
+
`No patches found for writer: ${writerId}\nKnown writers: ${knownWriters.join(', ')}\nUse: warp-graph history --writer <id>`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
throw notFoundError(`No patches found for writer: ${writerId}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const entries = patches
|
|
63
|
+
.filter((/** @type {PatchEntry} */ { patch }) => !historyOptions.node || patchTouchesNode(patch, historyOptions.node))
|
|
64
|
+
.map((/** @type {PatchEntry} */ { patch, sha }) => ({
|
|
65
|
+
sha,
|
|
66
|
+
schema: patch.schema,
|
|
67
|
+
lamport: patch.lamport,
|
|
68
|
+
opCount: Array.isArray(patch.ops) ? patch.ops.length : 0,
|
|
69
|
+
opSummary: Array.isArray(patch.ops) ? summarizeOps(patch.ops) : undefined,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const payload = {
|
|
73
|
+
graph: graphName,
|
|
74
|
+
writer: writerId,
|
|
75
|
+
nodeFilter: historyOptions.node,
|
|
76
|
+
entries,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return { payload, exitCode: EXIT_CODES.OK };
|
|
80
|
+
}
|
|
@@ -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 {{kind: string, version?: string, appended?: boolean}} 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 {{kind: string, version?: string, appended?: boolean}} 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 (err) {
|
|
85
|
+
if (err instanceof Error && /** @type {{code?: string}} */ (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: unknown, 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: unknown, 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) => 'error' in r);
|
|
95
|
+
return {
|
|
96
|
+
payload: { graphs: results },
|
|
97
|
+
exitCode: allFailed ? EXIT_CODES.INTERNAL : EXIT_CODES.OK,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { EXIT_CODES, usageError, notFoundError, parseCommandArgs } from '../infrastructure.js';
|
|
2
|
+
import { openGraph } from '../shared.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('../types.js').CliOptions} CliOptions */
|
|
6
|
+
|
|
7
|
+
const PATCH_OPTIONS = {
|
|
8
|
+
writer: { type: 'string' },
|
|
9
|
+
limit: { type: 'string' },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const patchSchema = z.object({
|
|
13
|
+
writer: z.string().optional(),
|
|
14
|
+
limit: z.coerce.number().int().positive().optional(),
|
|
15
|
+
}).strict();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Collects all patches across all writers (or a single writer).
|
|
19
|
+
* @param {import('../types.js').WarpGraphInstance} graph
|
|
20
|
+
* @param {string|null} writerFilter
|
|
21
|
+
* @returns {Promise<Array<{sha: string, writer: string, patch: {schema?: number, lamport: number, ops?: Array<Record<string, unknown>>, context?: Record<string, unknown>}}>>}
|
|
22
|
+
*/
|
|
23
|
+
async function collectPatches(graph, writerFilter) {
|
|
24
|
+
const writers = writerFilter ? [writerFilter] : await graph.discoverWriters();
|
|
25
|
+
const all = [];
|
|
26
|
+
for (const writerId of writers) {
|
|
27
|
+
const patches = await graph.getWriterPatches(writerId);
|
|
28
|
+
for (const { patch, sha } of patches) {
|
|
29
|
+
all.push({ sha, writer: writerId, patch });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Sort by lamport ascending
|
|
33
|
+
all.sort((a, b) => (a.patch.lamport ?? 0) - (b.patch.lamport ?? 0));
|
|
34
|
+
return all;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Handles the `patch` command: show or list decoded patches.
|
|
39
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
40
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
41
|
+
*/
|
|
42
|
+
export default async function handlePatch({ options, args }) {
|
|
43
|
+
// First positional is the subaction: show or list
|
|
44
|
+
const subaction = args[0];
|
|
45
|
+
const rest = args.slice(1);
|
|
46
|
+
|
|
47
|
+
if (subaction === 'show') {
|
|
48
|
+
return await handlePatchShow({ options, args: rest });
|
|
49
|
+
}
|
|
50
|
+
if (subaction === 'list') {
|
|
51
|
+
return await handlePatchList({ options, args: rest });
|
|
52
|
+
}
|
|
53
|
+
if (!subaction) {
|
|
54
|
+
throw usageError('Usage: warp-graph patch <show|list> [options]\n show <sha> Decode and display a single patch\n list List all patches');
|
|
55
|
+
}
|
|
56
|
+
throw usageError(`Unknown patch subaction: ${subaction}. Use: show, list`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
61
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
62
|
+
*/
|
|
63
|
+
async function handlePatchShow({ options, args }) {
|
|
64
|
+
if (!args[0]) {
|
|
65
|
+
throw usageError('Usage: warp-graph patch show <sha>');
|
|
66
|
+
}
|
|
67
|
+
const targetSha = args[0];
|
|
68
|
+
const { graph, graphName } = await openGraph(options);
|
|
69
|
+
const allPatches = await collectPatches(graph, null);
|
|
70
|
+
|
|
71
|
+
const match = allPatches.find((p) => p.sha === targetSha || p.sha.startsWith(targetSha));
|
|
72
|
+
if (!match) {
|
|
73
|
+
throw notFoundError(`Patch not found: ${targetSha}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const payload = {
|
|
77
|
+
graph: graphName,
|
|
78
|
+
sha: match.sha,
|
|
79
|
+
writer: match.writer,
|
|
80
|
+
lamport: match.patch.lamport,
|
|
81
|
+
schema: match.patch.schema,
|
|
82
|
+
ops: match.patch.ops,
|
|
83
|
+
context: match.patch.context,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return { payload, exitCode: EXIT_CODES.OK };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
91
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
92
|
+
*/
|
|
93
|
+
async function handlePatchList({ options, args }) {
|
|
94
|
+
const { values } = parseCommandArgs(args, PATCH_OPTIONS, patchSchema);
|
|
95
|
+
const { graph, graphName } = await openGraph(options);
|
|
96
|
+
const writerFilter = values.writer || null;
|
|
97
|
+
const allPatches = await collectPatches(graph, writerFilter);
|
|
98
|
+
|
|
99
|
+
const limit = values.limit ?? allPatches.length;
|
|
100
|
+
const entries = allPatches.slice(0, limit).map((p) => ({
|
|
101
|
+
sha: p.sha.slice(0, 7),
|
|
102
|
+
fullSha: p.sha,
|
|
103
|
+
writer: p.writer,
|
|
104
|
+
lamport: p.patch.lamport,
|
|
105
|
+
opCount: Array.isArray(p.patch.ops) ? p.patch.ops.length : 0,
|
|
106
|
+
nodeIds: extractNodeIds(p.patch.ops || []),
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
const payload = {
|
|
110
|
+
graph: graphName,
|
|
111
|
+
total: allPatches.length,
|
|
112
|
+
showing: entries.length,
|
|
113
|
+
writerFilter,
|
|
114
|
+
entries,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return { payload, exitCode: EXIT_CODES.OK };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extracts unique node IDs touched by a patch's operations.
|
|
122
|
+
* @param {Array<Record<string, unknown>>} ops
|
|
123
|
+
* @returns {string[]}
|
|
124
|
+
*/
|
|
125
|
+
function extractNodeIds(ops) {
|
|
126
|
+
if (!Array.isArray(ops)) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const ids = new Set();
|
|
130
|
+
for (const op of ops) {
|
|
131
|
+
if (op.node) {
|
|
132
|
+
ids.add(op.node);
|
|
133
|
+
}
|
|
134
|
+
if (op.from) {
|
|
135
|
+
ids.add(op.from);
|
|
136
|
+
}
|
|
137
|
+
if (op.to) {
|
|
138
|
+
ids.add(op.to);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return [...ids].sort();
|
|
142
|
+
}
|