@rahmantech/knl-view 0.1.0 → 0.3.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 +133 -32
- package/bin/knl-view.js +591 -58
- package/dist/assets/{abnfDiagram-WEYOIA3C-B1gaBaSI.js → abnfDiagram-WEYOIA3C-hoSbsMOz.js} +1 -1
- package/dist/assets/{arc-NZ9uRaCl.js → arc-g87Np6E5.js} +1 -1
- package/dist/assets/{architecture-WOLXFQ4H-8MalLwTd.js → architecture-WOLXFQ4H-DOIq-0IA.js} +1 -1
- package/dist/assets/{architectureDiagram-ZHVYKT53-eZ8i5cl4.js → architectureDiagram-ZHVYKT53-Dxwe8n7m.js} +1 -1
- package/dist/assets/{blockDiagram-RJAE7QA3-BavOE8BM.js → blockDiagram-RJAE7QA3-BnGng59Z.js} +1 -1
- package/dist/assets/{c4Diagram-XDPPPE54-DqMaTAzx.js → c4Diagram-XDPPPE54-C5LjqIIo.js} +1 -1
- package/dist/assets/channel-BVWkRFBw.js +1 -0
- package/dist/assets/{chunk-2E7WHVMD-B1Hj2BjJ.js → chunk-2E7WHVMD-BJ1dyGLq.js} +1 -1
- package/dist/assets/{chunk-4EMNKR2T-HDscrCiq.js → chunk-4EMNKR2T-DXF02Sgw.js} +1 -1
- package/dist/assets/{chunk-55XCU76K-QR_ARj8I.js → chunk-55XCU76K-CM1akAbu.js} +1 -1
- package/dist/assets/{chunk-7S6XSTFO-B0p3o547.js → chunk-7S6XSTFO-BOUjqldL.js} +1 -1
- package/dist/assets/{chunk-AQDTSKZB-CCxwNyyA.js → chunk-AQDTSKZB-C4isdNLA.js} +1 -1
- package/dist/assets/{chunk-DSNLSHJZ-BM5PBFmz.js → chunk-DSNLSHJZ-CbOTkSVM.js} +1 -1
- package/dist/assets/{chunk-HAGFOTED-UCDVFuN_.js → chunk-HAGFOTED-CRfnketR.js} +1 -1
- package/dist/assets/{chunk-HNE4Q36P-C_1sakp2.js → chunk-HNE4Q36P-rxlpGUU8.js} +1 -1
- package/dist/assets/{chunk-NQOKRVIF-b2D1Z4oK.js → chunk-NQOKRVIF-BOYCgTmV.js} +1 -1
- package/dist/assets/{chunk-UHW7PFBP-CJs0VwBY.js → chunk-UHW7PFBP-Cnogp1Fb.js} +1 -1
- package/dist/assets/{chunk-VQ5W62QI-DQxjgE4_.js → chunk-VQ5W62QI-Cu1WY5Uo.js} +1 -1
- package/dist/assets/{chunk-WQQL4CDK-DcjPeAfQ.js → chunk-WQQL4CDK-BJc3FPE2.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-glA0kB5s.js → chunk-XXDRQBXY-BlPj4H7s.js} +1 -1
- package/dist/assets/{chunk-YZO67HFG-DJqACL5V.js → chunk-YZO67HFG-V85H31x4.js} +1 -1
- package/dist/assets/{chunk-ZAUX2LTX-CWfaYtwb.js → chunk-ZAUX2LTX-C3XKXW09.js} +1 -1
- package/dist/assets/{chunk-ZCHMQSOR-BGX73Hho.js → chunk-ZCHMQSOR-B0mmpEgW.js} +1 -1
- package/dist/assets/{classDiagram-v2-4MYWR3UA-BpYXPz-e.js → classDiagram-v2-4MYWR3UA-oMd7YaHP.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-e1quKm0Q.js → cose-bilkent-JH36ORCC-J0kSVsDf.js} +1 -1
- package/dist/assets/{cynefin-EF2NZ3EQ-hb3oUmFO.js → cynefin-EF2NZ3EQ-DK2TNhel.js} +1 -1
- package/dist/assets/{cynefinDiagram-WF24UDRP-CnMQvLqi.js → cynefinDiagram-WF24UDRP-DDvKgieA.js} +1 -1
- package/dist/assets/{dagre-NK4GIZVX-BH_Y8y3F.js → dagre-NK4GIZVX-Ca2Fe-i-.js} +1 -1
- package/dist/assets/{diagram-CBK3MWTC-Dh79VetZ.js → diagram-CBK3MWTC-L6dmeQ2V.js} +1 -1
- package/dist/assets/{diagram-I5XIIV2D-D3u-o1RV.js → diagram-I5XIIV2D-DhG8R-v5.js} +1 -1
- package/dist/assets/{diagram-ODIL2UCW-Bfuo-OKS.js → diagram-ODIL2UCW-BpYYbYBT.js} +1 -1
- package/dist/assets/{diagram-PFBEJUXE-CwHasIbs.js → diagram-PFBEJUXE-DKQpHSsC.js} +1 -1
- package/dist/assets/{diagram-RJA3I4VV-D8ttmuE8.js → diagram-RJA3I4VV-DP7Dsnya.js} +1 -1
- package/dist/assets/{diagram-TYUNFSNR-CzC-8RZ0.js → diagram-TYUNFSNR-CfSFewUS.js} +1 -1
- package/dist/assets/{dist-CUJYGnnI.js → dist-CQNjnrld.js} +1 -1
- package/dist/assets/{ebnfDiagram-V4P3TTPT-9AAhU7In.js → ebnfDiagram-V4P3TTPT-DE3MZoJO.js} +1 -1
- package/dist/assets/{elk-QONTBCTQ-BgvnD1v_.js → elk-QONTBCTQ-p_aAQPHU.js} +1 -1
- package/dist/assets/{erDiagram-KYQIGQAA-RLtbiLlP.js → erDiagram-KYQIGQAA-DZO3vQrk.js} +1 -1
- package/dist/assets/{eventmodeling-K75KTNOO-DVY22vNZ.js → eventmodeling-K75KTNOO-BII9EPl5.js} +1 -1
- package/dist/assets/flowDiagram-IUGLLWBU-D9uPno39.js +1 -0
- package/dist/assets/{ganttDiagram-R7GPNTJY-hHiBGa9W.js → ganttDiagram-R7GPNTJY-BKiH3v2r.js} +1 -1
- package/dist/assets/{gitGraph-VSP46ZUC-Cvxay1wh.js → gitGraph-VSP46ZUC-oHL6cr73.js} +1 -1
- package/dist/assets/{gitGraphDiagram-PDYL7XHY-AOhlpSqg.js → gitGraphDiagram-PDYL7XHY-CqPVIEeo.js} +1 -1
- package/dist/assets/{index-CBLNQvK3.js → index-CSpirV16.js} +3 -3
- package/dist/assets/{info-OHQRW6UA-CJ8EUO1m.js → info-OHQRW6UA-Drrbnhpd.js} +1 -1
- package/dist/assets/{infoDiagram-MVDRKAX5-DOQu7oYI.js → infoDiagram-MVDRKAX5-DnQQFMAd.js} +1 -1
- package/dist/assets/{ishikawaDiagram-HPEHV2GE-DcVMvGmx.js → ishikawaDiagram-HPEHV2GE-DIpQlxRh.js} +1 -1
- package/dist/assets/{journeyDiagram-BOSHW7O7-luMeqEqz.js → journeyDiagram-BOSHW7O7-EBR9I5h1.js} +1 -1
- package/dist/assets/{kanban-definition-FNSA4HC5-KTZbmrW2.js → kanban-definition-FNSA4HC5-CdvNsjik.js} +1 -1
- package/dist/assets/{line-CmPHiOVm.js → line-CDLzgxDc.js} +1 -1
- package/dist/assets/{linear-DHYlKXBA.js → linear-DfXKql9u.js} +1 -1
- package/dist/assets/{mermaid-parser.core-BRDI-WhC.js → mermaid-parser.core-DnXGphT2.js} +3 -3
- package/dist/assets/{mermaid.core-DPTRaYLy.js → mermaid.core-h6fiCaSZ.js} +4 -4
- package/dist/assets/{mindmap-definition-6OY57UG3-70EN5Kw_.js → mindmap-definition-6OY57UG3-BiNfio-j.js} +1 -1
- package/dist/assets/{packet-JDAUHWVQ-CXWz83oS.js → packet-JDAUHWVQ-D76byv1S.js} +1 -1
- package/dist/assets/{pegDiagram-ECIUJNBX-z5UXKmp4.js → pegDiagram-ECIUJNBX-Drs8Pxdt.js} +1 -1
- package/dist/assets/{pie-XZMESJXO-rWI1HIQ7.js → pie-XZMESJXO-D_83z1lM.js} +1 -1
- package/dist/assets/{pieDiagram-GNMPY74T-Bh4Kv3NQ.js → pieDiagram-GNMPY74T-wvzIphSf.js} +1 -1
- package/dist/assets/{quadrantDiagram-D57K4MC3-wGM4Ckig.js → quadrantDiagram-D57K4MC3-B7EfMRWb.js} +1 -1
- package/dist/assets/{radar-ABXABTNO-CIJ92p5N.js → radar-ABXABTNO-Bs6dVdAR.js} +1 -1
- package/dist/assets/{railroad-I3PHUGI6-8bxua-Hf.js → railroad-I3PHUGI6-lKaN4miw.js} +1 -1
- package/dist/assets/{railroad-abnf-LNEFI6M7-B9KVI10m.js → railroad-abnf-LNEFI6M7-Bhh16b7Z.js} +1 -1
- package/dist/assets/{railroad-ebnf-DWYD2UWJ-Bt73rnyJ.js → railroad-ebnf-DWYD2UWJ-m6ybW-Y_.js} +1 -1
- package/dist/assets/{railroad-peg-JR7BVNR7-Cie82UPT.js → railroad-peg-JR7BVNR7-DNn72e7r.js} +1 -1
- package/dist/assets/{railroadDiagram-Y72NRBL7-C1nmi_tl.js → railroadDiagram-Y72NRBL7-DNeop1iZ.js} +1 -1
- package/dist/assets/{requirementDiagram-EQYSW5RA-BSianW40.js → requirementDiagram-EQYSW5RA-BVYZ2XYJ.js} +1 -1
- package/dist/assets/{sankeyDiagram-SY24JHO5-DIa11-h7.js → sankeyDiagram-SY24JHO5-B2MP51on.js} +1 -1
- package/dist/assets/{sequenceDiagram-BLEHNHQU-Df5rZKz2.js → sequenceDiagram-BLEHNHQU-BUvxstXG.js} +1 -1
- package/dist/assets/{src-dClg85lX.js → src-BrR0lqlG.js} +1 -1
- package/dist/assets/{stateDiagram-v2-A6IESKWZ-B15PTSvX.js → stateDiagram-v2-A6IESKWZ-Ba84OGKE.js} +1 -1
- package/dist/assets/{swimlanes-B3ENRKFQ-C9mgRCMi.js → swimlanes-B3ENRKFQ-BMSl2PNA.js} +1 -1
- package/dist/assets/swimlanesDiagram-AYQRV4AK-M1dmvq7m.js +8 -0
- package/dist/assets/{timeline-definition-JXYKGLGB-BDVf_mIv.js → timeline-definition-JXYKGLGB-DOdW-dxq.js} +1 -1
- package/dist/assets/{treeView-D4JQ5SDB-BbBK-25J.js → treeView-D4JQ5SDB-DZ9KQyn5.js} +1 -1
- package/dist/assets/{treemap-SAJKECNS-CEDX8apk.js → treemap-SAJKECNS-WEC5wtDV.js} +1 -1
- package/dist/assets/{usecaseDiagram-WPOLBNNX-D1QkwFF8.js → usecaseDiagram-WPOLBNNX-cldTztFL.js} +1 -1
- package/dist/assets/{vennDiagram-Q3Y3IDCW-5PeGZvEO.js → vennDiagram-Q3Y3IDCW-c41KySoY.js} +1 -1
- package/dist/assets/{wardley-7MLQ67FV-BatN94pO.js → wardley-7MLQ67FV-Bf9_aSSZ.js} +1 -1
- package/dist/assets/{wardleyDiagram-IVAIDKWA-DyCcDS3Y.js → wardleyDiagram-IVAIDKWA-BeKEvEBG.js} +1 -1
- package/dist/assets/{xychartDiagram-4URNHDU4-CbCXcRlg.js → xychartDiagram-4URNHDU4-tNkAxTxG.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +5 -1
- package/server/config.js +140 -14
- package/server/consensus.js +18 -13
- package/server/graph/modules_builder.js +290 -0
- package/server/graph/modules_catalog.js +288 -0
- package/server/graph/parser.js +3 -2
- package/server/graph/sources.js +165 -24
- package/server/graph/syntax.js +245 -28
- package/server/index.js +124 -61
- package/server/install.js +350 -10
- package/server/models_registry.js +34 -9
- package/server/scaffold.js +109 -9
- package/dist/assets/channel-BZ2_TTTJ.js +0 -1
- package/dist/assets/flowDiagram-IUGLLWBU-l7L5DTb5.js +0 -1
- package/dist/assets/swimlanesDiagram-AYQRV4AK-BGFq4t7O.js +0 -8
package/bin/knl-view.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseArgs } from 'node:util'
|
|
2
|
+
import { parseArgs, promisify } from 'node:util'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import fs from 'node:fs'
|
|
5
5
|
import readline from 'node:readline/promises'
|
|
6
6
|
import { execFile, spawn } from 'node:child_process'
|
|
7
|
-
import { promisify } from 'node:util'
|
|
8
7
|
import os from 'node:os'
|
|
9
8
|
import { fileURLToPath } from 'node:url'
|
|
10
9
|
import { createKnowledgeServer, attachAnchorDB } from '../server/index.js'
|
|
11
|
-
import { scaffoldBase, seedSkill, ensureGitignore } from '../server/scaffold.js'
|
|
10
|
+
import { scaffoldBase, seedSkill, ensureGitignore, seedAgentInstructions } from '../server/scaffold.js'
|
|
12
11
|
import { buildGraphFromRoot } from '../server/graph/parser.js'
|
|
13
12
|
import {
|
|
14
|
-
claimId,
|
|
15
13
|
deriveStatus,
|
|
16
14
|
recordClaim,
|
|
17
15
|
readClaimsIndex,
|
|
@@ -21,9 +19,20 @@ import {
|
|
|
21
19
|
outboundConsensus,
|
|
22
20
|
CONSENSUS_RELATIONS
|
|
23
21
|
} from '../server/consensus.js'
|
|
24
|
-
import { readProjectConfig, readGlobalConfig, projectSlug, anchordbClientOptions, resolveModel, resolveSettings,
|
|
22
|
+
import { readProjectConfig, readGlobalConfig, projectSlug, anchordbClientOptions, resolveModel, resolveSettings, knlGlobalDir, globalConfigPath } from '../server/config.js'
|
|
25
23
|
import { createAnchorWireClient, mirrorStillValid } from '../server/graph/anchordb_client.js'
|
|
26
24
|
import { syncSourcesToAnchorDB, traceSources, purgeArchivedRecords } from '../server/graph/sources.js'
|
|
25
|
+
import {
|
|
26
|
+
parseModuleSpecifier,
|
|
27
|
+
buildModuleGraph,
|
|
28
|
+
syncModuleToAnchorDB,
|
|
29
|
+
traceModuleSymbol
|
|
30
|
+
} from '../server/graph/modules_builder.js'
|
|
31
|
+
import {
|
|
32
|
+
readModulesCatalog,
|
|
33
|
+
defaultEcosystemModel,
|
|
34
|
+
detectProjectManifests
|
|
35
|
+
} from '../server/graph/modules_catalog.js'
|
|
27
36
|
import { readModelRegistry, registerModel, generateModelName, modelRegistryPath } from '../server/models_registry.js'
|
|
28
37
|
import {
|
|
29
38
|
latestRelease,
|
|
@@ -32,7 +41,15 @@ import {
|
|
|
32
41
|
downloadFile,
|
|
33
42
|
verifySha256,
|
|
34
43
|
mergeGlobalAnchordb,
|
|
35
|
-
|
|
44
|
+
availableContainerRuntimes,
|
|
45
|
+
detectRunningDaemon,
|
|
46
|
+
inspectContainerStatus,
|
|
47
|
+
startContainer,
|
|
48
|
+
removeContainer,
|
|
49
|
+
detectGraphify,
|
|
50
|
+
checkDecisionSkillsStatus,
|
|
51
|
+
installDecisionSkills,
|
|
52
|
+
resolveAnchorPorts,
|
|
36
53
|
GHCR_IMAGE,
|
|
37
54
|
HTTP_PORT_DEFAULT,
|
|
38
55
|
WIRE_PORT_DEFAULT,
|
|
@@ -91,6 +108,12 @@ Commands:
|
|
|
91
108
|
"sources" model via the REST collector
|
|
92
109
|
sources trace --target=<path> File lineage over the sources graph
|
|
93
110
|
(--hops, --direction in|out|both)
|
|
111
|
+
modules build-graph <target> Build & sync external module graph into global
|
|
112
|
+
AnchorDB model (modules-<eco>) on-demand
|
|
113
|
+
Target: <eco>::<module_name> (e.g. npm::express)
|
|
114
|
+
modules trace <target> [sym] Trace calls/definitions for an external module/symbol
|
|
115
|
+
modules list [ecosystem] List locally indexed modules from ~/.knlview/modules
|
|
116
|
+
setup Bootstrap project in one command (scaffold base, skill, and models)
|
|
94
117
|
|
|
95
118
|
Options:
|
|
96
119
|
--port, -p <number> HTTP port (default: 8085, or knl-config.json)
|
|
@@ -99,11 +122,13 @@ Options:
|
|
|
99
122
|
--anchordb <url> AnchorWire URL for this session, overrides the
|
|
100
123
|
"anchordb" block in knl-config.json
|
|
101
124
|
--method <method> Method for "install anchordb": binary | container
|
|
125
|
+
--runtime <engine> Container runtime for "install anchordb": docker | podman
|
|
102
126
|
--token <text> Auth token for "install anchordb" (default: random)
|
|
103
127
|
--direction <dir> Lineage direction for "graph trace": in | out | both
|
|
104
128
|
(default: both; see README "Two-way lineage")
|
|
105
129
|
--target <id> Target node id for the subgraph query
|
|
106
130
|
--hops <number> Graph trace hop radius (default: 2)
|
|
131
|
+
--force Rebuild external module graph even if already indexed
|
|
107
132
|
--init Scaffold a new base via the wizard (does not start the server)
|
|
108
133
|
--structure <layout> Layout for non-interactive --init:
|
|
109
134
|
standard | empty | custom (default: standard)
|
|
@@ -133,10 +158,12 @@ try {
|
|
|
133
158
|
host: { type: 'string' },
|
|
134
159
|
anchordb: { type: 'string' },
|
|
135
160
|
method: { type: 'string' },
|
|
161
|
+
runtime: { type: 'string' },
|
|
136
162
|
token: { type: 'string' },
|
|
137
163
|
target: { type: 'string' },
|
|
138
164
|
direction: { type: 'string' },
|
|
139
165
|
hops: { type: 'string', default: '2' },
|
|
166
|
+
force: { type: 'boolean', default: false },
|
|
140
167
|
init: { type: 'boolean', default: false },
|
|
141
168
|
structure: { type: 'string', default: 'standard' },
|
|
142
169
|
folders: { type: 'string', default: '' },
|
|
@@ -191,6 +218,12 @@ if (values.anchordb !== undefined && !settings.anchordb) {
|
|
|
191
218
|
fail('--anchordb must be a wire:// or tcp:// URL, example: --anchordb=wire://127.0.0.1:7448?token=xxx')
|
|
192
219
|
}
|
|
193
220
|
|
|
221
|
+
// The "setup" command: one-shot bootstrap for the project
|
|
222
|
+
if (positionals?.[0] === 'setup') {
|
|
223
|
+
await runSetupCommand({ values, settings, projectConfig, globalConfig })
|
|
224
|
+
process.exit(0)
|
|
225
|
+
}
|
|
226
|
+
|
|
194
227
|
// The "model" command: manage the role → AnchorDB model name map. No other
|
|
195
228
|
// code ever guesses names; everything resolves through the role in this map.
|
|
196
229
|
const modelCommand = positionals?.[0] === 'model' ? positionals?.[1] : null
|
|
@@ -230,6 +263,18 @@ if (sourcesSub) {
|
|
|
230
263
|
process.exit(0)
|
|
231
264
|
}
|
|
232
265
|
|
|
266
|
+
// The "modules" command: on-demand external boundary graph in global AnchorDB
|
|
267
|
+
// modules models (modules-npm, modules-go, modules-cargo, modules-pypi).
|
|
268
|
+
const modulesSub = positionals?.[0] === 'modules' ? positionals?.[1] : null
|
|
269
|
+
if (modulesSub) {
|
|
270
|
+
try {
|
|
271
|
+
await runModulesCommand(modulesSub, positionals?.slice(2), { values, settings })
|
|
272
|
+
} catch (err) {
|
|
273
|
+
fail(`Modules command failed: ${err.message}`)
|
|
274
|
+
}
|
|
275
|
+
process.exit(0)
|
|
276
|
+
}
|
|
277
|
+
|
|
233
278
|
// Register the project's whole models map to the global registry (updating
|
|
234
279
|
// path/timestamp when this project owns them) and warn about names claimed
|
|
235
280
|
// by other projects. This local registration needs no server connection.
|
|
@@ -280,6 +325,10 @@ if (values.init || values['seed-skill'] !== undefined) {
|
|
|
280
325
|
process.exit(0)
|
|
281
326
|
}
|
|
282
327
|
|
|
328
|
+
if (positionals && positionals.length > 0) {
|
|
329
|
+
fail(`Unknown command "${positionals[0]}".\n\nRun "knl-view --help" for available commands.`)
|
|
330
|
+
}
|
|
331
|
+
|
|
283
332
|
const port = Number(settings.port)
|
|
284
333
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
285
334
|
fail(`--port must be a number 1-65535, got "${settings.port}".`)
|
|
@@ -305,7 +354,9 @@ const server = createKnowledgeServer({
|
|
|
305
354
|
port,
|
|
306
355
|
anchordb: settings.anchordb,
|
|
307
356
|
consensusModel: resolveModel(settings.models, 'consensus'),
|
|
308
|
-
sourcesModel: resolveModel(settings.models, 'sources')
|
|
357
|
+
sourcesModel: resolveModel(settings.models, 'sources'),
|
|
358
|
+
include: settings.include,
|
|
359
|
+
exclude: settings.exclude
|
|
309
360
|
})
|
|
310
361
|
|
|
311
362
|
server.on('error', (err) => {
|
|
@@ -359,6 +410,8 @@ server.listen(port, settings.host, () => {
|
|
|
359
410
|
} else {
|
|
360
411
|
process.stdout.write(` anchordb failed: ${result.reason} (viewer runs local only)\n`)
|
|
361
412
|
}
|
|
413
|
+
}).catch((err) => {
|
|
414
|
+
process.stdout.write(` anchordb error: ${err.message} (viewer runs local only)\n`)
|
|
362
415
|
})
|
|
363
416
|
}
|
|
364
417
|
}
|
|
@@ -428,6 +481,271 @@ async function runSetup() {
|
|
|
428
481
|
}
|
|
429
482
|
}
|
|
430
483
|
|
|
484
|
+
async function runSetupCommand({ values, settings, projectConfig, globalConfig }) {
|
|
485
|
+
const root = path.resolve(process.cwd(), values.path || settings.path || '.knowledges')
|
|
486
|
+
const interactive = Boolean(process.stdin.isTTY) && !values.yes
|
|
487
|
+
const rl = interactive ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
process.stdout.write('knl-view setup: bootstrapping project ...\n')
|
|
491
|
+
|
|
492
|
+
// Scaffold base
|
|
493
|
+
const actions = await scaffoldBase({
|
|
494
|
+
root,
|
|
495
|
+
structure: values.structure || 'standard',
|
|
496
|
+
folders: values.folders ? values.folders.split(',').map((s) => s.trim()).filter(Boolean) : []
|
|
497
|
+
})
|
|
498
|
+
const createdCount = actions.filter((a) => a.status === 'created').length
|
|
499
|
+
const relRoot = path.relative(process.cwd(), root) || root
|
|
500
|
+
process.stdout.write(` [1/5] knowledge base: ${relRoot} (${createdCount > 0 ? 'created' : 'already present'})\n`)
|
|
501
|
+
|
|
502
|
+
// Audit and seed skill
|
|
503
|
+
const skillPath = path.join(process.cwd(), '.agents', 'skills', 'knl-view', 'SKILL.md')
|
|
504
|
+
let skillPresent = false
|
|
505
|
+
try {
|
|
506
|
+
await fs.promises.stat(skillPath)
|
|
507
|
+
skillPresent = true
|
|
508
|
+
} catch {}
|
|
509
|
+
|
|
510
|
+
if (skillPresent) {
|
|
511
|
+
process.stdout.write(` [2/5] agent skill: ${skillPath} (already present)\n`)
|
|
512
|
+
} else {
|
|
513
|
+
let wantSkill = true
|
|
514
|
+
if (interactive) {
|
|
515
|
+
wantSkill = await askYesNo(rl, 'Install knl-view agent skill to .agents/skills/knl-view?', true)
|
|
516
|
+
}
|
|
517
|
+
if (wantSkill) {
|
|
518
|
+
const skillResult = await seedSkill({ scope: values.dir || 'project', cwd: process.cwd() })
|
|
519
|
+
process.stdout.write(` [2/5] agent skill: ${skillResult.path} (${skillResult.status})\n`)
|
|
520
|
+
} else {
|
|
521
|
+
process.stdout.write(` [2/5] agent skill: skipped by user\n`)
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Audit and seed decision-history skill
|
|
526
|
+
const home = process.env.HOME || process.cwd()
|
|
527
|
+
const decisionStatus = await checkDecisionSkillsStatus({ cwd: process.cwd(), home })
|
|
528
|
+
const statusLabel =
|
|
529
|
+
decisionStatus.status === 'not_installed'
|
|
530
|
+
? 'not installed'
|
|
531
|
+
: decisionStatus.status === 'needs_update'
|
|
532
|
+
? `update available (current: ${decisionStatus.currentVersion || 'unknown'}, latest: ${decisionStatus.latestVersion})`
|
|
533
|
+
: 'already installed'
|
|
534
|
+
|
|
535
|
+
process.stdout.write(` [skill] decision-history: ${statusLabel}\n`)
|
|
536
|
+
|
|
537
|
+
if (decisionStatus.status === 'not_installed' || decisionStatus.status === 'needs_update') {
|
|
538
|
+
let wantDecisionSkill = true
|
|
539
|
+
if (interactive) {
|
|
540
|
+
wantDecisionSkill = await askYesNo(
|
|
541
|
+
rl,
|
|
542
|
+
'Would you like to install/update the decision-history skill (rachmanzz/decisionskills)?',
|
|
543
|
+
true
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
if (wantDecisionSkill) {
|
|
547
|
+
let scope = 'project'
|
|
548
|
+
if (interactive) {
|
|
549
|
+
scope = await askChoice(
|
|
550
|
+
rl,
|
|
551
|
+
'Scope for decision-history skill?',
|
|
552
|
+
[
|
|
553
|
+
['project', 'per-project (.agents/skills/decision-history)'],
|
|
554
|
+
['global', 'global (all agent terminals: agy, codex, opencode, claude)']
|
|
555
|
+
],
|
|
556
|
+
'project'
|
|
557
|
+
)
|
|
558
|
+
}
|
|
559
|
+
process.stdout.write(`Installing decision-history skill (${scope}) ...\n`)
|
|
560
|
+
await installDecisionSkills({ scope, cwd: process.cwd(), home, exec })
|
|
561
|
+
process.stdout.write(` [skill] decision-history: installed (${scope})\n`)
|
|
562
|
+
} else {
|
|
563
|
+
process.stdout.write(` [skill] decision-history: skipped by user\n`)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Ensure gitignore
|
|
568
|
+
const giResult = await ensureGitignore({ cwd: process.cwd() })
|
|
569
|
+
process.stdout.write(` [3/5] gitignore: ${giResult.path} (${giResult.status})\n`)
|
|
570
|
+
|
|
571
|
+
// Daemon and container lifecycle
|
|
572
|
+
let currentGlobalConfig = globalConfig
|
|
573
|
+
let containerStatus = await inspectContainerStatus(exec)
|
|
574
|
+
let daemonMeta = await detectRunningDaemon(exec)
|
|
575
|
+
|
|
576
|
+
if (containerStatus.exists && !containerStatus.valid) {
|
|
577
|
+
let wantRecreate = true
|
|
578
|
+
if (interactive) {
|
|
579
|
+
wantRecreate = await askYesNo(
|
|
580
|
+
rl,
|
|
581
|
+
`AnchorDB container "${containerStatus.name}" is in an invalid state (${containerStatus.status}). Would you like to recreate it?`,
|
|
582
|
+
true
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
if (wantRecreate) {
|
|
586
|
+
process.stdout.write(`Removing invalid container "${containerStatus.name}" ...\n`)
|
|
587
|
+
try {
|
|
588
|
+
await removeContainer(exec, containerStatus.runtime, containerStatus.name)
|
|
589
|
+
} catch {}
|
|
590
|
+
await runInstallCommand('anchordb', {
|
|
591
|
+
values: { ...values, method: 'container' },
|
|
592
|
+
rl
|
|
593
|
+
})
|
|
594
|
+
const freshGlobal = await readGlobalConfig()
|
|
595
|
+
currentGlobalConfig = freshGlobal.config || currentGlobalConfig
|
|
596
|
+
daemonMeta = await detectRunningDaemon(exec)
|
|
597
|
+
}
|
|
598
|
+
} else if (containerStatus.exists && !containerStatus.running && containerStatus.valid) {
|
|
599
|
+
let wantStart = true
|
|
600
|
+
if (interactive) {
|
|
601
|
+
wantStart = await askYesNo(
|
|
602
|
+
rl,
|
|
603
|
+
`AnchorDB container "${containerStatus.name}" is stopped. Would you like to start it now?`,
|
|
604
|
+
true
|
|
605
|
+
)
|
|
606
|
+
}
|
|
607
|
+
if (wantStart) {
|
|
608
|
+
process.stdout.write(`Starting container "${containerStatus.name}" ...\n`)
|
|
609
|
+
await startContainer(exec, containerStatus.runtime, containerStatus.name)
|
|
610
|
+
await waitAnchorHealth()
|
|
611
|
+
daemonMeta = await detectRunningDaemon(exec)
|
|
612
|
+
if (daemonMeta) {
|
|
613
|
+
const updatedAnchor = {
|
|
614
|
+
...(currentGlobalConfig?.anchordb || {}),
|
|
615
|
+
url: `wire://127.0.0.1:${daemonMeta.wire_port || WIRE_PORT_DEFAULT}`,
|
|
616
|
+
token: daemonMeta.token || currentGlobalConfig?.anchordb?.token || '',
|
|
617
|
+
sync: currentGlobalConfig?.anchordb?.sync !== false,
|
|
618
|
+
daemon: daemonMeta.daemon || daemonMeta.deamon,
|
|
619
|
+
deamon: daemonMeta.daemon || daemonMeta.deamon,
|
|
620
|
+
container_name: daemonMeta.container_name,
|
|
621
|
+
container_id: daemonMeta.container_id,
|
|
622
|
+
container_created_on: daemonMeta.container_created_on
|
|
623
|
+
}
|
|
624
|
+
await mergeGlobalAnchordb(home, updatedAnchor)
|
|
625
|
+
currentGlobalConfig = { ...currentGlobalConfig, anchordb: updatedAnchor }
|
|
626
|
+
process.stdout.write(` [info] global config updated with ${daemonMeta.daemon || daemonMeta.deamon} daemon metadata\n`)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
} else if (!daemonMeta && !containerStatus.exists) {
|
|
630
|
+
const hasConfigured = Boolean(currentGlobalConfig?.anchordb?.url)
|
|
631
|
+
let wantInstall = Boolean(values.runtime)
|
|
632
|
+
if (!wantInstall && interactive && !hasConfigured) {
|
|
633
|
+
wantInstall = await askYesNo(
|
|
634
|
+
rl,
|
|
635
|
+
'AnchorDB is not running. Would you like to create an AnchorDB container now?',
|
|
636
|
+
false
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
if (wantInstall) {
|
|
640
|
+
process.stdout.write('\nProvisioning AnchorDB container ...\n')
|
|
641
|
+
await runInstallCommand('anchordb', {
|
|
642
|
+
values: { ...values, method: 'container' },
|
|
643
|
+
rl
|
|
644
|
+
})
|
|
645
|
+
const freshGlobal = await readGlobalConfig()
|
|
646
|
+
currentGlobalConfig = freshGlobal.config || currentGlobalConfig
|
|
647
|
+
daemonMeta = await detectRunningDaemon(exec)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (daemonMeta && currentGlobalConfig?.anchordb) {
|
|
652
|
+
const existing = currentGlobalConfig.anchordb
|
|
653
|
+
const daemonType = daemonMeta.daemon || daemonMeta.deamon || existing.daemon || existing.deamon
|
|
654
|
+
const updated = {
|
|
655
|
+
...existing,
|
|
656
|
+
daemon: daemonType,
|
|
657
|
+
deamon: daemonType
|
|
658
|
+
}
|
|
659
|
+
if (daemonMeta.container_name) updated.container_name = daemonMeta.container_name
|
|
660
|
+
if (daemonMeta.container_id) updated.container_id = daemonMeta.container_id
|
|
661
|
+
if (daemonMeta.container_created_on) updated.container_created_on = daemonMeta.container_created_on
|
|
662
|
+
|
|
663
|
+
if (JSON.stringify(existing) !== JSON.stringify(updated)) {
|
|
664
|
+
await mergeGlobalAnchordb(home, updated)
|
|
665
|
+
currentGlobalConfig = { ...currentGlobalConfig, anchordb: updated }
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
let currentConfig = projectConfig || {}
|
|
670
|
+
const hasGlobalAnchor = Boolean(currentGlobalConfig?.anchordb)
|
|
671
|
+
const hasLocalAnchor = Boolean(projectConfig?.anchordb)
|
|
672
|
+
const hasAnchor = hasGlobalAnchor || hasLocalAnchor
|
|
673
|
+
|
|
674
|
+
if (hasAnchor) {
|
|
675
|
+
const slug = projectSlug(process.cwd())
|
|
676
|
+
const neededRoles = ['knowledges', 'sources']
|
|
677
|
+
let modelsUpdated = false
|
|
678
|
+
const nextModels = { ...(currentConfig.models || {}) }
|
|
679
|
+
|
|
680
|
+
for (const role of neededRoles) {
|
|
681
|
+
if (!nextModels[role]) {
|
|
682
|
+
const name = generateModelName()
|
|
683
|
+
await registerModel(name, { role, project: slug, cwd: process.cwd() })
|
|
684
|
+
nextModels[role] = name
|
|
685
|
+
modelsUpdated = true
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (modelsUpdated) {
|
|
690
|
+
currentConfig = { ...currentConfig, models: nextModels }
|
|
691
|
+
delete currentConfig.model
|
|
692
|
+
fs.writeFileSync(path.join(process.cwd(), 'knl-config.json'), JSON.stringify(currentConfig, null, 2) + '\n', 'utf8')
|
|
693
|
+
process.stdout.write(` [4/5] anchordb models: mapped knowledges (${nextModels.knowledges}) and sources (${nextModels.sources}) in knl-config.json\n`)
|
|
694
|
+
} else {
|
|
695
|
+
process.stdout.write(` [4/5] anchordb models: already mapped in knl-config.json\n`)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const targetAnchordb = {
|
|
699
|
+
...(settings.anchordb || currentGlobalConfig?.anchordb || currentConfig?.anchordb),
|
|
700
|
+
model: nextModels.sources
|
|
701
|
+
}
|
|
702
|
+
const opts = anchordbClientOptions(targetAnchordb)
|
|
703
|
+
if (opts) {
|
|
704
|
+
try {
|
|
705
|
+
process.stdout.write(' syncing project sources to AnchorDB ...\n')
|
|
706
|
+
const client = await createAnchorWireClient(opts)
|
|
707
|
+
const result = await syncSourcesToAnchorDB(client, process.cwd(), {
|
|
708
|
+
labelDir: settings.labelDir,
|
|
709
|
+
include: settings.include,
|
|
710
|
+
exclude: settings.exclude
|
|
711
|
+
})
|
|
712
|
+
client.close()
|
|
713
|
+
process.stdout.write(` sources sync complete: ${result.files} files, ${result.edges} edges\n`)
|
|
714
|
+
} catch (err) {
|
|
715
|
+
process.stdout.write(` (sources sync deferred: ${err.message})\n`)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} else {
|
|
719
|
+
process.stdout.write(' [4/5] anchordb: inactive (run "knl-view install anchordb" to enable)\n')
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Seed agent rules
|
|
723
|
+
let wantAgentRules = true
|
|
724
|
+
if (interactive) {
|
|
725
|
+
wantAgentRules = await askYesNo(
|
|
726
|
+
rl,
|
|
727
|
+
'Would you like to configure KNL-View as your primary code intelligence tool in your agent rule files (AGENTS.md, etc.)?',
|
|
728
|
+
true
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
if (wantAgentRules) {
|
|
732
|
+
const hasGraphify = await detectGraphify(exec)
|
|
733
|
+
const ruleActions = await seedAgentInstructions({ cwd: process.cwd(), home, hasGraphify })
|
|
734
|
+
for (const action of ruleActions) {
|
|
735
|
+
const rel = path.relative(process.cwd(), action.path)
|
|
736
|
+
const display = rel.startsWith('..') ? action.path : rel
|
|
737
|
+
process.stdout.write(` [5/5] agent rules: ${display} (${action.status}${hasGraphify ? ', graphify fallback enabled' : ''})\n`)
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
process.stdout.write(' [5/5] agent rules: skipped by user\n')
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
process.stdout.write('\nSetup complete! You can now run:\n knl-view # start the viewer\n knl-view graph trace --target=README.md\n')
|
|
744
|
+
} finally {
|
|
745
|
+
if (rl) rl.close()
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
431
749
|
function report(actions) {
|
|
432
750
|
for (const action of actions) {
|
|
433
751
|
const label = action.kind === 'dir' ? 'folder ' : 'file '
|
|
@@ -479,13 +797,14 @@ async function askYesNo(rl, question, fallback) {
|
|
|
479
797
|
// prefers it): ghcr.io container via docker/podman. After install, the
|
|
480
798
|
// "anchordb" block is written to the global config so every project can
|
|
481
799
|
// connect right away.
|
|
482
|
-
async function runInstallCommand(target, { values }) {
|
|
800
|
+
async function runInstallCommand(target, { values, rl: externalRl = null }) {
|
|
483
801
|
if (target !== 'anchordb') {
|
|
484
802
|
fail(`Unknown install target: ${target || '(none)'}\nAvailable: knl-view install anchordb`)
|
|
485
803
|
}
|
|
486
804
|
const nativeOk = process.platform === 'linux' || process.platform === 'darwin'
|
|
487
805
|
const interactive = Boolean(process.stdin.isTTY) && !values.yes
|
|
488
|
-
const
|
|
806
|
+
const ownsRl = !externalRl && interactive
|
|
807
|
+
const rl = externalRl || (interactive ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null)
|
|
489
808
|
|
|
490
809
|
let method = values.method
|
|
491
810
|
if (!method) {
|
|
@@ -510,6 +829,36 @@ async function runInstallCommand(target, { values }) {
|
|
|
510
829
|
// fs in this module is the callback API; installer steps need the promise version.
|
|
511
830
|
const fsp = fs.promises
|
|
512
831
|
let started = false
|
|
832
|
+
let daemonType = method === 'binary' ? 'binary' : null
|
|
833
|
+
let containerMetadata = null
|
|
834
|
+
|
|
835
|
+
const portInfo = await resolveAnchorPorts({
|
|
836
|
+
desiredWirePort: WIRE_PORT_DEFAULT,
|
|
837
|
+
desiredHttpPort: HTTP_PORT_DEFAULT
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
let wirePort = portInfo.wirePort
|
|
841
|
+
let httpPort = portInfo.httpPort
|
|
842
|
+
|
|
843
|
+
if (!portInfo.wireAvailable || !portInfo.httpAvailable) {
|
|
844
|
+
const conflictPort = !portInfo.wireAvailable ? wirePort : httpPort
|
|
845
|
+
const suggested = !portInfo.wireAvailable ? portInfo.suggestedWirePort : portInfo.suggestedHttpPort
|
|
846
|
+
let useSuggested = true
|
|
847
|
+
if (interactive) {
|
|
848
|
+
useSuggested = await askYesNo(
|
|
849
|
+
rl,
|
|
850
|
+
`Port ${conflictPort} is already in use. Would you like to use port ${suggested} instead?`,
|
|
851
|
+
true
|
|
852
|
+
)
|
|
853
|
+
}
|
|
854
|
+
if (useSuggested) {
|
|
855
|
+
wirePort = portInfo.suggestedWirePort
|
|
856
|
+
httpPort = portInfo.suggestedHttpPort
|
|
857
|
+
process.stdout.write(` [port] selected wire port ${wirePort} and HTTP port ${httpPort}\n`)
|
|
858
|
+
} else {
|
|
859
|
+
fail(`Port ${conflictPort} is in use and alternative port was declined.`)
|
|
860
|
+
}
|
|
861
|
+
}
|
|
513
862
|
|
|
514
863
|
try {
|
|
515
864
|
if (method === 'binary') {
|
|
@@ -517,60 +866,82 @@ async function runInstallCommand(target, { values }) {
|
|
|
517
866
|
fail('The binary method needs Linux or macOS.\nUse --method=container.')
|
|
518
867
|
}
|
|
519
868
|
const { version } = await latestRelease()
|
|
869
|
+
const knlViewDir = knlGlobalDir(home)
|
|
520
870
|
const asset = releaseAssetName(version)
|
|
521
871
|
const workdir = path.join(os.tmpdir(), `knlview-install-${Date.now()}`)
|
|
522
872
|
await fsp.mkdir(workdir, { recursive: true })
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
873
|
+
try {
|
|
874
|
+
const binDir = path.join(knlViewDir, 'bin')
|
|
875
|
+
|
|
876
|
+
process.stdout.write(`Downloading ${asset} (v${version}) ...\n`)
|
|
877
|
+
const archive = path.join(workdir, asset)
|
|
878
|
+
await downloadFile(`https://github.com/rachmanzz/anchordb/releases/download/v${version}/${asset}`, archive)
|
|
879
|
+
const sums = path.join(workdir, 'SHA256SUMS.txt')
|
|
880
|
+
await downloadFile(`https://github.com/rachmanzz/anchordb/releases/download/v${version}/SHA256SUMS.txt`, sums)
|
|
881
|
+
await verifySha256(archive, await fsp.readFile(sums, 'utf8'))
|
|
882
|
+
process.stdout.write(' SHA256 matches SHA256SUMS.txt\n')
|
|
883
|
+
|
|
884
|
+
process.stdout.write('Extracting ...\n')
|
|
885
|
+
await exec('tar', ['-xzf', archive, '-C', workdir])
|
|
886
|
+
await fsp.mkdir(binDir, { recursive: true, mode: 0o700 })
|
|
887
|
+
for (const bin of ['anchordb-server', 'anchordb']) {
|
|
888
|
+
const target2 = path.join(binDir, bin)
|
|
889
|
+
await fsp.copyFile(path.join(workdir, bin), target2)
|
|
890
|
+
await fsp.chmod(target2, 0o755)
|
|
891
|
+
}
|
|
892
|
+
process.stdout.write(` installed to ${binDir}\n`)
|
|
893
|
+
} finally {
|
|
894
|
+
await fsp.rm(workdir, { recursive: true, force: true }).catch(() => {})
|
|
895
|
+
}
|
|
542
896
|
|
|
543
897
|
const runNow = interactive
|
|
544
898
|
? await askYesNo(rl, 'Start the AnchorDB server now?', true)
|
|
545
899
|
: true
|
|
546
900
|
if (runNow) {
|
|
547
|
-
const dataDir = path.join(
|
|
901
|
+
const dataDir = path.join(knlViewDir, 'anchordb-data')
|
|
548
902
|
await fsp.mkdir(dataDir, { recursive: true })
|
|
549
|
-
const logFd = await fsp.open(path.join(
|
|
903
|
+
const logFd = await fsp.open(path.join(knlViewDir, 'anchordb.log'), 'a')
|
|
550
904
|
const child = spawn(path.join(binDir, 'anchordb-server'), [], {
|
|
551
905
|
env: {
|
|
552
906
|
...process.env,
|
|
553
907
|
ANCHORDB_AUTH_TOKEN: token,
|
|
554
908
|
ANCHORDB_DATA_DIR: dataDir,
|
|
555
|
-
ANCHORDB_PORT: String(
|
|
556
|
-
ANCHORDB_WIRE_PORT: String(
|
|
909
|
+
ANCHORDB_PORT: String(httpPort),
|
|
910
|
+
ANCHORDB_WIRE_PORT: String(wirePort)
|
|
557
911
|
},
|
|
558
912
|
detached: true,
|
|
559
913
|
stdio: ['ignore', logFd.fd, logFd.fd]
|
|
560
914
|
})
|
|
561
915
|
child.unref()
|
|
562
916
|
logFd.close()
|
|
563
|
-
await waitAnchorHealth()
|
|
917
|
+
await waitAnchorHealth(httpPort)
|
|
564
918
|
started = true
|
|
565
|
-
process.stdout.write(` server running (log: ${path.join(
|
|
919
|
+
process.stdout.write(` server running (log: ${path.join(knlViewDir, 'anchordb.log')})\n`)
|
|
566
920
|
}
|
|
567
921
|
} else {
|
|
568
|
-
const
|
|
922
|
+
const preferredRuntime = values.runtime
|
|
923
|
+
if (preferredRuntime && !['docker', 'podman'].includes(preferredRuntime)) {
|
|
924
|
+
fail(`--runtime must be docker or podman, got "${preferredRuntime}".`)
|
|
925
|
+
}
|
|
926
|
+
const which = async (cmd) => {
|
|
569
927
|
try { await exec(cmd, ['--version']); return true } catch { return false }
|
|
570
|
-
}
|
|
571
|
-
|
|
928
|
+
}
|
|
929
|
+
const available = await availableContainerRuntimes(which)
|
|
930
|
+
if (!available.length) {
|
|
572
931
|
fail('docker/podman not found.\nInstall one first: Docker Desktop (macOS/Windows) or podman.')
|
|
573
932
|
}
|
|
933
|
+
let runtime = preferredRuntime
|
|
934
|
+
if (!runtime) {
|
|
935
|
+
if (available.length === 1) {
|
|
936
|
+
runtime = available[0]
|
|
937
|
+
} else {
|
|
938
|
+
runtime = interactive
|
|
939
|
+
? await askChoice(rl, 'Container runtime to use?', available.map((r) => [r, r]), available[0])
|
|
940
|
+
: available[0]
|
|
941
|
+
}
|
|
942
|
+
} else if (!available.includes(runtime)) {
|
|
943
|
+
fail(`Runtime "${runtime}" was requested via --runtime, but it is not installed or available on this system.`)
|
|
944
|
+
}
|
|
574
945
|
const { version } = await latestRelease()
|
|
575
946
|
const image = `${GHCR_IMAGE}:${version}`
|
|
576
947
|
process.stdout.write(`Pulling ${image} via ${runtime} ...\n`)
|
|
@@ -583,14 +954,19 @@ async function runInstallCommand(target, { values }) {
|
|
|
583
954
|
fail(`Container "${CONTAINER_NAME}" already exists.\nRemove it first if you really want to reinstall: ${runtime} rm -f ${CONTAINER_NAME}`)
|
|
584
955
|
}
|
|
585
956
|
|
|
586
|
-
const
|
|
587
|
-
await fsp.mkdir(
|
|
588
|
-
await fsp.chmod(
|
|
957
|
+
const knlViewDir = knlGlobalDir(home)
|
|
958
|
+
await fsp.mkdir(knlViewDir, { recursive: true, mode: 0o700 })
|
|
959
|
+
try { await fsp.chmod(knlViewDir, 0o700) } catch {}
|
|
960
|
+
const dataDir = path.join(knlViewDir, 'anchordb-data')
|
|
961
|
+
await fsp.mkdir(dataDir, { recursive: true, mode: 0o770 })
|
|
962
|
+
try { await fsp.chmod(dataDir, 0o770) } catch {}
|
|
589
963
|
const baseArgs = ['run', '-d', '--name', CONTAINER_NAME,
|
|
590
|
-
'-p', `${
|
|
591
|
-
'-p', `${
|
|
964
|
+
'-p', `${httpPort}:${httpPort}`,
|
|
965
|
+
'-p', `${wirePort}:${wirePort}`,
|
|
592
966
|
'-e', `ANCHORDB_AUTH_TOKEN=${token}`,
|
|
593
|
-
'-e', 'ANCHORDB_DATA_DIR=/data'
|
|
967
|
+
'-e', 'ANCHORDB_DATA_DIR=/data',
|
|
968
|
+
'-e', `ANCHORDB_PORT=${httpPort}`,
|
|
969
|
+
'-e', `ANCHORDB_WIRE_PORT=${wirePort}`]
|
|
594
970
|
// The image runs as user "anchordb" (not root). Rootless podman maps
|
|
595
971
|
// that uid to a different host uid, so data files owned by the host
|
|
596
972
|
// user become unreadable ("permission denied"). keep-id runs the
|
|
@@ -598,27 +974,53 @@ async function runInstallCommand(target, { values }) {
|
|
|
598
974
|
// container can share the folder. Docker (macOS/Windows) runs in a VM
|
|
599
975
|
// and its bind mounts are unaffected, hence no flag here.
|
|
600
976
|
if (runtime === 'podman') baseArgs.push('--userns=keep-id')
|
|
977
|
+
daemonType = runtime
|
|
978
|
+
let containerId = null
|
|
979
|
+
let containerCreatedOn = new Date().toISOString()
|
|
601
980
|
process.stdout.write(`Starting container ...\n`)
|
|
602
981
|
try {
|
|
603
|
-
await exec(runtime, [...baseArgs, '-v', `${dataDir}:/data:Z`, image])
|
|
982
|
+
const { stdout } = await exec(runtime, [...baseArgs, '-v', `${dataDir}:/data:Z`, image])
|
|
983
|
+
containerId = stdout.trim().split('\n')[0]
|
|
604
984
|
} catch {
|
|
605
985
|
// Rootless/sandbox setups sometimes reject volume binds; without a
|
|
606
986
|
// volume the data dies with the container, so we say so plainly.
|
|
607
987
|
process.stdout.write(' volume could not be mounted; data is ephemeral inside the container\n')
|
|
608
|
-
await exec(runtime, [...baseArgs, image])
|
|
988
|
+
const { stdout } = await exec(runtime, [...baseArgs, image])
|
|
989
|
+
containerId = stdout.trim().split('\n')[0]
|
|
609
990
|
}
|
|
610
|
-
|
|
991
|
+
try {
|
|
992
|
+
const { stdout: idOut } = await exec(runtime, ['inspect', '--format', '{{.Id}}', CONTAINER_NAME])
|
|
993
|
+
if (idOut.trim()) containerId = idOut.trim()
|
|
994
|
+
const { stdout: createdOut } = await exec(runtime, ['inspect', '--format', '{{.Created}}', CONTAINER_NAME])
|
|
995
|
+
if (createdOut.trim()) containerCreatedOn = createdOut.trim()
|
|
996
|
+
} catch {
|
|
997
|
+
// fallback to captured containerId and timestamp
|
|
998
|
+
}
|
|
999
|
+
containerMetadata = {
|
|
1000
|
+
container_created_on: containerCreatedOn,
|
|
1001
|
+
container_name: CONTAINER_NAME,
|
|
1002
|
+
container_id: containerId
|
|
1003
|
+
}
|
|
1004
|
+
await waitAnchorHealth(httpPort)
|
|
611
1005
|
started = true
|
|
612
1006
|
}
|
|
613
1007
|
} catch (err) {
|
|
614
1008
|
fail(`Install failed: ${err.message || err}`)
|
|
615
1009
|
}
|
|
616
1010
|
|
|
617
|
-
const
|
|
618
|
-
url: `wire://127.0.0.1:${
|
|
1011
|
+
const anchordbBlock = {
|
|
1012
|
+
url: `wire://127.0.0.1:${wirePort}`,
|
|
619
1013
|
token,
|
|
620
|
-
sync: true
|
|
621
|
-
|
|
1014
|
+
sync: true,
|
|
1015
|
+
deamon: daemonType
|
|
1016
|
+
}
|
|
1017
|
+
if (containerMetadata) {
|
|
1018
|
+
anchordbBlock.container_created_on = containerMetadata.container_created_on
|
|
1019
|
+
anchordbBlock.container_name = containerMetadata.container_name
|
|
1020
|
+
anchordbBlock.container_id = containerMetadata.container_id
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const before = await mergeGlobalAnchordb(home, anchordbBlock)
|
|
622
1024
|
if (before) {
|
|
623
1025
|
process.stdout.write(' (old anchordb block in the global config overwritten; new token applied)\n')
|
|
624
1026
|
}
|
|
@@ -626,10 +1028,10 @@ async function runInstallCommand(target, { values }) {
|
|
|
626
1028
|
process.stdout.write(
|
|
627
1029
|
`\nAnchorDB ready (optional layer: projects without an anchordb block\n` +
|
|
628
1030
|
` in the global config still run fully local).\n` +
|
|
629
|
-
` HTTP http://127.0.0.1:${
|
|
630
|
-
` wire wire://127.0.0.1:${
|
|
631
|
-
` token ${token.slice(0, 8)}... (full value in
|
|
632
|
-
` config ${home}
|
|
1031
|
+
` HTTP http://127.0.0.1:${httpPort}/health${started ? ' (already responding)' : ''}\n` +
|
|
1032
|
+
` wire wire://127.0.0.1:${wirePort}\n` +
|
|
1033
|
+
` token ${token.slice(0, 8)}... (full value in ${globalConfigPath(home)})\n` +
|
|
1034
|
+
` config ${globalConfigPath(home)}\n\n` +
|
|
633
1035
|
`Next steps (if this project should use sync):\n` +
|
|
634
1036
|
` 1. In your project: knl-view model new knowledges\n` +
|
|
635
1037
|
` 2. knl-view, the "anchordb connected" line appears once the graph is in sync\n`
|
|
@@ -640,19 +1042,20 @@ async function runInstallCommand(target, { values }) {
|
|
|
640
1042
|
` ~/.knlview/bin/anchordb-server (env: ANCHORDB_AUTH_TOKEN, ANCHORDB_DATA_DIR)\n`
|
|
641
1043
|
)
|
|
642
1044
|
}
|
|
1045
|
+
if (ownsRl && rl) rl.close()
|
|
643
1046
|
}
|
|
644
1047
|
|
|
645
|
-
async function waitAnchorHealth(attempts = 30) {
|
|
1048
|
+
async function waitAnchorHealth(httpPort = HTTP_PORT_DEFAULT, attempts = 30) {
|
|
646
1049
|
for (let i = 0; i < attempts; i++) {
|
|
647
1050
|
try {
|
|
648
|
-
const res = await fetch(`http://127.0.0.1:${
|
|
1051
|
+
const res = await fetch(`http://127.0.0.1:${httpPort}/health`, { signal: AbortSignal.timeout(2000) })
|
|
649
1052
|
if (res.ok) return
|
|
650
1053
|
} catch {
|
|
651
1054
|
// not ready yet, retry
|
|
652
1055
|
}
|
|
653
1056
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
654
1057
|
}
|
|
655
|
-
throw new Error(`server not responding at http://127.0.0.1:${
|
|
1058
|
+
throw new Error(`server not responding at http://127.0.0.1:${httpPort}/health within ${attempts / 2} seconds, check the anchordb log`)
|
|
656
1059
|
}
|
|
657
1060
|
|
|
658
1061
|
async function containerExists(runtime) {
|
|
@@ -793,7 +1196,11 @@ async function runSourcesCommand(sub, values, { settings }) {
|
|
|
793
1196
|
const projectRoot = process.cwd()
|
|
794
1197
|
|
|
795
1198
|
if (sub === 'sync') {
|
|
796
|
-
const result = await syncSourcesToAnchorDB(client, projectRoot, {
|
|
1199
|
+
const result = await syncSourcesToAnchorDB(client, projectRoot, {
|
|
1200
|
+
labelDir: settings.labelDir,
|
|
1201
|
+
include: settings.include,
|
|
1202
|
+
exclude: settings.exclude
|
|
1203
|
+
})
|
|
797
1204
|
const parts = [
|
|
798
1205
|
`${result.files} files`,
|
|
799
1206
|
`${result.symbols} symbols`,
|
|
@@ -870,6 +1277,132 @@ async function runSourcesCollector(base, model, { quiet }) {
|
|
|
870
1277
|
}
|
|
871
1278
|
}
|
|
872
1279
|
|
|
1280
|
+
// The "modules" subcommands: on-demand external boundary graph in global
|
|
1281
|
+
// AnchorDB modules models (modules-npm, modules-go, modules-cargo, modules-pypi).
|
|
1282
|
+
async function runModulesCommand(sub, args, { values, settings }) {
|
|
1283
|
+
if (sub === 'list') {
|
|
1284
|
+
const ecoFilter = args?.[0]?.toLowerCase()
|
|
1285
|
+
const ecosystems = ecoFilter ? [ecoFilter] : ['npm', 'go', 'cargo', 'pypi']
|
|
1286
|
+
const all = {}
|
|
1287
|
+
for (const eco of ecosystems) {
|
|
1288
|
+
all[eco] = await readModulesCatalog(eco)
|
|
1289
|
+
}
|
|
1290
|
+
process.stdout.write(JSON.stringify(all, null, 2) + '\n')
|
|
1291
|
+
return
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (sub === 'build-graph' || sub === 'build') {
|
|
1295
|
+
const target = args?.[0] || values.target
|
|
1296
|
+
if (!target) {
|
|
1297
|
+
fail('Target is required:\n knl-view modules build-graph <ecosystem>::<module_name> [--force]')
|
|
1298
|
+
}
|
|
1299
|
+
const parsed = parseModuleSpecifier(target)
|
|
1300
|
+
if (!parsed) {
|
|
1301
|
+
fail(`Invalid module target "${target}".\nExpected format: <ecosystem>::<module_name> (e.g. npm::express, go::github.com/gin-gonic/gin)`)
|
|
1302
|
+
}
|
|
1303
|
+
const { ecosystem, moduleName } = parsed
|
|
1304
|
+
|
|
1305
|
+
if (!settings.anchordb) {
|
|
1306
|
+
fail('External modules graphs live in AnchorDB, which is not configured.\n' +
|
|
1307
|
+
'Run "knl-view install anchordb" first (optional layer).')
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const model = defaultEcosystemModel(ecosystem)
|
|
1311
|
+
const base = anchordbClientOptions(settings.anchordb)
|
|
1312
|
+
|
|
1313
|
+
if (!values.force) {
|
|
1314
|
+
const catalog = await readModulesCatalog(ecosystem)
|
|
1315
|
+
const manifestVersions = await detectProjectManifests(process.cwd())
|
|
1316
|
+
const expectedVersion = manifestVersions[ecosystem]?.[moduleName]
|
|
1317
|
+
if (expectedVersion) {
|
|
1318
|
+
const key = `${moduleName}@${expectedVersion}`
|
|
1319
|
+
if (catalog[key]?.syncedAt) {
|
|
1320
|
+
process.stdout.write(`module ${ecosystem}::${key} is already synced in model ${model} (${catalog[key].symbols.length} symbols). Use --force to rebuild.\n`)
|
|
1321
|
+
return
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
process.stdout.write(`building graph for ${ecosystem}::${moduleName} ...\n`)
|
|
1327
|
+
const moduleGraph = await buildModuleGraph(ecosystem, moduleName, { projectRoot: process.cwd() })
|
|
1328
|
+
|
|
1329
|
+
const client = await createAnchorWireClient({ ...base, model })
|
|
1330
|
+
try {
|
|
1331
|
+
const result = await syncModuleToAnchorDB(client, moduleGraph)
|
|
1332
|
+
process.stdout.write(`module synced: ${ecosystem}::${moduleGraph.name}@${moduleGraph.version} -> model "${model}" (${result.symbols} symbols, ${result.edges} edges, +${result.linked} linked)\n`)
|
|
1333
|
+
} finally {
|
|
1334
|
+
client.close()
|
|
1335
|
+
}
|
|
1336
|
+
return
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (sub === 'trace') {
|
|
1340
|
+
const rawTarget = args?.[0] || values.target
|
|
1341
|
+
if (!rawTarget) {
|
|
1342
|
+
fail('Target is required:\n knl-view modules trace <ecosystem>::<module_name> [symbol] [--hops=2] [--direction=in|out|both]')
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
let ecosystem, symbolId
|
|
1346
|
+
if (rawTarget.startsWith('ext:')) {
|
|
1347
|
+
const m = rawTarget.match(/^ext:([a-z]+)\//)
|
|
1348
|
+
if (!m) fail(`Cannot determine ecosystem from node ID "${rawTarget}".`)
|
|
1349
|
+
ecosystem = m[1]
|
|
1350
|
+
symbolId = rawTarget
|
|
1351
|
+
} else {
|
|
1352
|
+
const parsed = parseModuleSpecifier(rawTarget)
|
|
1353
|
+
if (!parsed) {
|
|
1354
|
+
fail(`Invalid module target "${rawTarget}".\nExpected format: <ecosystem>::<module_name> [symbol]`)
|
|
1355
|
+
}
|
|
1356
|
+
ecosystem = parsed.ecosystem
|
|
1357
|
+
const catalog = await readModulesCatalog(ecosystem)
|
|
1358
|
+
const subSymbol = args?.[1]
|
|
1359
|
+
const entryKey = Object.keys(catalog).find((k) => k.startsWith(parsed.moduleName + '@') || k === parsed.moduleName)
|
|
1360
|
+
if (!entryKey) {
|
|
1361
|
+
fail(`Module "${rawTarget}" is not indexed in catalog ~/.knlview/modules/${ecosystem}.json.\nRun "knl-view modules build-graph ${rawTarget}" first.`)
|
|
1362
|
+
}
|
|
1363
|
+
const entry = catalog[entryKey]
|
|
1364
|
+
symbolId = subSymbol ? `${entry.nodeId}#${subSymbol}` : entry.nodeId
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (!settings.anchordb) {
|
|
1368
|
+
fail('The modules graph lives in AnchorDB, which is not configured.\n' +
|
|
1369
|
+
'Run "knl-view install anchordb" first.')
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const model = defaultEcosystemModel(ecosystem)
|
|
1373
|
+
const base = anchordbClientOptions(settings.anchordb)
|
|
1374
|
+
const client = await createAnchorWireClient({ ...base, model })
|
|
1375
|
+
try {
|
|
1376
|
+
let direction = 'both'
|
|
1377
|
+
if (values.direction !== undefined) {
|
|
1378
|
+
if (!['in', 'out', 'both'].includes(values.direction)) {
|
|
1379
|
+
fail(`--direction must be in, out, or both, got "${values.direction}".`)
|
|
1380
|
+
}
|
|
1381
|
+
direction = values.direction
|
|
1382
|
+
}
|
|
1383
|
+
const hops = Number.parseInt(values.hops || '2', 10)
|
|
1384
|
+
const maxHops = Number.isInteger(hops) && hops > 0 ? hops : 2
|
|
1385
|
+
const results = await traceModuleSymbol(client, symbolId, { direction, maxHops })
|
|
1386
|
+
process.stdout.write(JSON.stringify({
|
|
1387
|
+
center: symbolId,
|
|
1388
|
+
model,
|
|
1389
|
+
direction,
|
|
1390
|
+
maxHops,
|
|
1391
|
+
trace: results
|
|
1392
|
+
}, null, 2) + '\n')
|
|
1393
|
+
} finally {
|
|
1394
|
+
client.close()
|
|
1395
|
+
}
|
|
1396
|
+
return
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
fail('Unknown modules subcommand.\n' +
|
|
1400
|
+
'Usage:\n' +
|
|
1401
|
+
' knl-view modules build-graph <ecosystem>::<module_name> [--force]\n' +
|
|
1402
|
+
' knl-view modules trace <ecosystem>::<module_name> [symbol] [--hops=2] [--direction=in|out|both]\n' +
|
|
1403
|
+
' knl-view modules list [ecosystem]')
|
|
1404
|
+
}
|
|
1405
|
+
|
|
873
1406
|
function fail(message) {
|
|
874
1407
|
process.stderr.write(`knl-view: ${message}\n`)
|
|
875
1408
|
process.exit(1)
|