dsh-gitnexus-plus 0.0.0-stage → 0.1.2

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.
Files changed (90) hide show
  1. package/LICENSE +38 -0
  2. package/README.md +631 -2
  3. package/cordis.patch.yml +18 -0
  4. package/lib/bin/gitnexus-coverage.d.ts +14 -0
  5. package/lib/bin/gitnexus-coverage.d.ts.map +1 -0
  6. package/lib/bin/gitnexus-coverage.js +171 -0
  7. package/lib/bin/gitnexus-coverage.js.map +1 -0
  8. package/lib/bin/search-helper.d.ts +22 -0
  9. package/lib/bin/search-helper.d.ts.map +1 -0
  10. package/lib/bin/search-helper.js +57 -0
  11. package/lib/bin/search-helper.js.map +1 -0
  12. package/lib/client.js +2131 -0
  13. package/lib/gitnexus/analyze.d.ts +173 -0
  14. package/lib/gitnexus/analyze.d.ts.map +1 -0
  15. package/lib/gitnexus/analyze.js +308 -0
  16. package/lib/gitnexus/analyze.js.map +1 -0
  17. package/lib/gitnexus/commands.d.ts +129 -0
  18. package/lib/gitnexus/commands.d.ts.map +1 -0
  19. package/lib/gitnexus/commands.js +185 -0
  20. package/lib/gitnexus/commands.js.map +1 -0
  21. package/lib/gitnexus/coverage.d.ts +340 -0
  22. package/lib/gitnexus/coverage.d.ts.map +1 -0
  23. package/lib/gitnexus/coverage.js +554 -0
  24. package/lib/gitnexus/coverage.js.map +1 -0
  25. package/lib/gitnexus/format.d.ts +16 -0
  26. package/lib/gitnexus/format.d.ts.map +1 -0
  27. package/lib/gitnexus/format.js +178 -0
  28. package/lib/gitnexus/format.js.map +1 -0
  29. package/lib/gitnexus/git-activity.d.ts +136 -0
  30. package/lib/gitnexus/git-activity.d.ts.map +1 -0
  31. package/lib/gitnexus/git-activity.js +345 -0
  32. package/lib/gitnexus/git-activity.js.map +1 -0
  33. package/lib/gitnexus/git.d.ts +70 -0
  34. package/lib/gitnexus/git.d.ts.map +1 -0
  35. package/lib/gitnexus/git.js +121 -0
  36. package/lib/gitnexus/git.js.map +1 -0
  37. package/lib/gitnexus/index-meta.d.ts +160 -0
  38. package/lib/gitnexus/index-meta.d.ts.map +1 -0
  39. package/lib/gitnexus/index-meta.js +176 -0
  40. package/lib/gitnexus/index-meta.js.map +1 -0
  41. package/lib/gitnexus/install.d.ts +97 -0
  42. package/lib/gitnexus/install.d.ts.map +1 -0
  43. package/lib/gitnexus/install.js +228 -0
  44. package/lib/gitnexus/install.js.map +1 -0
  45. package/lib/gitnexus/languages.d.ts +80 -0
  46. package/lib/gitnexus/languages.d.ts.map +1 -0
  47. package/lib/gitnexus/languages.js +297 -0
  48. package/lib/gitnexus/languages.js.map +1 -0
  49. package/lib/gitnexus/root.d.ts +74 -0
  50. package/lib/gitnexus/root.d.ts.map +1 -0
  51. package/lib/gitnexus/root.js +168 -0
  52. package/lib/gitnexus/root.js.map +1 -0
  53. package/lib/gitnexus/scan.d.ts +68 -0
  54. package/lib/gitnexus/scan.d.ts.map +1 -0
  55. package/lib/gitnexus/scan.js +249 -0
  56. package/lib/gitnexus/scan.js.map +1 -0
  57. package/lib/gitnexus/search.d.ts +167 -0
  58. package/lib/gitnexus/search.d.ts.map +1 -0
  59. package/lib/gitnexus/search.js +517 -0
  60. package/lib/gitnexus/search.js.map +1 -0
  61. package/lib/gitnexus/toolchain.d.ts +136 -0
  62. package/lib/gitnexus/toolchain.d.ts.map +1 -0
  63. package/lib/gitnexus/toolchain.js +251 -0
  64. package/lib/gitnexus/toolchain.js.map +1 -0
  65. package/lib/index.d.ts +98 -0
  66. package/lib/index.d.ts.map +1 -0
  67. package/lib/index.js +252 -0
  68. package/lib/index.js.map +1 -0
  69. package/lib/routes/analyze.d.ts +41 -0
  70. package/lib/routes/analyze.d.ts.map +1 -0
  71. package/lib/routes/analyze.js +75 -0
  72. package/lib/routes/analyze.js.map +1 -0
  73. package/lib/routes/coverage.d.ts +52 -0
  74. package/lib/routes/coverage.d.ts.map +1 -0
  75. package/lib/routes/coverage.js +81 -0
  76. package/lib/routes/coverage.js.map +1 -0
  77. package/lib/routes/guard.d.ts +92 -0
  78. package/lib/routes/guard.d.ts.map +1 -0
  79. package/lib/routes/guard.js +187 -0
  80. package/lib/routes/guard.js.map +1 -0
  81. package/lib/routes/install.d.ts +27 -0
  82. package/lib/routes/install.d.ts.map +1 -0
  83. package/lib/routes/install.js +46 -0
  84. package/lib/routes/install.js.map +1 -0
  85. package/lib/routes/search.d.ts +27 -0
  86. package/lib/routes/search.d.ts.map +1 -0
  87. package/lib/routes/search.js +77 -0
  88. package/lib/routes/search.js.map +1 -0
  89. package/package.json +87 -4
  90. package/scripts/link-runtime.mjs +146 -0
