ctxr-cli 0.24.0 → 0.26.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/README.md +59 -4
- package/dist/commands/land.d.ts +50 -0
- package/dist/commands/land.d.ts.map +1 -0
- package/dist/commands/land.js +414 -0
- package/dist/commands/land.js.map +1 -0
- package/dist/commands/publish-check.d.ts +12 -0
- package/dist/commands/publish-check.d.ts.map +1 -1
- package/dist/commands/publish-check.js +38 -7
- package/dist/commands/publish-check.js.map +1 -1
- package/dist/commands/publish-urls.d.ts +7 -0
- package/dist/commands/publish-urls.d.ts.map +1 -1
- package/dist/commands/publish-urls.js +13 -5
- package/dist/commands/publish-urls.js.map +1 -1
- package/dist/commands/review-list.d.ts +34 -0
- package/dist/commands/review-list.d.ts.map +1 -0
- package/dist/commands/review-list.js +49 -0
- package/dist/commands/review-list.js.map +1 -0
- package/dist/commands/serve.d.ts +3 -6
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +81 -48
- package/dist/commands/serve.js.map +1 -1
- package/dist/config/schema.d.ts +40 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +39 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/core/approval/decision.d.ts +73 -0
- package/dist/core/approval/decision.d.ts.map +1 -0
- package/dist/core/approval/decision.js +166 -0
- package/dist/core/approval/decision.js.map +1 -0
- package/dist/core/approval/file-diff.d.ts +32 -0
- package/dist/core/approval/file-diff.d.ts.map +1 -0
- package/dist/core/approval/file-diff.js +83 -0
- package/dist/core/approval/file-diff.js.map +1 -0
- package/dist/core/approval/identity.d.ts +34 -0
- package/dist/core/approval/identity.d.ts.map +1 -0
- package/dist/core/approval/identity.js +105 -0
- package/dist/core/approval/identity.js.map +1 -0
- package/dist/core/approval/landing-record.d.ts +17 -0
- package/dist/core/approval/landing-record.d.ts.map +1 -0
- package/dist/core/approval/landing-record.js +30 -0
- package/dist/core/approval/landing-record.js.map +1 -0
- package/dist/core/approval/resolver.d.ts +15 -0
- package/dist/core/approval/resolver.d.ts.map +1 -0
- package/dist/core/approval/resolver.js +41 -0
- package/dist/core/approval/resolver.js.map +1 -0
- package/dist/core/approval/sessions.d.ts +36 -0
- package/dist/core/approval/sessions.d.ts.map +1 -0
- package/dist/core/approval/sessions.js +76 -0
- package/dist/core/approval/sessions.js.map +1 -0
- package/dist/core/browse/approve.d.ts +48 -0
- package/dist/core/browse/approve.d.ts.map +1 -0
- package/dist/core/browse/approve.js +284 -0
- package/dist/core/browse/approve.js.map +1 -0
- package/dist/core/browse/memo.d.ts +7 -0
- package/dist/core/browse/memo.d.ts.map +1 -0
- package/dist/core/browse/memo.js +56 -0
- package/dist/core/browse/memo.js.map +1 -0
- package/dist/core/browse/review.d.ts +22 -0
- package/dist/core/browse/review.d.ts.map +1 -0
- package/dist/core/browse/review.js +58 -0
- package/dist/core/browse/review.js.map +1 -0
- package/dist/core/browse/serving-state.d.ts +45 -0
- package/dist/core/browse/serving-state.d.ts.map +1 -0
- package/dist/core/browse/serving-state.js +108 -0
- package/dist/core/browse/serving-state.js.map +1 -0
- package/dist/core/checks/organize-checks.js +2 -2
- package/dist/core/checks/organize-checks.js.map +1 -1
- package/dist/core/errors.d.ts +24 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +55 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/forge/credentials.d.ts +69 -0
- package/dist/core/forge/credentials.d.ts.map +1 -0
- package/dist/core/forge/credentials.js +197 -0
- package/dist/core/forge/credentials.js.map +1 -0
- package/dist/core/forge/github.d.ts +73 -0
- package/dist/core/forge/github.d.ts.map +1 -0
- package/dist/core/forge/github.js +101 -0
- package/dist/core/forge/github.js.map +1 -0
- package/dist/core/git/exec.d.ts +15 -0
- package/dist/core/git/exec.d.ts.map +1 -1
- package/dist/core/git/exec.js +48 -1
- package/dist/core/git/exec.js.map +1 -1
- package/dist/core/graph/diff-block.d.ts.map +1 -1
- package/dist/core/graph/diff-block.js +13 -4
- package/dist/core/graph/diff-block.js.map +1 -1
- package/dist/core/graph/diff-document.d.ts +3 -2
- package/dist/core/graph/diff-document.d.ts.map +1 -1
- package/dist/core/graph/diff-document.js +2 -4
- package/dist/core/graph/diff-document.js.map +1 -1
- package/dist/core/publish/lifespan.d.ts +5 -5
- package/dist/core/publish/lifespan.d.ts.map +1 -1
- package/dist/core/publish/lifespan.js +13 -8
- package/dist/core/publish/lifespan.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +26 -0
- package/dist/run.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/templates/approve/approve-button.md +1 -0
- package/templates/approve/author-heading.md +1 -0
- package/templates/approve/author-note.md +1 -0
- package/templates/approve/comment-label.md +1 -0
- package/templates/approve/comment-required.md +1 -0
- package/templates/approve/commits-heading.md +1 -0
- package/templates/approve/decision-heading.md +1 -0
- package/templates/approve/decision-note.md +1 -0
- package/templates/approve/diff-binary.md +1 -0
- package/templates/approve/diff-empty.md +1 -0
- package/templates/approve/diff-heading.md +1 -0
- package/templates/approve/facts-heading.md +1 -0
- package/templates/approve/forge-unreachable.md +1 -0
- package/templates/approve/github-app-manifest.json +15 -0
- package/templates/approve/head-missing.md +1 -0
- package/templates/approve/index-empty.md +1 -0
- package/templates/approve/index-heading.md +1 -0
- package/templates/approve/index-intro.md +1 -0
- package/templates/approve/landed.md +1 -0
- package/templates/approve/moved-head.md +1 -0
- package/templates/approve/no-pull.md +1 -0
- package/templates/approve/not-open.md +1 -0
- package/templates/approve/pages-heading.md +1 -0
- package/templates/approve/pages-note.md +1 -0
- package/templates/approve/record-failed.md +1 -0
- package/templates/approve/request-changes-button.md +1 -0
- package/templates/approve/state-line.md +1 -0
- package/templates/graph/knowledge-changes/archived-item.md +1 -0
- package/templates/graph/knowledge-changes/dangling-item.md +1 -0
- package/templates/graph/knowledge-changes/reason-ambiguous.md +1 -0
- package/templates/graph/knowledge-changes/reason-not_found.md +1 -0
- package/templates/graph/review/moved-head.md +1 -0
- package/templates/land/resolver.md +12 -0
- package/templates/serve/style.css +63 -0
- package/templates/skills/ctxr-land.md +37 -31
- package/templates/skills/ctxr-publish.md +25 -1
- package/templates/skills/ctxr-second-opinion/run.mjs +28 -17
- package/templates/skills/ctxr-second-opinion.md +19 -14
- package/templates/skills/ctxr-session-lifecycle.md +9 -0
- package/templates/skills/ctxr-submit.md +4 -2
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createPublicKey, verify } from 'node:crypto';
|
|
2
|
+
const ALGORITHMS = {
|
|
3
|
+
RS256: { hash: 'sha256' },
|
|
4
|
+
ES256: { hash: 'sha256', dsaEncoding: 'ieee-p1363' },
|
|
5
|
+
};
|
|
6
|
+
/** Seconds of clock skew allowed on `exp` and `nbf`. */
|
|
7
|
+
const SKEW_SECONDS = 30;
|
|
8
|
+
/** An unknown key id re-fetches the key set at most this often, so a stream of forged ids cannot turn into a stream of fetches. */
|
|
9
|
+
const REFETCH_INTERVAL_MS = 10_000;
|
|
10
|
+
function decodeSegment(segment) {
|
|
11
|
+
return JSON.parse(Buffer.from(segment, 'base64url').toString('utf8'));
|
|
12
|
+
}
|
|
13
|
+
export function createIdentityVerifier(identity, opts = {}) {
|
|
14
|
+
const fetchImpl = opts.fetch ?? ((url) => fetch(url));
|
|
15
|
+
const now = opts.now ?? (() => new Date());
|
|
16
|
+
let keys = new Map();
|
|
17
|
+
let lastFetch = 0;
|
|
18
|
+
async function refreshKeys() {
|
|
19
|
+
lastFetch = Date.now();
|
|
20
|
+
const response = await fetchImpl(identity.jwks_url);
|
|
21
|
+
if (!response.ok)
|
|
22
|
+
throw new Error(`key set fetch answered ${response.status}`);
|
|
23
|
+
const body = (await response.json());
|
|
24
|
+
const next = new Map();
|
|
25
|
+
for (const jwk of body.keys ?? []) {
|
|
26
|
+
if (typeof jwk.kid !== 'string')
|
|
27
|
+
continue;
|
|
28
|
+
try {
|
|
29
|
+
next.set(jwk.kid, createPublicKey({ key: jwk, format: 'jwk' }));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// A key this runtime cannot read verifies nothing.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
keys = next;
|
|
36
|
+
}
|
|
37
|
+
async function keyFor(kid) {
|
|
38
|
+
const known = keys.get(kid);
|
|
39
|
+
if (known !== undefined)
|
|
40
|
+
return known;
|
|
41
|
+
if (Date.now() - lastFetch < REFETCH_INTERVAL_MS && keys.size > 0)
|
|
42
|
+
return null;
|
|
43
|
+
await refreshKeys();
|
|
44
|
+
return keys.get(kid) ?? null;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
async verify(headerValue) {
|
|
48
|
+
const token = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
49
|
+
if (token === undefined || token === '')
|
|
50
|
+
return { ok: false, reason: 'no identity assertion' };
|
|
51
|
+
const parts = token.split('.');
|
|
52
|
+
if (parts.length !== 3)
|
|
53
|
+
return { ok: false, reason: 'malformed assertion' };
|
|
54
|
+
const [headerPart, payloadPart, signaturePart] = parts;
|
|
55
|
+
let header;
|
|
56
|
+
let claims;
|
|
57
|
+
try {
|
|
58
|
+
header = decodeSegment(headerPart);
|
|
59
|
+
claims = decodeSegment(payloadPart);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { ok: false, reason: 'malformed assertion' };
|
|
63
|
+
}
|
|
64
|
+
const algorithm = typeof header.alg === 'string' ? ALGORITHMS[header.alg] : undefined;
|
|
65
|
+
if (algorithm === undefined)
|
|
66
|
+
return { ok: false, reason: 'unsupported signing algorithm' };
|
|
67
|
+
if (typeof header.kid !== 'string')
|
|
68
|
+
return { ok: false, reason: 'assertion names no key' };
|
|
69
|
+
let key;
|
|
70
|
+
try {
|
|
71
|
+
key = await keyFor(header.kid);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return { ok: false, reason: `key set unavailable: ${err instanceof Error ? err.message : String(err)}` };
|
|
75
|
+
}
|
|
76
|
+
if (key === null)
|
|
77
|
+
return { ok: false, reason: 'assertion signed by an unknown key' };
|
|
78
|
+
const signed = Buffer.from(`${headerPart}.${payloadPart}`, 'utf8');
|
|
79
|
+
const signature = Buffer.from(signaturePart, 'base64url');
|
|
80
|
+
let valid = false;
|
|
81
|
+
try {
|
|
82
|
+
valid = verify(algorithm.hash, signed, algorithm.dsaEncoding ? { key, dsaEncoding: algorithm.dsaEncoding } : key, signature);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
valid = false;
|
|
86
|
+
}
|
|
87
|
+
if (!valid)
|
|
88
|
+
return { ok: false, reason: 'signature does not verify' };
|
|
89
|
+
if (claims.iss !== identity.issuer)
|
|
90
|
+
return { ok: false, reason: 'wrong issuer' };
|
|
91
|
+
const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
|
|
92
|
+
if (!audiences.includes(identity.audience))
|
|
93
|
+
return { ok: false, reason: 'wrong audience' };
|
|
94
|
+
const nowSeconds = now().getTime() / 1000;
|
|
95
|
+
if (typeof claims.exp !== 'number' || claims.exp + SKEW_SECONDS < nowSeconds)
|
|
96
|
+
return { ok: false, reason: 'expired' };
|
|
97
|
+
if (typeof claims.nbf === 'number' && claims.nbf - SKEW_SECONDS > nowSeconds)
|
|
98
|
+
return { ok: false, reason: 'not yet valid' };
|
|
99
|
+
if (typeof claims.email !== 'string' || !/^[^@\s]+@[^@\s]+$/.test(claims.email))
|
|
100
|
+
return { ok: false, reason: 'no email claim' };
|
|
101
|
+
return { ok: true, email: claims.email };
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../../src/core/approval/identity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* write-lifecycle spec: where `ctxr land` records a landing for the approval
|
|
3
|
+
* page to read once the worktree is gone. A derived cache path, beside the
|
|
4
|
+
* graph document's, written atomically and never staged.
|
|
5
|
+
*/
|
|
6
|
+
export declare const LANDINGS_RELATIVE_PATH = ".contexture/cache/landings";
|
|
7
|
+
export interface LandingRecord {
|
|
8
|
+
worktree: string;
|
|
9
|
+
branch: string;
|
|
10
|
+
pull_request: number;
|
|
11
|
+
merged_commit: string | null;
|
|
12
|
+
landed_at: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function writeLandingRecord(storeRoot: string, record: LandingRecord): Promise<void>;
|
|
15
|
+
/** The record for `worktree`, or null — including for a name that is not a plain directory name. */
|
|
16
|
+
export declare function readLandingRecord(storeRoot: string, worktree: string): Promise<LandingRecord | null>;
|
|
17
|
+
//# sourceMappingURL=landing-record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landing-record.d.ts","sourceRoot":"","sources":["../../../src/core/approval/landing-record.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,+BAA+B,CAAC;AAEnE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAGhG;AAED,oGAAoG;AACpG,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAQ1G"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { writeFileAtomic } from '../fs/atomic.js';
|
|
4
|
+
/**
|
|
5
|
+
* write-lifecycle spec: where `ctxr land` records a landing for the approval
|
|
6
|
+
* page to read once the worktree is gone. A derived cache path, beside the
|
|
7
|
+
* graph document's, written atomically and never staged.
|
|
8
|
+
*/
|
|
9
|
+
export const LANDINGS_RELATIVE_PATH = '.contexture/cache/landings';
|
|
10
|
+
function recordPath(storeRoot, worktree) {
|
|
11
|
+
return path.join(storeRoot, LANDINGS_RELATIVE_PATH, `${worktree}.json`);
|
|
12
|
+
}
|
|
13
|
+
export async function writeLandingRecord(storeRoot, record) {
|
|
14
|
+
await mkdir(path.join(storeRoot, LANDINGS_RELATIVE_PATH), { recursive: true });
|
|
15
|
+
await writeFileAtomic(recordPath(storeRoot, record.worktree), `${JSON.stringify(record, null, 2)}\n`);
|
|
16
|
+
}
|
|
17
|
+
/** The record for `worktree`, or null — including for a name that is not a plain directory name. */
|
|
18
|
+
export async function readLandingRecord(storeRoot, worktree) {
|
|
19
|
+
if (worktree === '' || worktree.includes('/') || worktree.includes('\\') || worktree.startsWith('.'))
|
|
20
|
+
return null;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(await readFile(recordPath(storeRoot, worktree), 'utf8'));
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (err.code === 'ENOENT')
|
|
26
|
+
return null;
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=landing-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landing-record.js","sourceRoot":"","sources":["../../../src/core/approval/landing-record.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,4BAA4B,CAAC;AAUnE,SAAS,UAAU,CAAC,SAAiB,EAAE,QAAgB;IACrD,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,sBAAsB,EAAE,GAAG,QAAQ,OAAO,CAAC,CAAC;AAC1E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,SAAiB,EAAE,MAAqB;IAC/E,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,sBAAsB,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/E,MAAM,eAAe,CAAC,UAAU,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AACxG,CAAC;AAED,oGAAoG;AACpG,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,SAAiB,EAAE,QAAgB;IACzE,IAAI,QAAQ,KAAK,EAAE,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAClH,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,UAAU,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAkB,CAAC;IAC9F,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAClE,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function resolverPrompt(pullNumber: number, base: string, defaultBranch: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* design.md D6: the one place `ctxr land` spawns the store's configured
|
|
4
|
+
* conflict resolver — an argument vector, never a shell string, run in the
|
|
5
|
+
* checkout to resolve, with the prompt as its last argument. Its output goes to
|
|
6
|
+
* `output` (the command's stderr), so stdout keeps the one envelope.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runResolver(input: {
|
|
9
|
+
argv: readonly string[];
|
|
10
|
+
prompt: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
13
|
+
output: NodeJS.WritableStream;
|
|
14
|
+
}): Promise<number>;
|
|
15
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../../src/core/approval/resolver.ts"],"names":[],"mappings":"AAmBA,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAK9F;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAClD,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;CAC/B,GAAG,OAAO,CAAC,MAAM,CAAC,CASlB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { packagedTemplate } from '../templates.js';
|
|
5
|
+
/**
|
|
6
|
+
* write-lifecycle spec: the resolver gets "the conflict playbook the shipped
|
|
7
|
+
* lifecycle skill carries" — read out of that skill's own template, so the
|
|
8
|
+
* playbook has one source and the resolver can never follow an older copy.
|
|
9
|
+
*/
|
|
10
|
+
function lifecyclePlaybook(defaultBranch) {
|
|
11
|
+
const skill = readFileSync(fileURLToPath(new URL('../../../templates/skills/ctxr-session-lifecycle.md', import.meta.url)), 'utf8');
|
|
12
|
+
const start = skill.indexOf('## Conflict playbook');
|
|
13
|
+
if (start === -1)
|
|
14
|
+
throw new Error('the lifecycle skill template carries no "## Conflict playbook" section');
|
|
15
|
+
const next = skill.indexOf('\n## ', start + 1);
|
|
16
|
+
const section = skill.slice(skill.indexOf('\n', start) + 1, next === -1 ? undefined : next).trim();
|
|
17
|
+
return section.replaceAll('__DEFAULT_BRANCH__', defaultBranch);
|
|
18
|
+
}
|
|
19
|
+
export function resolverPrompt(pullNumber, base, defaultBranch) {
|
|
20
|
+
return packagedTemplate('land', 'resolver')
|
|
21
|
+
.replaceAll('__PR__', () => String(pullNumber))
|
|
22
|
+
.replaceAll('__BASE__', () => base)
|
|
23
|
+
.replace('__PLAYBOOK__', () => lifecyclePlaybook(defaultBranch));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* design.md D6: the one place `ctxr land` spawns the store's configured
|
|
27
|
+
* conflict resolver — an argument vector, never a shell string, run in the
|
|
28
|
+
* checkout to resolve, with the prompt as its last argument. Its output goes to
|
|
29
|
+
* `output` (the command's stderr), so stdout keeps the one envelope.
|
|
30
|
+
*/
|
|
31
|
+
export function runResolver(input) {
|
|
32
|
+
const [command, ...args] = input.argv;
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const child = spawn(command, [...args, input.prompt], { cwd: input.cwd, env: input.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
35
|
+
child.stdout.on('data', (chunk) => input.output.write(chunk));
|
|
36
|
+
child.stderr.on('data', (chunk) => input.output.write(chunk));
|
|
37
|
+
child.on('error', () => resolve(127));
|
|
38
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../../src/core/approval/resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,aAAqB;IAC9C,MAAM,KAAK,GAAG,YAAY,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,qDAAqD,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACnI,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACpD,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,wEAAwE,CAAC,CAAC;IAC5G,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IACnG,OAAO,OAAO,CAAC,UAAU,CAAC,oBAAoB,EAAE,aAAa,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,UAAkB,EAAE,IAAY,EAAE,aAAqB;IACpF,OAAO,gBAAgB,CAAC,MAAM,EAAE,UAAU,CAAC;SACxC,UAAU,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;SAC9C,UAAU,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC;SAClC,OAAO,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,aAAa,CAAC,CAAC,CAAC;AACrE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAM3B;IACC,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC;IACtC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,GAAwB,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QACnJ,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QACtE,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QACtE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QACtC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { StoreConfig } from '../../config/schema.js';
|
|
2
|
+
import type { GitRunner } from '../git/exec.js';
|
|
3
|
+
import { type ForgeClient, type PullRequest } from '../forge/github.js';
|
|
4
|
+
import { type ReviewState } from './decision.js';
|
|
5
|
+
export interface SessionWorktree {
|
|
6
|
+
/** The worktree's directory name — the key the review and approval routes use. */
|
|
7
|
+
name: string;
|
|
8
|
+
path: string;
|
|
9
|
+
branch: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SessionPull extends SessionWorktree {
|
|
12
|
+
pull: PullRequest;
|
|
13
|
+
review: ReviewState;
|
|
14
|
+
}
|
|
15
|
+
/** The session worktrees git knows, recognized as `ctxr session list` recognizes them. */
|
|
16
|
+
export declare function sessionWorktrees(git: GitRunner, repoCwd: string, config: StoreConfig): Promise<SessionWorktree[]>;
|
|
17
|
+
/** `origin/<default>` when the repository has that ref, else the local default branch — the review route's own rule. */
|
|
18
|
+
export declare function baseRef(git: GitRunner, cwd: string, defaultBranch: string): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* write-lifecycle spec (design.md D5): whether `currentHead` carries over the
|
|
21
|
+
* approval of `approvedHead` — each head's commits since it left `base`, compared
|
|
22
|
+
* by patch identity, commit for commit. Anything that cannot be computed (an
|
|
23
|
+
* approved head no longer in this repository) is "no".
|
|
24
|
+
*/
|
|
25
|
+
export declare function carriesOver(git: GitRunner, cwd: string, base: string, approvedHead: string, currentHead: string): Promise<boolean>;
|
|
26
|
+
/** Every forge call here, turned into the usage error the spec asks for when the forge cannot be read. */
|
|
27
|
+
export declare function forgeRead<T>(call: () => Promise<T>): Promise<T>;
|
|
28
|
+
/** One pull request's decision state, with an approval of an earlier head upgraded when the current head carries it over. */
|
|
29
|
+
export declare function reviewStateOf(git: GitRunner, cwd: string, base: string, forge: ForgeClient, recordingLogin: string, pull: PullRequest): Promise<ReviewState>;
|
|
30
|
+
/**
|
|
31
|
+
* change-approval spec: every session worktree whose branch has an open pull
|
|
32
|
+
* request, with its decision state. A worktree with no open pull request is
|
|
33
|
+
* left out.
|
|
34
|
+
*/
|
|
35
|
+
export declare function sessionPulls(git: GitRunner, repoCwd: string, config: StoreConfig, forge: ForgeClient, recordingLogin: string, openPulls?: () => Promise<PullRequest[]>): Promise<SessionPull[]>;
|
|
36
|
+
//# sourceMappingURL=sessions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../../src/core/approval/sessions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAE1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,OAAO,EAAqB,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE3F,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAErE,MAAM,WAAW,eAAe;IAC9B,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAY,SAAQ,eAAe;IAClD,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,0FAA0F;AAC1F,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAQvH;AAED,wHAAwH;AACxH,wBAAsB,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGjG;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUxI;AAED,0GAA0G;AAC1G,wBAAsB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAOrE;AAED,6HAA6H;AAC7H,wBAAsB,aAAa,CACjC,GAAG,EAAE,SAAS,EACd,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,EAClB,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC,WAAW,CAAC,CAOtB;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,WAAW,EAClB,cAAc,EAAE,MAAM,EACtB,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,GACvC,OAAO,CAAC,WAAW,EAAE,CAAC,CAcxB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ForgeUnreachableError } from '../errors.js';
|
|
3
|
+
import { patchIds } from '../git/exec.js';
|
|
4
|
+
import { listWorktrees } from '../git/worktree.js';
|
|
5
|
+
import { ForgeRequestError } from '../forge/github.js';
|
|
6
|
+
import { isSessionBranch, isSessionWorktreePath } from '../session.js';
|
|
7
|
+
import { resolveReviewState } from './decision.js';
|
|
8
|
+
/** The session worktrees git knows, recognized as `ctxr session list` recognizes them. */
|
|
9
|
+
export async function sessionWorktrees(git, repoCwd, config) {
|
|
10
|
+
const worktrees = await listWorktrees(git, repoCwd);
|
|
11
|
+
return worktrees
|
|
12
|
+
.filter((w) => w.branch !== null && (isSessionBranch(config, w.branch) || isSessionWorktreePath(config, w.path)))
|
|
13
|
+
.map((w) => ({ name: path.basename(w.path), path: w.path, branch: w.branch }));
|
|
14
|
+
}
|
|
15
|
+
/** `origin/<default>` when the repository has that ref, else the local default branch — the review route's own rule. */
|
|
16
|
+
export async function baseRef(git, cwd, defaultBranch) {
|
|
17
|
+
const remote = await git.run(['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${defaultBranch}`], { cwd, allowFailure: true });
|
|
18
|
+
return remote.exitCode === 0 ? `origin/${defaultBranch}` : defaultBranch;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* write-lifecycle spec (design.md D5): whether `currentHead` carries over the
|
|
22
|
+
* approval of `approvedHead` — each head's commits since it left `base`, compared
|
|
23
|
+
* by patch identity, commit for commit. Anything that cannot be computed (an
|
|
24
|
+
* approved head no longer in this repository) is "no".
|
|
25
|
+
*/
|
|
26
|
+
export async function carriesOver(git, cwd, base, approvedHead, currentHead) {
|
|
27
|
+
try {
|
|
28
|
+
const forkOf = async (head) => (await git.run(['merge-base', head, base], { cwd })).stdout.trim();
|
|
29
|
+
const before = await patchIds(cwd, `${await forkOf(approvedHead)}..${approvedHead}`);
|
|
30
|
+
const after = await patchIds(cwd, `${await forkOf(currentHead)}..${currentHead}`);
|
|
31
|
+
return before.length > 0 && before.length === after.length && before.every((id, i) => id === after[i]);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Every forge call here, turned into the usage error the spec asks for when the forge cannot be read. */
|
|
38
|
+
export async function forgeRead(call) {
|
|
39
|
+
try {
|
|
40
|
+
return await call();
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err instanceof ForgeRequestError)
|
|
44
|
+
throw new ForgeUnreachableError(err.message);
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** One pull request's decision state, with an approval of an earlier head upgraded when the current head carries it over. */
|
|
49
|
+
export async function reviewStateOf(git, cwd, base, forge, recordingLogin, pull) {
|
|
50
|
+
const [reviews, comments] = await Promise.all([forge.reviews(pull.number), forge.comments(pull.number)]);
|
|
51
|
+
const state = resolveReviewState({ reviews, comments, recordingLogin, head: pull.headSha });
|
|
52
|
+
if (state.approvalNeedsCarryCheck && state.decision !== null && (await carriesOver(git, cwd, base, state.decision.head, pull.headSha))) {
|
|
53
|
+
return { ...state, state: 'approved', approvalNeedsCarryCheck: false };
|
|
54
|
+
}
|
|
55
|
+
return state;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* change-approval spec: every session worktree whose branch has an open pull
|
|
59
|
+
* request, with its decision state. A worktree with no open pull request is
|
|
60
|
+
* left out.
|
|
61
|
+
*/
|
|
62
|
+
export async function sessionPulls(git, repoCwd, config, forge, recordingLogin, openPulls) {
|
|
63
|
+
const worktrees = await sessionWorktrees(git, repoCwd, config);
|
|
64
|
+
const open = await forgeRead(openPulls ?? (() => forge.openPulls()));
|
|
65
|
+
const base = await baseRef(git, repoCwd, config.git.default_branch);
|
|
66
|
+
// Each pull request's reviews and comments are independent reads, so they go out together.
|
|
67
|
+
const found = await Promise.all(worktrees.map(async (worktree) => {
|
|
68
|
+
const pull = open.find((p) => p.headRef === worktree.branch);
|
|
69
|
+
if (pull === undefined)
|
|
70
|
+
return null;
|
|
71
|
+
const review = await forgeRead(() => reviewStateOf(git, worktree.path, base, forge, recordingLogin, pull));
|
|
72
|
+
return { ...worktree, pull, review };
|
|
73
|
+
}));
|
|
74
|
+
return found.filter((p) => p !== null);
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../../src/core/approval/sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import type { ChangeApprovalConfig } from '../../config/schema.js';
|
|
3
|
+
import type { IdentityVerifier } from '../approval/identity.js';
|
|
4
|
+
import type { ForgeClient } from '../forge/github.js';
|
|
5
|
+
import type { GitRunner } from '../git/exec.js';
|
|
6
|
+
import type { Store } from '../store.js';
|
|
7
|
+
import type { LinkResolver } from './link-resolver.js';
|
|
8
|
+
import { type RenderedBody } from './render.js';
|
|
9
|
+
/**
|
|
10
|
+
* change-approval spec: the approval index, one session's approval page, and
|
|
11
|
+
* the one route on this server that admits `POST`. Every path under the prefix
|
|
12
|
+
* is answered here, and only after the identity assertion verifies — nothing
|
|
13
|
+
* about the store or the forge is read before that.
|
|
14
|
+
*/
|
|
15
|
+
export declare const APPROVE_ROUTE_PREFIX = "/approve/";
|
|
16
|
+
export interface ApprovalServer {
|
|
17
|
+
approval: ChangeApprovalConfig;
|
|
18
|
+
/** The origin of `serve.base_url`, which a decision's `Origin` header must equal (design.md D9). */
|
|
19
|
+
origin: string;
|
|
20
|
+
forge: ForgeClient;
|
|
21
|
+
verifier: IdentityVerifier;
|
|
22
|
+
git: GitRunner;
|
|
23
|
+
now(): Date;
|
|
24
|
+
}
|
|
25
|
+
/** What a route needs from the browsing surface once the requester is known: the shell, and note links. */
|
|
26
|
+
export interface PageKit {
|
|
27
|
+
page(title: string, body: RenderedBody): Promise<string>;
|
|
28
|
+
linkFor(notePath: string): string | null;
|
|
29
|
+
resolveLink: LinkResolver;
|
|
30
|
+
}
|
|
31
|
+
export interface Reply {
|
|
32
|
+
status: number;
|
|
33
|
+
contentType: string;
|
|
34
|
+
body: string;
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
export declare function isApprovePath(pathname: string): boolean;
|
|
38
|
+
export declare function isDecisionRoute(pathname: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Answers every request under the approval prefix. `server` is null when the
|
|
41
|
+
* store declares no change approval, and then every such path is `404` —
|
|
42
|
+
* unless `unavailable` says the block is declared but its surface could not be
|
|
43
|
+
* built after startup (serve-follows-config-changes D4), and then every such
|
|
44
|
+
* path is `503`, naming why. `makeKit` builds the browsing surface's shell only
|
|
45
|
+
* after the requester is known.
|
|
46
|
+
*/
|
|
47
|
+
export declare function handleApprove(store: Store, server: ApprovalServer | null, req: IncomingMessage, method: string, pathname: string, makeKit: () => Promise<PageKit>, unavailable?: string | null): Promise<Reply>;
|
|
48
|
+
//# sourceMappingURL=approve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approve.d.ts","sourceRoot":"","sources":["../../../src/core/browse/approve.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAInE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAIhE,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,oBAAoB,CAAC;AAEnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAA8B,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAO5E;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,cAAc,CAAC;AAOhD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,oGAAoG;IACpG,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,GAAG,EAAE,SAAS,CAAC;IACf,GAAG,IAAI,IAAI,CAAC;CACb;AAED,2GAA2G;AAC3G,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACzC,WAAW,EAAE,YAAY,CAAC;CAC3B;AAED,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAQD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEzD;AA0QD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,cAAc,GAAG,IAAI,EAC7B,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAC/B,WAAW,GAAE,MAAM,GAAG,IAAW,GAChC,OAAO,CAAC,KAAK,CAAC,CAkBhB"}
|