package/lib/index.js ADDED
@@ -0,0 +1,252 @@
1
+ /**
2
+ * dsh-gitnexus-plus —host half.
3
+ *
4
+ * Registers one read-only tool and one loopback HTTP route over the coverage
5
+ * engine in `./gitnexus/`. Both answer the same question —"what did this index
6
+ * leave out?" —for the two callers that need it: the model, and a browser half
7
+ * that will render it.
8
+ *
9
+ * The read path is still never allowed to change anything: a status check must not
10
+ * start a 30-second rebuild. The two mutating capabilities are separate routes with
11
+ * their own guards, and both go through the host's shell executor rather than
12
+ * spawning from the read path. Keyword search is a third read-only route over the
13
+ * graph itself; the tool above stays read-only.
14
+ */
15
+ import { fileURLToPath } from 'node:url';
16
+ import Schema from '@deepseek-ai/schemastery';
17
+ import { defineTool } from '@deepseek-ai/dsh-tools';
18
+ import { analyzeCoverage } from './gitnexus/coverage.js';
19
+ import { formatReport } from './gitnexus/format.js';
20
+ import { commandRationale, defaultGitnexusHome, effectiveSkipFts, maintenanceCommands, } from './gitnexus/commands.js';
21
+ import { mountCoverageRoute } from './routes/coverage.js';
22
+ import { mountAnalyzeRoute } from './routes/analyze.js';
23
+ import { mountInstallRoute } from './routes/install.js';
24
+ import { mountSearchRoute } from './routes/search.js';
25
+ import { searchWithFallback } from './gitnexus/search.js';
26
+ import { createAnalyzeRunner } from './gitnexus/analyze.js';
27
+ import { createInstallRunner, defaultInstallPrefix } from './gitnexus/install.js';
28
+ /**
29
+ * Whether a service resolved from the context has the shape the runner needs.
30
+ *
31
+ * `ctx.get` is untyped here —the DSH packages ship no declarations, so the service
32
+ * arrives as `unknown`. Checking the two methods the runner actually calls is the
33
+ * narrowest claim that can be made: asserting the full `ctx.shell` contract from this
34
+ * side would be a guess, and a wrong guess fails at the first click rather than here.
35
+ */
36
+ function isShellLike(value) {
37
+ if (value === null || typeof value !== 'object')
38
+ return false;
39
+ const candidate = value;
40
+ return typeof candidate.resolve === 'function' && typeof candidate.execute === 'function';
41
+ }
42
+ export const name = 'gitnexus-plus';
43
+ /**
44
+ * Only `tools` is required. `webServer` is reached through `ctx.inject` because a
45
+ * headless profile has no HTTP carrier at all —naming it here would leave the
46
+ * plugin permanently pending there, and the tool would never register.
47
+ */
48
+ export const inject = ['tools'];
49
+ export const Config = Schema.object({
50
+ scanDefaultCapKb: Schema.natural().default(512),
51
+ minifiedSampleLimit: Schema.natural().default(40),
52
+ serveRoutes: Schema.boolean().default(true),
53
+ skipFts: Schema.boolean().default(false),
54
+ memoryOff: Schema.boolean().default(true),
55
+ analyzeHeapMb: Schema.natural().default(8192),
56
+ analyzeTimeoutMs: Schema.natural().default(600000),
57
+ gitnexusInstallDir: Schema.string().default(''),
58
+ installTimeoutMs: Schema.natural().default(1200000),
59
+ gitnexusHome: Schema.string().default(''),
60
+ });
61
+ const configShapeMatches = true;
62
+ void configShapeMatches;
63
+ export function apply(ctx, config) {
64
+ const cache = new Map();
65
+ const CACHE_TTL_MS = 15_000;
66
+ /**
67
+ * The one install location, resolved once and shared by every path that needs to
68
+ * know it. This was the broken link in "install, then initialize": the installer
69
+ * wrote a plugin-owned tree under DSH_HOME, while the coverage engine and the
70
+ * analyze runner probed only PATH and the declared dependency. On a machine with
71
+ * no global GitNexus the install succeeded and then *nothing used it* — the panel
72
+ * still reported "absent" and the refresh still said "no CLI". Passing the same
73
+ * prefix to all three (reader, language detector, runner) is what makes the
74
+ * install a prerequisite rather than an isolated side effect.
75
+ */
76
+ const installPrefix = config.gitnexusInstallDir === '' ? defaultInstallPrefix() : config.gitnexusInstallDir;
77
+ const managedToolchain = { managedPrefix: installPrefix };
78
+ // Resolved at boot so a search never depends on the host's working directory. The
79
+ // file is run by the system Node (see `searchViaHelper`); this process only names it.
80
+ const searchHelperPath = fileURLToPath(new URL('./bin/search-helper.js', import.meta.url));
81
+ /**
82
+ * Resolve a coverage report, from cache when it is recent enough.
83
+ *
84
+ * Scan results are pure functions of the working tree, so a short TTL removes the
85
+ * repeated cost of a 28k-file walk without ever showing a stale verdict to a caller
86
+ * who just changed something.
87
+ *
88
+ * `fresh` bypasses the TTL. A manual refresh is an explicit instruction from a
89
+ * person who believes the numbers may have moved, and answering it with a cached
90
+ * copy makes the control look broken —which is exactly how it was reported. A
91
+ * background poll is the case the TTL exists for; a click is not.
92
+ */
93
+ const coverageFor = async (repoPath, options = {}) => {
94
+ const key = String(repoPath);
95
+ if (options.fresh !== true) {
96
+ const hit = cache.get(key);
97
+ if (hit !== undefined && Date.now() - hit.at < CACHE_TTL_MS)
98
+ return hit.report;
99
+ }
100
+ const report = await analyzeCoverage(key, {
101
+ minifiedSampleLimit: config.minifiedSampleLimit,
102
+ toolchain: managedToolchain,
103
+ });
104
+ cache.set(key, { at: Date.now(), report });
105
+ return report;
106
+ };
107
+ /**
108
+ * The maintenance commands, resolved at the boundary rather than inside
109
+ * `analyzeCoverage`: they depend on this plugin's config, and the report is a
110
+ * pure function of the repository.
111
+ */
112
+ const maintenanceFor = (repoPath) => {
113
+ const options = {
114
+ // `skipFts: false` means automatic, not "attempt FTS": if the index has no FTS,
115
+ // attempting the install dies with `spawn EPERM` in a confined host.
116
+ skipFts: effectiveSkipFts(repoPath, config.skipFts),
117
+ memoryOff: config.memoryOff,
118
+ // Default to a plugin-owned home inside the project so the global registry
119
+ // write stays within the workspace. The explicit config still wins.
120
+ gitnexusHome: config.gitnexusHome === '' ? defaultGitnexusHome(repoPath) : config.gitnexusHome,
121
+ };
122
+ return {
123
+ commands: maintenanceCommands(repoPath, options),
124
+ rationale: commandRationale(options),
125
+ supportsForce: true,
126
+ };
127
+ };
128
+ ctx.tools.register(defineTool({
129
+ name: 'gitnexus_coverage',
130
+ description: 'Report what a GitNexus index omitted for a repository: files skipped for exceeding the ' +
131
+ 'size cap, oversized generated/minified output that inflates a language share, and files ' +
132
+ 'a fresh scan would select but the index does not cover. Read-only; never triggers a ' +
133
+ 're-index. Use it to judge how much of a repository the index actually represents before ' +
134
+ 'trusting graph queries.',
135
+ parameters: {
136
+ repo: {
137
+ type: 'string',
138
+ required: true,
139
+ description: 'Absolute path to the repository root (the directory containing .gitnexus).',
140
+ },
141
+ includeLanguages: {
142
+ type: 'boolean',
143
+ description: 'Include the per-language byte breakdown in the result. Defaults to true.',
144
+ },
145
+ },
146
+ output: {
147
+ schema: {
148
+ type: 'object',
149
+ additionalProperties: true,
150
+ properties: {
151
+ // `required: true` per property, never `required: [...]`. The DSL's
152
+ // compiler rejects the array form outright —it collects these marks
153
+ // into the JSON Schema `required` list itself.
154
+ text: { type: 'string', required: true },
155
+ indexed: { type: 'boolean', required: true },
156
+ largeFileCount: { type: 'integer', required: true },
157
+ largeBytes: { type: 'integer', required: true },
158
+ warnings: { type: 'array' },
159
+ },
160
+ },
161
+ render: (_args, value) => [{ type: 'text', text: value.text }],
162
+ },
163
+ async execute(args) {
164
+ const report = await coverageFor(args.repo);
165
+ return {
166
+ text: formatReport(report, args.includeLanguages !== false),
167
+ indexed: report.repo.indexed,
168
+ largeFileCount: report.totals.large,
169
+ largeBytes: report.totals.largeBytes,
170
+ warnings: report.warnings.map((w) => ({ level: w.level, code: w.code, message: w.message })),
171
+ };
172
+ },
173
+ presentCall: (args) => ({
174
+ card: 'generic',
175
+ title: `GitNexus coverage 路 ${String(args.repo).split(/[\\/]/).pop()}`,
176
+ kind: 'read',
177
+ }),
178
+ }));
179
+ if (config.serveRoutes) {
180
+ // Optional carrier: absent on headless profiles, so the tool above still works.
181
+ ctx.inject(['webServer'], (svcCtx) => {
182
+ // Read through `ctx.get` rather than injecting, because `shell` is optional here
183
+ // and `inject` would make the whole route block wait for a service a headless
184
+ // deployment never provides. `null` is handed to the runner, which reports
185
+ // `unavailable` and tells the operator to run the command themselves —the
186
+ // read-only half of the panel keeps working either way.
187
+ const shellRaw = svcCtx.get('shell');
188
+ const searchShell = isShellLike(shellRaw) ? shellRaw : null;
189
+ const analyze = createAnalyzeRunner({
190
+ shell: searchShell,
191
+ timeoutMs: config.analyzeTimeoutMs,
192
+ toolchain: managedToolchain,
193
+ // The runner must read the same config as the command text it confirms,
194
+ // otherwise the dialog describes a command the run does not execute. It
195
+ // resolves `false` per repository (automatic), and defaults to the
196
+ // project-local `GITNEXUS_HOME` when the config leaves it empty.
197
+ skipFts: config.skipFts,
198
+ gitnexusHome: config.gitnexusHome === '' ? null : config.gitnexusHome,
199
+ });
200
+ // GitNexus is what produces the index this plugin reads, so a machine without it
201
+ // has nothing to display and no way to change that from here. This runner is the
202
+ // way out: it installs a plugin-owned copy so the operator does not have to work
203
+ // out a package manager, a registry, or a global prefix themselves.
204
+ const install = createInstallRunner({
205
+ shell: isShellLike(shellRaw) ? shellRaw : null,
206
+ prefix: installPrefix,
207
+ timeoutMs: config.installTimeoutMs,
208
+ });
209
+ const disposers = [
210
+ mountCoverageRoute(svcCtx.webServer, {
211
+ get: (repoPath, options) => coverageFor(repoPath, options),
212
+ maintenance: (repoPath) => maintenanceFor(repoPath),
213
+ }),
214
+ mountAnalyzeRoute(svcCtx.webServer, {
215
+ start: (repoPath, options) => analyze.start(repoPath, options),
216
+ status: (repoPath) => analyze.status(repoPath),
217
+ cancel: (repoPath) => analyze.cancel(repoPath),
218
+ }),
219
+ mountInstallRoute(svcCtx.webServer, {
220
+ start: () => install.start(),
221
+ status: () => install.status(),
222
+ cancel: () => install.cancel(),
223
+ cliPath: () => install.cliPath(),
224
+ }),
225
+ mountSearchRoute(svcCtx.webServer, {
226
+ search: (repoPath, query, options) => searchWithFallback(repoPath, query, {
227
+ shell: searchShell,
228
+ helperPath: searchHelperPath,
229
+ limit: options?.limit,
230
+ managedPrefix: installPrefix,
231
+ }),
232
+ }),
233
+ ];
234
+ ctx.effect(() => () => {
235
+ for (const dispose of disposers)
236
+ dispose();
237
+ // Abort before clearing, so a run in flight is stopped rather than orphaned
238
+ // holding a handle onto a server that no longer exists.
239
+ analyze.dispose();
240
+ install.dispose();
241
+ cache.clear();
242
+ }, 'gitnexus-plus: HTTP routes');
243
+ });
244
+ }
245
+ }
246
+ /**
247
+ * Human-readable rendering lives in `./gitnexus/format.js` —free of DSH imports
248
+ * so it can be tested without a host. Re-exported here for callers that already
249
+ * hold the plugin module.
250
+ */
251
+ export { formatReport } from './gitnexus/format.js';
252
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The one endpoint that changes something.
3
+ *
4
+ * Everything else this plugin serves is a reading of the working tree; this route
5
+ * starts `gitnexus analyze --index-only` through the host's shell executor. It is a
6
+ * separate module from `coverage.ts` rather than another branch inside it, so the
7
+ * read-only half keeps a guard set that cannot mutate and this half can be reviewed
8
+ * on its own.
9
+ *
10
+ * THREE METHODS, THREE MEANINGS
11
+ * -----------------------------
12
+ * `POST` starts a run, `GET` reports where it got to, `DELETE` asks it to stop. They
13
+ * are one endpoint because the run is one resource and the panel polls the same URL it
14
+ * started, which removes the id bookkeeping a separate status route would need.
15
+ *
16
+ * WHAT THIS ROUTE DOES NOT DO
17
+ * ---------------------------
18
+ * It does not escalate. The runner never sets `sandboxPolicy`, so the process is
19
+ * confined exactly as the deployment confines every other command. A refusal comes
20
+ * back as `denied` and is not retried wider — see `gitnexus/analyze.ts`, which is
21
+ * where that decision lives and is tested.
22
+ */
23
+ import type { WebServerService } from '@deepseek-ai/cordis';
24
+ import type { AnalyzeStatus } from '../gitnexus/analyze.js';
25
+ export interface AnalyzeRouteDeps {
26
+ /**
27
+ * Begin a run, or return the one already in flight for this repository.
28
+ * `force` selects the destructive full rebuild; it is passed through, never
29
+ * inferred from the method or the route.
30
+ */
31
+ start: (repoPath: string, options?: {
32
+ force?: boolean;
33
+ }) => AnalyzeStatus;
34
+ status: (repoPath: string) => AnalyzeStatus | null;
35
+ cancel: (repoPath: string) => boolean;
36
+ }
37
+ /**
38
+ * @returns disposer that unregisters the route
39
+ */
40
+ export declare function mountAnalyzeRoute(webServer: WebServerService, deps: AnalyzeRouteDeps): () => void;
41
+ //# sourceMappingURL=analyze.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/routes/analyze.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAQ5D,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,aAAa,CAAC;IAC1E,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,aAAa,GAAG,IAAI,CAAC;IACnD,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;CACvC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,gBAAgB,EAAE,IAAI,EAAE,gBAAgB,GAAG,MAAM,IAAI,CA2CjG"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The one endpoint that changes something.
3
+ *
4
+ * Everything else this plugin serves is a reading of the working tree; this route
5
+ * starts `gitnexus analyze --index-only` through the host's shell executor. It is a
6
+ * separate module from `coverage.ts` rather than another branch inside it, so the
7
+ * read-only half keeps a guard set that cannot mutate and this half can be reviewed
8
+ * on its own.
9
+ *
10
+ * THREE METHODS, THREE MEANINGS
11
+ * -----------------------------
12
+ * `POST` starts a run, `GET` reports where it got to, `DELETE` asks it to stop. They
13
+ * are one endpoint because the run is one resource and the panel polls the same URL it
14
+ * started, which removes the id bookkeeping a separate status route would need.
15
+ *
16
+ * WHAT THIS ROUTE DOES NOT DO
17
+ * ---------------------------
18
+ * It does not escalate. The runner never sets `sandboxPolicy`, so the process is
19
+ * confined exactly as the deployment confines every other command. A refusal comes
20
+ * back as `denied` and is not retried wider — see `gitnexus/analyze.ts`, which is
21
+ * where that decision lives and is tested.
22
+ */
23
+ import path from 'node:path';
24
+ import { guardHandler, passGuard, passWriteGuard, answerHead, sendJson } from './guard.js';
25
+ /**
26
+ * The methods this route answers. A wider set than the coverage route's on purpose:
27
+ * the difference between the two routes *is* their method policy.
28
+ */
29
+ const ANALYZE_METHODS = new Set(['GET', 'HEAD', 'POST', 'DELETE']);
30
+ /**
31
+ * @returns disposer that unregisters the route
32
+ */
33
+ export function mountAnalyzeRoute(webServer, deps) {
34
+ const handler = guardHandler(async (req, res) => {
35
+ if (!passGuard(req, res, ANALYZE_METHODS))
36
+ return;
37
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
38
+ const repo = url.searchParams.get('repo');
39
+ if (repo === null || repo.trim() === '') {
40
+ sendJson(res, 400, { ok: false, code: 'missing-repo', message: 'repo query parameter is required' });
41
+ return;
42
+ }
43
+ const absRepo = path.resolve(repo);
44
+ if (answerHead(req, res))
45
+ return;
46
+ if (req.method === 'POST') {
47
+ // The only mutating verb here, so the only one that takes the write guard. A
48
+ // cross-origin page can reach loopback; `Origin` is what separates it from the
49
+ // desktop shell's own fetch.
50
+ if (!passWriteGuard(req, res))
51
+ return;
52
+ // `force=1` is the full rebuild. It is an explicit query flag rather than a
53
+ // body field because the panel's only other POST parameter is the repo, and a
54
+ // flag in the URL is visible in the run's own command and in a log.
55
+ const force = url.searchParams.get('force') === '1';
56
+ sendJson(res, 200, { ok: true, run: deps.start(absRepo, { force }) });
57
+ return;
58
+ }
59
+ if (req.method === 'DELETE') {
60
+ if (!passWriteGuard(req, res))
61
+ return;
62
+ const cancelled = deps.cancel(absRepo);
63
+ // Cancelling nothing is not an error worth a 404: the caller's intent — "this
64
+ // repository should not be analysing" — already holds.
65
+ sendJson(res, 200, { ok: true, cancelled, run: deps.status(absRepo) });
66
+ return;
67
+ }
68
+ const run = deps.status(absRepo);
69
+ // `null` means nothing has run here in this host's lifetime, which is a state the
70
+ // panel renders ("尚未运行"), not a failure.
71
+ sendJson(res, 200, { ok: true, run });
72
+ });
73
+ return webServer.register({ kind: 'prefix', path: '/api/gitnexus/analyze', handler });
74
+ }
75
+ //# sourceMappingURL=analyze.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze.js","sourceRoot":"","sources":["../../src/routes/analyze.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI3F;;;GAGG;AACH,MAAM,eAAe,GAAwB,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;AAaxF;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAA2B,EAAE,IAAsB;IACnF,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QAC9C,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,eAAe,CAAC;YAAE,OAAO;QAElD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC,CAAC;QACxD,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC1C,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACxC,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,kCAAkC,EAAE,CAAC,CAAC;YACrG,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAEnC,IAAI,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;YAAE,OAAO;QAEjC,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC1B,6EAA6E;YAC7E,+EAA+E;YAC/E,6BAA6B;YAC7B,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC;gBAAE,OAAO;YACtC,4EAA4E;YAC5E,8EAA8E;YAC9E,oEAAoE;YACpE,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC;YACpD,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;YACtE,OAAO;QACT,CAAC;QAED,IAAI,GAAG,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC5B,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC;gBAAE,OAAO;YACtC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACvC,8EAA8E;YAC9E,uDAAuD;YACvD,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACvE,OAAO;QACT,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACjC,kFAAkF;QAClF,yCAAyC;QACzC,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,OAAO,SAAS,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,CAAC,CAAC;AACxF,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `GET /api/gitnexus/coverage?repo=<project>` — the panel's data source.
3
+ *
4
+ * Route shape follows the community convention (`/api/<plugin-slug>/…`) and the
5
+ * carrier contract in `@deepseek-ai/dsh-host-webserver`: bare `node:http`, no
6
+ * Express, exact-then-longest-prefix matching, and a disposer for teardown.
7
+ *
8
+ * THE BOUND IS "IS A PROJECT ROOT", NOT "IS IN THE REGISTRY"
9
+ * ----------------------------------------------------------
10
+ * An earlier revision accepted only paths present in `~/.gitnexus/registry.json`.
11
+ * That was wrong for the panel's actual job: it shows the project the user is
12
+ * *in*, and a working directory the global registry has never heard of — a fresh
13
+ * clone, an index built with `GITNEXUS_HOME` redirected — is exactly the case the
14
+ * panel exists to report on. Refusing it would have been a correctness bug wearing
15
+ * a security costume.
16
+ *
17
+ * A later revision required an index. That was also wrong, for the same reason at a
18
+ * different state: a project with no index yet still has a readable working tree,
19
+ * and the engine already builds a useful scan-only report for it. The bound is now a
20
+ * project root (`.gitnexus` or `.git`), which still refuses arbitrary directories.
21
+ *
22
+ * The route still refuses to describe arbitrary directories: the path must be a
23
+ * project root — carrying `.gitnexus/gitnexus.json`, `.gitnexus/meta.json`, or a
24
+ * `.git` marker — so a caller cannot use it to enumerate a filesystem. An index is
25
+ * no longer required: the engine's scan-only report is what makes the "no index
26
+ * yet" state informative instead of an empty command. Loopback and method guards
27
+ * live in `./guard.js`.
28
+ */
29
+ import type { WebServerService } from '@deepseek-ai/cordis';
30
+ import type { CoverageReport } from '../gitnexus/coverage.js';
31
+ import type { MaintenanceInfo } from '../index.js';
32
+ /**
33
+ * What the route needs from the host half.
34
+ *
35
+ * Injected rather than imported so the route stays testable without a GitNexus
36
+ * install: the engine arrives as two functions, and a test supplies stubs.
37
+ */
38
+ export interface CoverageRouteDeps {
39
+ /**
40
+ * `fresh` asks the engine to skip its short-lived cache. A manual refresh carries
41
+ * it; a background poll does not, which is the case the cache exists for.
42
+ */
43
+ get: (repoPath: string, options?: {
44
+ fresh?: boolean;
45
+ }) => Promise<CoverageReport>;
46
+ maintenance?: (repoPath: string) => MaintenanceInfo;
47
+ }
48
+ /**
49
+ * @returns disposer that unregisters the route
50
+ */
51
+ export declare function mountCoverageRoute(webServer: WebServerService, deps: CoverageRouteDeps): () => void;
52
+ //# sourceMappingURL=coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../src/routes/coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,GAAG,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAClF,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,eAAe,CAAC;CACrD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,gBAAgB,EAAE,IAAI,EAAE,iBAAiB,GAAG,MAAM,IAAI,CAgDnG"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `GET /api/gitnexus/coverage?repo=<project>` — the panel's data source.
3
+ *
4
+ * Route shape follows the community convention (`/api/<plugin-slug>/…`) and the
5
+ * carrier contract in `@deepseek-ai/dsh-host-webserver`: bare `node:http`, no
6
+ * Express, exact-then-longest-prefix matching, and a disposer for teardown.
7
+ *
8
+ * THE BOUND IS "IS A PROJECT ROOT", NOT "IS IN THE REGISTRY"
9
+ * ----------------------------------------------------------
10
+ * An earlier revision accepted only paths present in `~/.gitnexus/registry.json`.
11
+ * That was wrong for the panel's actual job: it shows the project the user is
12
+ * *in*, and a working directory the global registry has never heard of — a fresh
13
+ * clone, an index built with `GITNEXUS_HOME` redirected — is exactly the case the
14
+ * panel exists to report on. Refusing it would have been a correctness bug wearing
15
+ * a security costume.
16
+ *
17
+ * A later revision required an index. That was also wrong, for the same reason at a
18
+ * different state: a project with no index yet still has a readable working tree,
19
+ * and the engine already builds a useful scan-only report for it. The bound is now a
20
+ * project root (`.gitnexus` or `.git`), which still refuses arbitrary directories.
21
+ *
22
+ * The route still refuses to describe arbitrary directories: the path must be a
23
+ * project root — carrying `.gitnexus/gitnexus.json`, `.gitnexus/meta.json`, or a
24
+ * `.git` marker — so a caller cannot use it to enumerate a filesystem. An index is
25
+ * no longer required: the engine's scan-only report is what makes the "no index
26
+ * yet" state informative instead of an empty command. Loopback and method guards
27
+ * live in `./guard.js`.
28
+ */
29
+ import path from 'node:path';
30
+ import { isProjectRoot } from '../gitnexus/index-meta.js';
31
+ import { guardHandler, passGuard, answerHead, sendJson } from './guard.js';
32
+ /**
33
+ * @returns disposer that unregisters the route
34
+ */
35
+ export function mountCoverageRoute(webServer, deps) {
36
+ const handler = guardHandler(async (req, res) => {
37
+ if (!passGuard(req, res))
38
+ return;
39
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
40
+ const repo = url.searchParams.get('repo');
41
+ if (repo === null || repo.trim() === '') {
42
+ sendJson(res, 400, {
43
+ ok: false,
44
+ code: 'missing-repo',
45
+ message: 'repo query parameter is required',
46
+ });
47
+ return;
48
+ }
49
+ const absRepo = path.resolve(repo);
50
+ // The bound changed from "is a GitNexus index" to "is a project root": a
51
+ // directory with neither an index nor a `.git` marker is still refused, so the
52
+ // route cannot be used to enumerate a filesystem. A git repository without an
53
+ // index is the state the panel's initialize action exists for, and the engine
54
+ // already produces a useful scan-only report for it (file counts, bytes,
55
+ // languages, the size cap). Refusing that state was hiding data, not protecting
56
+ // anything — the route only answers loopback callers.
57
+ if (!(await isProjectRoot(absRepo))) {
58
+ sendJson(res, 404, {
59
+ ok: false,
60
+ code: 'not-a-project',
61
+ message: 'directory is neither a GitNexus index nor a git repository root',
62
+ });
63
+ return;
64
+ }
65
+ if (answerHead(req, res))
66
+ return;
67
+ // `refresh=1` comes from the panel's manual refresh control. A background poll
68
+ // omits it and gets the cached copy, which is what the TTL is for.
69
+ const fresh = url.searchParams.get('refresh') === '1';
70
+ const report = await deps.get(absRepo, { fresh });
71
+ // The maintenance commands ride along with the report because they are
72
+ // config-derived and only meaningful next to the index they would rebuild.
73
+ const maintenance = typeof deps.maintenance === 'function' ? deps.maintenance(absRepo) : null;
74
+ // `readAt` is what makes a refresh visible: without it two reads of an unchanged
75
+ // repository are byte-identical on screen, so the control looks inert even when
76
+ // it did exactly what it was asked.
77
+ sendJson(res, 200, { ok: true, report, maintenance, readAt: new Date().toISOString() });
78
+ });
79
+ return webServer.register({ kind: 'prefix', path: '/api/gitnexus/coverage', handler });
80
+ }
81
+ //# sourceMappingURL=coverage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../src/routes/coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAoB3E;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAA2B,EAAE,IAAuB;IACrF,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QAC9C,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC;YAAE,OAAO;QAEjC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC,CAAC;QACxD,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC1C,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACxC,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE;gBACjB,EAAE,EAAE,KAAK;gBACT,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,kCAAkC;aAC5C,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACnC,yEAAyE;QACzE,+EAA+E;QAC/E,8EAA8E;QAC9E,8EAA8E;QAC9E,yEAAyE;QACzE,gFAAgF;QAChF,sDAAsD;QACtD,IAAI,CAAC,CAAC,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YACpC,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE;gBACjB,EAAE,EAAE,KAAK;gBACT,IAAI,EAAE,eAAe;gBACrB,OAAO,EAAE,iEAAiE;aAC3E,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,IAAI,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;YAAE,OAAO;QAEjC,+EAA+E;QAC/E,mEAAmE;QACnE,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC;QACtD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAClD,uEAAuE;QACvE,2EAA2E;QAC3E,MAAM,WAAW,GAAG,OAAO,IAAI,CAAC,WAAW,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9F,iFAAiF;QACjF,gFAAgF;QAChF,oCAAoC;QACpC,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,OAAO,SAAS,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,CAAC,CAAC;AACzF,CAAC"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Shared guards for this plugin's HTTP routes.
3
+ *
4
+ * Extracted because two routes now need identical treatment, and a guard that is
5
+ * copy-pasted stops being a guard the first time one copy is edited.
6
+ *
7
+ * WHY THE GUARD EXISTS AT ALL
8
+ * ---------------------------
9
+ * On a bare `dsh web` profile a third-party named route is unauthenticated: the
10
+ * carrier owns no TLS, authentication, or Origin policy, and the Host/Origin
11
+ * checks that do exist belong to `client/connection` and cover only the routes it
12
+ * registers itself. The Desktop carrier adds its own gate (an
13
+ * `x-dsh-desktop-renderer` token; unauthenticated requests get 403), but a route
14
+ * that is safe only on one carrier is not safe.
15
+ */
16
+ import type { IncomingMessage, ServerResponse } from 'node:http';
17
+ import type { RequestHandler } from '@deepseek-ai/cordis';
18
+ /** This plugin only ever answers reads. */
19
+ export declare const ALLOWED_METHODS: Set<string>;
20
+ /**
21
+ * @param {import('node:http').IncomingMessage} req
22
+ * @returns {boolean} whether the peer is this machine
23
+ */
24
+ export declare function isLoopback(req: IncomingMessage): boolean;
25
+ /**
26
+ * @param {import('node:http').ServerResponse} res
27
+ * @param {number} status
28
+ * @param {unknown} body
29
+ * @param {Record<string, string>} [headers]
30
+ */
31
+ export declare function sendJson(res: ServerResponse, status: number, body: unknown, headers?: Record<string, string>): void;
32
+ /**
33
+ * Run the shared pre-checks, answering the request when one fails.
34
+ *
35
+ * `methods` exists because the default set is the read-only one: the coverage route
36
+ * answers GET and HEAD and nothing else, and giving it POST would widen it by accident.
37
+ * A route that legitimately mutates passes its own set, so the method policy is stated
38
+ * at the route rather than inferred from what the guard happens to allow.
39
+ *
40
+ * @param {import('node:http').IncomingMessage} req
41
+ * @param {import('node:http').ServerResponse} res
42
+ * @param methods the methods this route accepts
43
+ * @returns `true` when the caller may continue handling the request
44
+ */
45
+ export declare function passGuard(req: IncomingMessage, res: ServerResponse, methods?: ReadonlySet<string>): boolean;
46
+ /**
47
+ * Answer a HEAD request with headers only, or report that the caller should
48
+ * continue and send the body.
49
+ *
50
+ * @returns `true` when the request was fully answered
51
+ */
52
+ export declare function answerHead(req: IncomingMessage, res: ServerResponse): boolean;
53
+ /**
54
+ * Wrap a handler so no failure escapes as an unhandled rejection on the carrier.
55
+ *
56
+ * `Promise.resolve` around the call is not decoration: `RequestHandler` may return
57
+ * `void`, and calling `.catch` on `void` is a runtime `TypeError`. The shape the
58
+ * carrier actually uses is "sync throw or async rejection", and only the resolved
59
+ * form covers both.
60
+ */
61
+ export declare function guardHandler(handler: RequestHandler): RequestHandler;
62
+ /**
63
+ * Extra guard for routes that *change* something.
64
+ *
65
+ * The loopback check alone is not enough for a mutation: any page the operator
66
+ * visits can issue a request to `127.0.0.1`, and a cross-origin `POST` needs no
67
+ * preflight when it uses a simple content type. `Origin` is the discriminator —
68
+ * a malicious page always sends its own origin, and it will not match this host.
69
+ *
70
+ * A request with NO `Origin` is allowed through, and that is deliberate rather
71
+ * than lazy: a same-origin `fetch` from the desktop shell, and any local tool
72
+ * (including this repository's own tests), legitimately send none. The remaining
73
+ * gates — loopback peer, and the Desktop carrier's renderer token — still apply,
74
+ * so an absent header is not by itself a way in.
75
+ *
76
+ * @param {import('node:http').IncomingMessage} req
77
+ * @param {import('node:http').ServerResponse} res
78
+ * @returns {boolean} `true` when the caller may proceed
79
+ */
80
+ export declare function passWriteGuard(req: IncomingMessage, res: ServerResponse): boolean;
81
+ /**
82
+ * Read a JSON body with a hard byte cap.
83
+ *
84
+ * Bounded because the cap is the only thing standing between a malformed client
85
+ * and unbounded host memory — and this plugin's bodies are at most a few fields.
86
+ *
87
+ * @param {import('node:http').IncomingMessage} req
88
+ * @param {number} [maxBytes]
89
+ * @returns {Promise<Record<string, unknown>>}
90
+ */
91
+ export declare function readJsonBody(req: IncomingMessage, maxBytes?: number): Promise<unknown>;
92
+ //# sourceMappingURL=guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/routes/guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,2CAA2C;AAC3C,eAAO,MAAM,eAAe,aAA2B,CAAC;AAExD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CASxD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,QAShH;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,OAAO,GAAE,WAAW,CAAC,MAAM,CAAmB,GAC7C,OAAO,CAUT;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAM7E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,CAcpE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAmBjF;AAED;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,eAAe,EAAE,QAAQ,SAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CA4B9F"}