@datasynx/agentic-ai-cartography 0.3.3 → 0.5.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 +18 -8
- package/dist/{chunk-FFNOC6HF.js → chunk-A7FTULDM.js} +67 -2
- package/dist/chunk-A7FTULDM.js.map +1 -0
- package/dist/cli.js +1008 -42
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +223 -1
- package/dist/index.js +1073 -11
- package/dist/index.js.map +1 -1
- package/dist/{types-ROE3Z6HY.js → types-ZD6G5JKR.js} +12 -2
- package/package.json +1 -1
- package/dist/chunk-FFNOC6HF.js.map +0 -1
- /package/dist/{types-ROE3Z6HY.js.map → types-ZD6G5JKR.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
DOMAIN_COLORS,
|
|
4
|
+
DOMAIN_PALETTE,
|
|
3
5
|
EDGE_RELATIONSHIPS,
|
|
4
6
|
NODE_TYPES,
|
|
5
7
|
SOPStepSchema,
|
|
6
8
|
defaultConfig
|
|
7
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-A7FTULDM.js";
|
|
8
10
|
import {
|
|
9
11
|
scanAllBookmarks,
|
|
10
12
|
scanAllHistory
|
|
@@ -79,9 +81,21 @@ CREATE TABLE IF NOT EXISTS nodes (
|
|
|
79
81
|
confidence REAL DEFAULT 0.5,
|
|
80
82
|
metadata TEXT NOT NULL DEFAULT '{}',
|
|
81
83
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
84
|
+
domain TEXT,
|
|
85
|
+
sub_domain TEXT,
|
|
86
|
+
quality_score REAL,
|
|
82
87
|
PRIMARY KEY (id, session_id)
|
|
83
88
|
);
|
|
84
89
|
|
|
90
|
+
CREATE TABLE IF NOT EXISTS connections (
|
|
91
|
+
id TEXT PRIMARY KEY,
|
|
92
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
93
|
+
source_asset_id TEXT NOT NULL,
|
|
94
|
+
target_asset_id TEXT NOT NULL,
|
|
95
|
+
type TEXT,
|
|
96
|
+
created_at TEXT NOT NULL
|
|
97
|
+
);
|
|
98
|
+
|
|
85
99
|
CREATE TABLE IF NOT EXISTS edges (
|
|
86
100
|
id TEXT PRIMARY KEY,
|
|
87
101
|
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
@@ -156,6 +170,7 @@ CREATE INDEX IF NOT EXISTS idx_edges_session ON edges(session_id);
|
|
|
156
170
|
CREATE INDEX IF NOT EXISTS idx_events_session ON activity_events(session_id);
|
|
157
171
|
CREATE INDEX IF NOT EXISTS idx_events_task ON activity_events(task_id);
|
|
158
172
|
CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_connections_session ON connections(session_id);
|
|
159
174
|
`;
|
|
160
175
|
var CartographyDB = class {
|
|
161
176
|
db;
|
|
@@ -171,7 +186,24 @@ var CartographyDB = class {
|
|
|
171
186
|
const version = this.db.pragma("user_version", { simple: true });
|
|
172
187
|
if (version === 0) {
|
|
173
188
|
this.db.exec(SCHEMA);
|
|
174
|
-
this.db.pragma("user_version =
|
|
189
|
+
this.db.pragma("user_version = 2");
|
|
190
|
+
} else if (version === 1) {
|
|
191
|
+
const cols = this.db.prepare("PRAGMA table_info(nodes)").all().map((c) => c.name);
|
|
192
|
+
if (!cols.includes("domain")) this.db.exec("ALTER TABLE nodes ADD COLUMN domain TEXT");
|
|
193
|
+
if (!cols.includes("sub_domain")) this.db.exec("ALTER TABLE nodes ADD COLUMN sub_domain TEXT");
|
|
194
|
+
if (!cols.includes("quality_score")) this.db.exec("ALTER TABLE nodes ADD COLUMN quality_score REAL");
|
|
195
|
+
this.db.exec(`
|
|
196
|
+
CREATE TABLE IF NOT EXISTS connections (
|
|
197
|
+
id TEXT PRIMARY KEY,
|
|
198
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
199
|
+
source_asset_id TEXT NOT NULL,
|
|
200
|
+
target_asset_id TEXT NOT NULL,
|
|
201
|
+
type TEXT,
|
|
202
|
+
created_at TEXT NOT NULL
|
|
203
|
+
);
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_connections_session ON connections(session_id);
|
|
205
|
+
`);
|
|
206
|
+
this.db.pragma("user_version = 2");
|
|
175
207
|
}
|
|
176
208
|
}
|
|
177
209
|
close() {
|
|
@@ -214,8 +246,9 @@ var CartographyDB = class {
|
|
|
214
246
|
upsertNode(sessionId, node, depth = 0) {
|
|
215
247
|
this.db.prepare(`
|
|
216
248
|
INSERT OR REPLACE INTO nodes
|
|
217
|
-
(id, session_id, type, name, discovered_via, discovered_at, depth, confidence, metadata, tags
|
|
218
|
-
|
|
249
|
+
(id, session_id, type, name, discovered_via, discovered_at, depth, confidence, metadata, tags,
|
|
250
|
+
domain, sub_domain, quality_score)
|
|
251
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
219
252
|
`).run(
|
|
220
253
|
node.id,
|
|
221
254
|
sessionId,
|
|
@@ -226,12 +259,18 @@ var CartographyDB = class {
|
|
|
226
259
|
depth,
|
|
227
260
|
node.confidence,
|
|
228
261
|
JSON.stringify(node.metadata ?? {}),
|
|
229
|
-
JSON.stringify(node.tags ?? [])
|
|
262
|
+
JSON.stringify(node.tags ?? []),
|
|
263
|
+
node.domain ?? null,
|
|
264
|
+
node.subDomain ?? null,
|
|
265
|
+
node.qualityScore ?? null
|
|
230
266
|
);
|
|
231
267
|
}
|
|
232
268
|
getNodes(sessionId) {
|
|
233
269
|
const rows = this.db.prepare("SELECT * FROM nodes WHERE session_id = ?").all(sessionId);
|
|
234
|
-
return rows.map((r) => (
|
|
270
|
+
return rows.map((r) => this.mapNode(r));
|
|
271
|
+
}
|
|
272
|
+
mapNode(r) {
|
|
273
|
+
return {
|
|
235
274
|
id: r["id"],
|
|
236
275
|
sessionId: r["session_id"],
|
|
237
276
|
type: r["type"],
|
|
@@ -242,8 +281,11 @@ var CartographyDB = class {
|
|
|
242
281
|
confidence: r["confidence"],
|
|
243
282
|
metadata: JSON.parse(r["metadata"]),
|
|
244
283
|
tags: JSON.parse(r["tags"]),
|
|
245
|
-
pathId: r["path_id"]
|
|
246
|
-
|
|
284
|
+
pathId: r["path_id"],
|
|
285
|
+
domain: r["domain"] ?? void 0,
|
|
286
|
+
subDomain: r["sub_domain"] ?? void 0,
|
|
287
|
+
qualityScore: r["quality_score"] ?? void 0
|
|
288
|
+
};
|
|
247
289
|
}
|
|
248
290
|
deleteNode(sessionId, nodeId) {
|
|
249
291
|
this.db.prepare("DELETE FROM nodes WHERE session_id = ? AND id = ?").run(sessionId, nodeId);
|
|
@@ -455,6 +497,33 @@ var CartographyDB = class {
|
|
|
455
497
|
generatedAt: r["generated_at"]
|
|
456
498
|
}));
|
|
457
499
|
}
|
|
500
|
+
// ── Connections (user-created hex map links) ─────────────────────────────
|
|
501
|
+
upsertConnection(sessionId, conn) {
|
|
502
|
+
const existing = this.db.prepare(
|
|
503
|
+
"SELECT id FROM connections WHERE session_id = ? AND source_asset_id = ? AND target_asset_id = ?"
|
|
504
|
+
).get(sessionId, conn.sourceAssetId, conn.targetAssetId);
|
|
505
|
+
if (existing) return existing.id;
|
|
506
|
+
const id = crypto.randomUUID();
|
|
507
|
+
this.db.prepare(`
|
|
508
|
+
INSERT INTO connections (id, session_id, source_asset_id, target_asset_id, type, created_at)
|
|
509
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
510
|
+
`).run(id, sessionId, conn.sourceAssetId, conn.targetAssetId, conn.type ?? null, (/* @__PURE__ */ new Date()).toISOString());
|
|
511
|
+
return id;
|
|
512
|
+
}
|
|
513
|
+
getConnections(sessionId) {
|
|
514
|
+
const rows = this.db.prepare("SELECT * FROM connections WHERE session_id = ?").all(sessionId);
|
|
515
|
+
return rows.map((r) => ({
|
|
516
|
+
id: r["id"],
|
|
517
|
+
sessionId: r["session_id"],
|
|
518
|
+
sourceAssetId: r["source_asset_id"],
|
|
519
|
+
targetAssetId: r["target_asset_id"],
|
|
520
|
+
type: r["type"] ?? void 0,
|
|
521
|
+
createdAt: r["created_at"]
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
deleteConnection(sessionId, connectionId) {
|
|
525
|
+
this.db.prepare("DELETE FROM connections WHERE session_id = ? AND id = ?").run(sessionId, connectionId);
|
|
526
|
+
}
|
|
458
527
|
// ── Approvals ───────────────────────────
|
|
459
528
|
setApproval(pattern, action) {
|
|
460
529
|
this.db.prepare(`
|
|
@@ -496,7 +565,10 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
496
565
|
discoveredVia: z.string(),
|
|
497
566
|
confidence: z.number().min(0).max(1),
|
|
498
567
|
metadata: z.record(z.unknown()).optional(),
|
|
499
|
-
tags: z.array(z.string()).optional()
|
|
568
|
+
tags: z.array(z.string()).optional(),
|
|
569
|
+
domain: z.string().optional().describe('Business domain, e.g. "Marketing", "Finance"'),
|
|
570
|
+
subDomain: z.string().optional().describe('Sub-domain, e.g. "Forecast client orders"'),
|
|
571
|
+
qualityScore: z.number().min(0).max(100).optional().describe("Data quality score 0\u2013100")
|
|
500
572
|
}, async (args) => {
|
|
501
573
|
const node = {
|
|
502
574
|
id: stripSensitive(args["id"]),
|
|
@@ -505,7 +577,10 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
505
577
|
discoveredVia: args["discoveredVia"],
|
|
506
578
|
confidence: args["confidence"],
|
|
507
579
|
metadata: args["metadata"] ?? {},
|
|
508
|
-
tags: args["tags"] ?? []
|
|
580
|
+
tags: args["tags"] ?? [],
|
|
581
|
+
domain: args["domain"],
|
|
582
|
+
subDomain: args["subDomain"],
|
|
583
|
+
qualityScore: args["qualityScore"]
|
|
509
584
|
};
|
|
510
585
|
db.upsertNode(sessionId, node);
|
|
511
586
|
return { content: [{ type: "text", text: `\u2713 Node: ${node.id}` }] };
|
|
@@ -600,14 +675,14 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
600
675
|
tool("scan_local_databases", "Scan for local database files and running DB servers \u2014 PostgreSQL databases, MySQL, SQLite files from installed apps", {
|
|
601
676
|
deep: z.boolean().default(false).optional().describe("Also search home directory recursively for SQLite/DB files (slower)")
|
|
602
677
|
}, async (args) => {
|
|
603
|
-
const { execSync:
|
|
678
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
604
679
|
const { homedir } = await import("os");
|
|
605
680
|
const { existsSync: existsSync3 } = await import("fs");
|
|
606
681
|
const deep = args["deep"] ?? false;
|
|
607
682
|
const HOME = homedir();
|
|
608
683
|
const run = (cmd) => {
|
|
609
684
|
try {
|
|
610
|
-
return
|
|
685
|
+
return execSync3(cmd, { stdio: "pipe", timeout: 1e4, shell: "/bin/sh" }).toString().trim();
|
|
611
686
|
} catch {
|
|
612
687
|
return "";
|
|
613
688
|
}
|
|
@@ -634,12 +709,12 @@ ${v}`).join("\n\n");
|
|
|
634
709
|
tool("scan_k8s_resources", "Scan Kubernetes cluster via kubectl \u2014 100% readonly (get, describe)", {
|
|
635
710
|
namespace: z.string().optional().describe("Filter by namespace \u2014 empty = all namespaces")
|
|
636
711
|
}, async (args) => {
|
|
637
|
-
const { execSync:
|
|
712
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
638
713
|
const ns = args["namespace"];
|
|
639
714
|
const nsFlag = ns ? `-n ${ns}` : "--all-namespaces";
|
|
640
715
|
const run = (cmd) => {
|
|
641
716
|
try {
|
|
642
|
-
return
|
|
717
|
+
return execSync3(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
|
|
643
718
|
} catch (e) {
|
|
644
719
|
return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
|
|
645
720
|
}
|
|
@@ -663,7 +738,7 @@ ${run(c)}`).join("\n\n");
|
|
|
663
738
|
region: z.string().optional().describe("AWS Region \u2014 default: AWS_DEFAULT_REGION or profile"),
|
|
664
739
|
profile: z.string().optional().describe("AWS CLI profile")
|
|
665
740
|
}, async (args) => {
|
|
666
|
-
const { execSync:
|
|
741
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
667
742
|
const region = args["region"];
|
|
668
743
|
const profile = args["profile"];
|
|
669
744
|
const env = { ...process.env };
|
|
@@ -671,7 +746,7 @@ ${run(c)}`).join("\n\n");
|
|
|
671
746
|
const pf = profile ? `--profile ${profile}` : "";
|
|
672
747
|
const run = (cmd) => {
|
|
673
748
|
try {
|
|
674
|
-
return
|
|
749
|
+
return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh", env }).toString().trim();
|
|
675
750
|
} catch (e) {
|
|
676
751
|
return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
|
|
677
752
|
}
|
|
@@ -693,12 +768,12 @@ ${run(c)}`).join("\n\n");
|
|
|
693
768
|
tool("scan_gcp_resources", "Scan Google Cloud Platform via gcloud CLI \u2014 100% readonly (list, describe)", {
|
|
694
769
|
project: z.string().optional().describe("GCP Project ID \u2014 default: current gcloud project")
|
|
695
770
|
}, async (args) => {
|
|
696
|
-
const { execSync:
|
|
771
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
697
772
|
const project = args["project"];
|
|
698
773
|
const pf = project ? `--project ${project}` : "";
|
|
699
774
|
const run = (cmd) => {
|
|
700
775
|
try {
|
|
701
|
-
return
|
|
776
|
+
return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
|
|
702
777
|
} catch (e) {
|
|
703
778
|
return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
|
|
704
779
|
}
|
|
@@ -722,14 +797,14 @@ ${run(c)}`).join("\n\n");
|
|
|
722
797
|
subscription: z.string().optional().describe("Azure Subscription ID"),
|
|
723
798
|
resourceGroup: z.string().optional().describe("Filter by resource group")
|
|
724
799
|
}, async (args) => {
|
|
725
|
-
const { execSync:
|
|
800
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
726
801
|
const sub = args["subscription"];
|
|
727
802
|
const rg = args["resourceGroup"];
|
|
728
803
|
const sf = sub ? `--subscription ${sub}` : "";
|
|
729
804
|
const rf = rg ? `--resource-group ${rg}` : "";
|
|
730
805
|
const run = (cmd) => {
|
|
731
806
|
try {
|
|
732
|
-
return
|
|
807
|
+
return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
|
|
733
808
|
} catch (e) {
|
|
734
809
|
return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
|
|
735
810
|
}
|
|
@@ -752,11 +827,11 @@ ${run(c)}`).join("\n\n");
|
|
|
752
827
|
tool("scan_installed_apps", "Scan all installed apps and tools \u2014 IDEs, office, dev tools, business apps, databases", {
|
|
753
828
|
searchHint: z.string().optional().describe('Optional search term to find specific tools (e.g. "hubspot windsurf cursor")')
|
|
754
829
|
}, async (args) => {
|
|
755
|
-
const { execSync:
|
|
830
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
756
831
|
const hint = args["searchHint"];
|
|
757
832
|
const run = (cmd) => {
|
|
758
833
|
try {
|
|
759
|
-
return
|
|
834
|
+
return execSync3(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
|
|
760
835
|
} catch {
|
|
761
836
|
return "";
|
|
762
837
|
}
|
|
@@ -1137,6 +1212,221 @@ Use ask_user when you need context from the user.`;
|
|
|
1137
1212
|
// src/exporter.ts
|
|
1138
1213
|
import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
1139
1214
|
import { join as join2 } from "path";
|
|
1215
|
+
|
|
1216
|
+
// src/hex.ts
|
|
1217
|
+
function hexToPixel(q, r, size) {
|
|
1218
|
+
const x = size * (3 / 2 * q);
|
|
1219
|
+
const y = size * (Math.sqrt(3) / 2 * q + Math.sqrt(3) * r);
|
|
1220
|
+
return { x, y };
|
|
1221
|
+
}
|
|
1222
|
+
var HEX_DIRECTIONS = [
|
|
1223
|
+
{ q: 1, r: 0 },
|
|
1224
|
+
{ q: 1, r: -1 },
|
|
1225
|
+
{ q: 0, r: -1 },
|
|
1226
|
+
{ q: -1, r: 0 },
|
|
1227
|
+
{ q: -1, r: 1 },
|
|
1228
|
+
{ q: 0, r: 1 }
|
|
1229
|
+
];
|
|
1230
|
+
function hexDistance(a, b) {
|
|
1231
|
+
return (Math.abs(a.q - b.q) + Math.abs(a.q + a.r - b.q - b.r) + Math.abs(a.r - b.r)) / 2;
|
|
1232
|
+
}
|
|
1233
|
+
function hexRing(center, radius) {
|
|
1234
|
+
if (radius === 0) return [{ q: center.q, r: center.r }];
|
|
1235
|
+
const results = [];
|
|
1236
|
+
let hex = {
|
|
1237
|
+
q: center.q + HEX_DIRECTIONS[4].q * radius,
|
|
1238
|
+
r: center.r + HEX_DIRECTIONS[4].r * radius
|
|
1239
|
+
};
|
|
1240
|
+
for (let side = 0; side < 6; side++) {
|
|
1241
|
+
for (let step = 0; step < radius; step++) {
|
|
1242
|
+
results.push({ q: hex.q, r: hex.r });
|
|
1243
|
+
hex = {
|
|
1244
|
+
q: hex.q + HEX_DIRECTIONS[side].q,
|
|
1245
|
+
r: hex.r + HEX_DIRECTIONS[side].r
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return results;
|
|
1250
|
+
}
|
|
1251
|
+
function hexSpiral(center, count) {
|
|
1252
|
+
const positions = [];
|
|
1253
|
+
let ring = 0;
|
|
1254
|
+
while (positions.length < count) {
|
|
1255
|
+
const ringPositions = hexRing(center, ring);
|
|
1256
|
+
for (const pos of ringPositions) {
|
|
1257
|
+
if (positions.length >= count) break;
|
|
1258
|
+
positions.push(pos);
|
|
1259
|
+
}
|
|
1260
|
+
ring++;
|
|
1261
|
+
}
|
|
1262
|
+
return positions;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/cluster.ts
|
|
1266
|
+
function assignColor(domain, allDomains) {
|
|
1267
|
+
if (DOMAIN_COLORS[domain]) return DOMAIN_COLORS[domain];
|
|
1268
|
+
const idx = allDomains.indexOf(domain);
|
|
1269
|
+
return DOMAIN_PALETTE[idx % DOMAIN_PALETTE.length];
|
|
1270
|
+
}
|
|
1271
|
+
function assignColors(domains) {
|
|
1272
|
+
const result = {};
|
|
1273
|
+
for (const d of domains) {
|
|
1274
|
+
result[d] = assignColor(d, domains);
|
|
1275
|
+
}
|
|
1276
|
+
return result;
|
|
1277
|
+
}
|
|
1278
|
+
function groupByDomain(assets) {
|
|
1279
|
+
const map = /* @__PURE__ */ new Map();
|
|
1280
|
+
for (const a of assets) {
|
|
1281
|
+
const d = a.domain || "Other";
|
|
1282
|
+
if (!map.has(d)) map.set(d, []);
|
|
1283
|
+
map.get(d).push(a);
|
|
1284
|
+
}
|
|
1285
|
+
return map;
|
|
1286
|
+
}
|
|
1287
|
+
var CLUSTER_GAP = 3;
|
|
1288
|
+
function layoutClusters(groups, hexSize) {
|
|
1289
|
+
const allDomains = Array.from(groups.keys());
|
|
1290
|
+
const colors = assignColors(allDomains);
|
|
1291
|
+
const sorted = Array.from(groups.entries()).sort((a, b) => b[1].length - a[1].length);
|
|
1292
|
+
const occupied = /* @__PURE__ */ new Set();
|
|
1293
|
+
const key = (q, r) => `${q},${r}`;
|
|
1294
|
+
const clusters = [];
|
|
1295
|
+
const allAssets = [];
|
|
1296
|
+
for (const [domain, domainAssets] of sorted) {
|
|
1297
|
+
const origin = clusters.length === 0 ? { q: 0, r: 0 } : findFreeOrigin(occupied, domainAssets.length, CLUSTER_GAP);
|
|
1298
|
+
const positions = hexSpiral(origin, domainAssets.length);
|
|
1299
|
+
const assetIds = [];
|
|
1300
|
+
for (let i = 0; i < domainAssets.length; i++) {
|
|
1301
|
+
const asset = domainAssets[i];
|
|
1302
|
+
asset.position = positions[i];
|
|
1303
|
+
assetIds.push(asset.id);
|
|
1304
|
+
occupied.add(key(positions[i].q, positions[i].r));
|
|
1305
|
+
allAssets.push(asset);
|
|
1306
|
+
}
|
|
1307
|
+
const centroid = computeCentroid(positions, hexSize);
|
|
1308
|
+
clusters.push({
|
|
1309
|
+
id: `cluster:${domain}`,
|
|
1310
|
+
label: domain,
|
|
1311
|
+
domain,
|
|
1312
|
+
color: colors[domain],
|
|
1313
|
+
assetIds,
|
|
1314
|
+
centroid
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
return { clusters, assets: allAssets };
|
|
1318
|
+
}
|
|
1319
|
+
function findFreeOrigin(occupied, count, gap) {
|
|
1320
|
+
const key = (q, r) => `${q},${r}`;
|
|
1321
|
+
for (let searchRadius = 1; searchRadius < 100; searchRadius++) {
|
|
1322
|
+
const candidates = hexSpiral({ q: 0, r: 0 }, 1 + 6 * searchRadius * (searchRadius + 1) / 2);
|
|
1323
|
+
for (const candidate of candidates) {
|
|
1324
|
+
const testPositions = hexSpiral(candidate, count);
|
|
1325
|
+
let fits = true;
|
|
1326
|
+
for (const tp of testPositions) {
|
|
1327
|
+
if (occupied.has(key(tp.q, tp.r))) {
|
|
1328
|
+
fits = false;
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
for (const oKey of occupied) {
|
|
1332
|
+
const [oq, or] = oKey.split(",").map(Number);
|
|
1333
|
+
if (hexDistance(tp, { q: oq, r: or }) < gap) {
|
|
1334
|
+
fits = false;
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (!fits) break;
|
|
1339
|
+
}
|
|
1340
|
+
if (fits) return candidate;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return { q: occupied.size * 5, r: 0 };
|
|
1344
|
+
}
|
|
1345
|
+
function computeCentroid(positions, hexSize) {
|
|
1346
|
+
if (positions.length === 0) return { x: 0, y: 0 };
|
|
1347
|
+
let sx = 0, sy = 0;
|
|
1348
|
+
for (const { q, r } of positions) {
|
|
1349
|
+
const { x, y } = hexToPixel(q, r, hexSize);
|
|
1350
|
+
sx += x;
|
|
1351
|
+
sy += y;
|
|
1352
|
+
}
|
|
1353
|
+
return { x: sx / positions.length, y: sy / positions.length };
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/mapper.ts
|
|
1357
|
+
var TYPE_TO_DOMAIN = {
|
|
1358
|
+
database_server: "Data Layer",
|
|
1359
|
+
database: "Data Layer",
|
|
1360
|
+
table: "Data Layer",
|
|
1361
|
+
cache_server: "Data Layer",
|
|
1362
|
+
web_service: "Web / API",
|
|
1363
|
+
api_endpoint: "Web / API",
|
|
1364
|
+
message_broker: "Messaging",
|
|
1365
|
+
queue: "Messaging",
|
|
1366
|
+
topic: "Messaging",
|
|
1367
|
+
host: "Infrastructure",
|
|
1368
|
+
container: "Infrastructure",
|
|
1369
|
+
pod: "Infrastructure",
|
|
1370
|
+
k8s_cluster: "Infrastructure",
|
|
1371
|
+
config_file: "Infrastructure",
|
|
1372
|
+
saas_tool: "SaaS Tools"
|
|
1373
|
+
};
|
|
1374
|
+
function resolveDomain(node) {
|
|
1375
|
+
if (node.domain) return node.domain;
|
|
1376
|
+
const meta = node.metadata;
|
|
1377
|
+
if (typeof meta["category"] === "string" && meta["category"].length > 0) {
|
|
1378
|
+
return meta["category"];
|
|
1379
|
+
}
|
|
1380
|
+
for (const tag of node.tags ?? []) {
|
|
1381
|
+
if (tag.length > 2 && tag[0] === tag[0].toUpperCase()) {
|
|
1382
|
+
return tag;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return TYPE_TO_DOMAIN[node.type] ?? "Other";
|
|
1386
|
+
}
|
|
1387
|
+
function nodesToAssets(nodes) {
|
|
1388
|
+
return nodes.map((n) => ({
|
|
1389
|
+
id: n.id,
|
|
1390
|
+
name: n.name,
|
|
1391
|
+
domain: resolveDomain(n),
|
|
1392
|
+
subDomain: n.subDomain,
|
|
1393
|
+
qualityScore: n.qualityScore ?? Math.round(n.confidence * 100),
|
|
1394
|
+
metadata: n.metadata ?? {},
|
|
1395
|
+
position: { q: 0, r: 0 }
|
|
1396
|
+
// will be assigned by layoutClusters
|
|
1397
|
+
}));
|
|
1398
|
+
}
|
|
1399
|
+
function edgesToConnections(edges) {
|
|
1400
|
+
return edges.map((e) => ({
|
|
1401
|
+
id: e.id,
|
|
1402
|
+
sourceAssetId: e.sourceId,
|
|
1403
|
+
targetAssetId: e.targetId,
|
|
1404
|
+
type: e.relationship
|
|
1405
|
+
}));
|
|
1406
|
+
}
|
|
1407
|
+
var HEX_SIZE = 24;
|
|
1408
|
+
function buildMapData(nodes, edges, options) {
|
|
1409
|
+
const rawAssets = nodesToAssets(nodes);
|
|
1410
|
+
const connections = edgesToConnections(edges);
|
|
1411
|
+
if (rawAssets.length === 0) {
|
|
1412
|
+
return {
|
|
1413
|
+
assets: [],
|
|
1414
|
+
clusters: [],
|
|
1415
|
+
connections,
|
|
1416
|
+
meta: { exportedAt: (/* @__PURE__ */ new Date()).toISOString(), theme: options?.theme ?? "light" }
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
const groups = groupByDomain(rawAssets);
|
|
1420
|
+
const { clusters, assets } = layoutClusters(groups, HEX_SIZE);
|
|
1421
|
+
return {
|
|
1422
|
+
assets,
|
|
1423
|
+
clusters,
|
|
1424
|
+
connections,
|
|
1425
|
+
meta: { exportedAt: (/* @__PURE__ */ new Date()).toISOString(), theme: options?.theme ?? "light" }
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/exporter.ts
|
|
1140
1430
|
function nodeLayer(type) {
|
|
1141
1431
|
if (type === "saas_tool") return "saas";
|
|
1142
1432
|
if (["web_service", "api_endpoint"].includes(type)) return "web";
|
|
@@ -1880,7 +2170,647 @@ function exportSOPMarkdown(sop) {
|
|
|
1880
2170
|
}
|
|
1881
2171
|
return lines.join("\n");
|
|
1882
2172
|
}
|
|
1883
|
-
function
|
|
2173
|
+
function exportCartographyMap(nodes, edges, options) {
|
|
2174
|
+
const mapData = buildMapData(nodes, edges, options);
|
|
2175
|
+
const { assets, clusters, connections, meta } = mapData;
|
|
2176
|
+
const isEmpty = assets.length === 0;
|
|
2177
|
+
const HEX_SIZE2 = 24;
|
|
2178
|
+
const dataJson = JSON.stringify({
|
|
2179
|
+
assets: assets.map((a) => ({
|
|
2180
|
+
id: a.id,
|
|
2181
|
+
name: a.name,
|
|
2182
|
+
domain: a.domain,
|
|
2183
|
+
subDomain: a.subDomain ?? null,
|
|
2184
|
+
qualityScore: a.qualityScore ?? null,
|
|
2185
|
+
metadata: a.metadata,
|
|
2186
|
+
q: a.position.q,
|
|
2187
|
+
r: a.position.r
|
|
2188
|
+
})),
|
|
2189
|
+
clusters: clusters.map((c) => ({
|
|
2190
|
+
id: c.id,
|
|
2191
|
+
label: c.label,
|
|
2192
|
+
domain: c.domain,
|
|
2193
|
+
color: c.color,
|
|
2194
|
+
assetIds: c.assetIds,
|
|
2195
|
+
centroid: c.centroid
|
|
2196
|
+
})),
|
|
2197
|
+
connections: connections.map((c) => ({
|
|
2198
|
+
id: c.id,
|
|
2199
|
+
sourceAssetId: c.sourceAssetId,
|
|
2200
|
+
targetAssetId: c.targetAssetId,
|
|
2201
|
+
type: c.type ?? "connection"
|
|
2202
|
+
}))
|
|
2203
|
+
});
|
|
2204
|
+
return `<!DOCTYPE html>
|
|
2205
|
+
<html lang="en">
|
|
2206
|
+
<head>
|
|
2207
|
+
<meta charset="UTF-8"/>
|
|
2208
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
2209
|
+
<title>Data Cartography Map</title>
|
|
2210
|
+
<style>
|
|
2211
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
2212
|
+
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
2213
|
+
body{display:flex;flex-direction:column;background:${meta.theme === "dark" ? "#0f172a" : "#f8fafc"};color:${meta.theme === "dark" ? "#e2e8f0" : "#1e293b"}}
|
|
2214
|
+
#topbar{
|
|
2215
|
+
height:48px;display:flex;align-items:center;gap:16px;padding:0 20px;
|
|
2216
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};z-index:10;flex-shrink:0;
|
|
2217
|
+
}
|
|
2218
|
+
#topbar h1{font-size:15px;font-weight:600;letter-spacing:-0.01em}
|
|
2219
|
+
#search-box{
|
|
2220
|
+
display:flex;align-items:center;gap:8px;background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"};
|
|
2221
|
+
border-radius:8px;padding:5px 10px;margin-left:auto;
|
|
2222
|
+
}
|
|
2223
|
+
#search-box input{
|
|
2224
|
+
border:none;background:transparent;font-size:13px;outline:none;width:180px;color:inherit;
|
|
2225
|
+
}
|
|
2226
|
+
#search-box input::placeholder{color:#94a3b8}
|
|
2227
|
+
#main{flex:1;display:flex;overflow:hidden;position:relative}
|
|
2228
|
+
#canvas-wrap{flex:1;position:relative;overflow:hidden;cursor:grab}
|
|
2229
|
+
#canvas-wrap.dragging{cursor:grabbing}
|
|
2230
|
+
#canvas-wrap.connecting{cursor:crosshair}
|
|
2231
|
+
canvas{display:block;width:100%;height:100%}
|
|
2232
|
+
/* Detail panel */
|
|
2233
|
+
#detail-panel{
|
|
2234
|
+
width:280px;background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-left:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2235
|
+
display:flex;flex-direction:column;transform:translateX(100%);
|
|
2236
|
+
transition:transform .2s ease;z-index:5;flex-shrink:0;overflow-y:auto;
|
|
2237
|
+
}
|
|
2238
|
+
#detail-panel.open{transform:translateX(0)}
|
|
2239
|
+
#detail-panel .panel-header{
|
|
2240
|
+
padding:16px;border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};display:flex;align-items:center;gap:10px;
|
|
2241
|
+
}
|
|
2242
|
+
#detail-panel .panel-header h3{font-size:14px;font-weight:600;flex:1;word-break:break-word}
|
|
2243
|
+
#detail-panel .close-btn{
|
|
2244
|
+
width:24px;height:24px;border:none;background:transparent;cursor:pointer;
|
|
2245
|
+
color:#94a3b8;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:16px;
|
|
2246
|
+
}
|
|
2247
|
+
#detail-panel .close-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2248
|
+
#detail-panel .panel-body{padding:12px 16px;display:flex;flex-direction:column;gap:12px}
|
|
2249
|
+
#detail-panel .meta-row{display:flex;flex-direction:column;gap:3px}
|
|
2250
|
+
#detail-panel .meta-label{font-size:11px;font-weight:500;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em}
|
|
2251
|
+
#detail-panel .meta-value{font-size:13px;word-break:break-all}
|
|
2252
|
+
#detail-panel .quality-bar{height:6px;border-radius:3px;background:${meta.theme === "dark" ? "#334155" : "#e2e8f0"};margin-top:4px}
|
|
2253
|
+
#detail-panel .quality-fill{height:6px;border-radius:3px;transition:width .3s}
|
|
2254
|
+
/* Bottom-left toolbar */
|
|
2255
|
+
#toolbar-left{
|
|
2256
|
+
position:absolute;bottom:20px;left:20px;display:flex;gap:8px;z-index:10;
|
|
2257
|
+
}
|
|
2258
|
+
.tb-btn{
|
|
2259
|
+
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2260
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2261
|
+
display:flex;align-items:center;justify-content:center;font-size:18px;
|
|
2262
|
+
transition:all .15s;color:inherit;
|
|
2263
|
+
}
|
|
2264
|
+
.tb-btn:hover{border-color:#94a3b8}
|
|
2265
|
+
.tb-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6}
|
|
2266
|
+
/* Bottom-right toolbar */
|
|
2267
|
+
#toolbar-right{
|
|
2268
|
+
position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;
|
|
2269
|
+
align-items:flex-end;gap:8px;z-index:10;
|
|
2270
|
+
}
|
|
2271
|
+
#zoom-controls{display:flex;align-items:center;gap:6px}
|
|
2272
|
+
.zoom-btn{
|
|
2273
|
+
width:34px;height:34px;border-radius:8px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2274
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2275
|
+
font-size:18px;color:inherit;display:flex;align-items:center;justify-content:center;
|
|
2276
|
+
}
|
|
2277
|
+
.zoom-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2278
|
+
#zoom-pct{font-size:12px;font-weight:500;color:#64748b;min-width:38px;text-align:center}
|
|
2279
|
+
#detail-selector{display:flex;flex-direction:column;gap:4px}
|
|
2280
|
+
.detail-btn{
|
|
2281
|
+
width:34px;height:34px;border-radius:8px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2282
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2283
|
+
font-size:12px;font-weight:600;color:#64748b;display:flex;align-items:center;justify-content:center;
|
|
2284
|
+
}
|
|
2285
|
+
.detail-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2286
|
+
.detail-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6;color:#2563eb}
|
|
2287
|
+
#connect-btn{
|
|
2288
|
+
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2289
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2290
|
+
font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
|
|
2291
|
+
}
|
|
2292
|
+
#connect-btn.active{background:#fef3c7;border-color:#f59e0b}
|
|
2293
|
+
/* Tooltip */
|
|
2294
|
+
#tooltip{
|
|
2295
|
+
position:fixed;background:#1e293b;color:#fff;border-radius:8px;
|
|
2296
|
+
padding:8px 12px;font-size:12px;pointer-events:none;z-index:100;
|
|
2297
|
+
display:none;max-width:220px;box-shadow:0 4px 12px rgba(0,0,0,.15);
|
|
2298
|
+
}
|
|
2299
|
+
#tooltip .tt-name{font-weight:600;margin-bottom:2px}
|
|
2300
|
+
#tooltip .tt-domain{color:#94a3b8;font-size:11px}
|
|
2301
|
+
#tooltip .tt-quality{font-size:11px;margin-top:2px}
|
|
2302
|
+
/* Empty state */
|
|
2303
|
+
#empty-state{
|
|
2304
|
+
position:absolute;inset:0;display:flex;flex-direction:column;
|
|
2305
|
+
align-items:center;justify-content:center;gap:12px;color:#94a3b8;
|
|
2306
|
+
}
|
|
2307
|
+
#empty-state p{font-size:14px}
|
|
2308
|
+
/* Theme toggle */
|
|
2309
|
+
#theme-btn{
|
|
2310
|
+
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2311
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2312
|
+
font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
|
|
2313
|
+
}
|
|
2314
|
+
/* Dark mode overrides (toggled via JS) */
|
|
2315
|
+
body.dark{background:#0f172a;color:#e2e8f0}
|
|
2316
|
+
body.dark #topbar{background:#1e293b;border-color:#334155}
|
|
2317
|
+
body.dark #search-box{background:#334155}
|
|
2318
|
+
body.dark #detail-panel{background:#1e293b;border-color:#334155}
|
|
2319
|
+
body.dark .tb-btn,body.dark .zoom-btn,body.dark .detail-btn,body.dark #connect-btn,body.dark #theme-btn{
|
|
2320
|
+
background:#1e293b;border-color:#334155;color:#e2e8f0;
|
|
2321
|
+
}
|
|
2322
|
+
/* Light mode overrides */
|
|
2323
|
+
body.light{background:#f8fafc;color:#1e293b}
|
|
2324
|
+
body.light #topbar{background:#fff;border-color:#e2e8f0}
|
|
2325
|
+
/* Connection hint */
|
|
2326
|
+
#connect-hint{
|
|
2327
|
+
position:absolute;top:12px;left:50%;transform:translateX(-50%);
|
|
2328
|
+
background:#fef3c7;border:1px solid #f59e0b;color:#92400e;
|
|
2329
|
+
padding:6px 14px;border-radius:20px;font-size:12px;font-weight:500;
|
|
2330
|
+
display:none;z-index:20;pointer-events:none;
|
|
2331
|
+
}
|
|
2332
|
+
/* Screen reader only */
|
|
2333
|
+
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}
|
|
2334
|
+
</style>
|
|
2335
|
+
</head>
|
|
2336
|
+
<body class="${meta.theme}">
|
|
2337
|
+
<!-- Top bar -->
|
|
2338
|
+
<div id="topbar">
|
|
2339
|
+
<h1>Data Cartography Map</h1>
|
|
2340
|
+
<div id="search-box">
|
|
2341
|
+
<span style="color:#94a3b8;font-size:14px">⌕</span>
|
|
2342
|
+
<input id="search-input" type="text" placeholder="Search assets..." aria-label="Search data assets"/>
|
|
2343
|
+
</div>
|
|
2344
|
+
<button id="theme-btn" title="Toggle dark/light mode" aria-label="Toggle theme">${meta.theme === "dark" ? "☼" : "☾"}</button>
|
|
2345
|
+
</div>
|
|
2346
|
+
<!-- SR summary -->
|
|
2347
|
+
<div class="sr-only" role="status" aria-live="polite" id="sr-summary">
|
|
2348
|
+
Data cartography map with ${assets.length} assets in ${clusters.length} clusters.
|
|
2349
|
+
</div>
|
|
2350
|
+
<!-- Main area -->
|
|
2351
|
+
<div id="main">
|
|
2352
|
+
<div id="canvas-wrap" role="application" aria-label="Data cartography hex map" tabindex="0">
|
|
2353
|
+
<canvas id="hexmap" aria-hidden="true"></canvas>
|
|
2354
|
+
${isEmpty ? '<div id="empty-state"><p style="font-size:48px">🗺</p><p>No data assets available</p><p style="font-size:12px">Run <code>datasynx-cartography discover</code> to populate the map</p></div>' : ""}
|
|
2355
|
+
</div>
|
|
2356
|
+
<div id="detail-panel" role="complementary" aria-label="Asset details">
|
|
2357
|
+
<div class="panel-header">
|
|
2358
|
+
<h3 id="dp-name">—</h3>
|
|
2359
|
+
<button class="close-btn" id="dp-close" aria-label="Close panel">✕</button>
|
|
2360
|
+
</div>
|
|
2361
|
+
<div class="panel-body" id="dp-body"></div>
|
|
2362
|
+
</div>
|
|
2363
|
+
</div>
|
|
2364
|
+
<!-- Bottom-left toolbar -->
|
|
2365
|
+
<div id="toolbar-left">
|
|
2366
|
+
<button class="tb-btn active" id="btn-labels" title="Show labels" aria-pressed="true" aria-label="Toggle labels">🏷</button>
|
|
2367
|
+
<button class="tb-btn" id="btn-quality" title="Quality layer" aria-pressed="false" aria-label="Toggle quality layer">👁</button>
|
|
2368
|
+
</div>
|
|
2369
|
+
<!-- Bottom-right toolbar -->
|
|
2370
|
+
<div id="toolbar-right">
|
|
2371
|
+
<div id="zoom-controls">
|
|
2372
|
+
<button class="zoom-btn" id="zoom-out" aria-label="Zoom out">−</button>
|
|
2373
|
+
<span id="zoom-pct">100%</span>
|
|
2374
|
+
<button class="zoom-btn" id="zoom-in" aria-label="Zoom in">+</button>
|
|
2375
|
+
</div>
|
|
2376
|
+
<div id="detail-selector">
|
|
2377
|
+
<button class="detail-btn" id="dl-1" aria-label="Detail level 1">1</button>
|
|
2378
|
+
<button class="detail-btn active" id="dl-2" aria-label="Detail level 2">2</button>
|
|
2379
|
+
<button class="detail-btn" id="dl-3" aria-label="Detail level 3">3</button>
|
|
2380
|
+
<button class="detail-btn" id="dl-4" aria-label="Detail level 4">4</button>
|
|
2381
|
+
</div>
|
|
2382
|
+
<button id="connect-btn" title="Connection tool" aria-label="Toggle connection tool">🔗</button>
|
|
2383
|
+
</div>
|
|
2384
|
+
<!-- Connection hint -->
|
|
2385
|
+
<div id="connect-hint">Click two assets to create a connection</div>
|
|
2386
|
+
<!-- Tooltip -->
|
|
2387
|
+
<div id="tooltip" role="tooltip">
|
|
2388
|
+
<div class="tt-name" id="tt-name"></div>
|
|
2389
|
+
<div class="tt-domain" id="tt-domain"></div>
|
|
2390
|
+
<div class="tt-quality" id="tt-quality"></div>
|
|
2391
|
+
</div>
|
|
2392
|
+
|
|
2393
|
+
<script>
|
|
2394
|
+
(function() {
|
|
2395
|
+
'use strict';
|
|
2396
|
+
|
|
2397
|
+
// \u2500\u2500 Data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2398
|
+
const MAP = ${dataJson};
|
|
2399
|
+
const HEX_SIZE = ${HEX_SIZE2};
|
|
2400
|
+
const IS_EMPTY = ${isEmpty};
|
|
2401
|
+
|
|
2402
|
+
// Build asset index
|
|
2403
|
+
const assetIndex = new Map();
|
|
2404
|
+
const clusterByAsset = new Map();
|
|
2405
|
+
for (const c of MAP.clusters) {
|
|
2406
|
+
for (const aid of c.assetIds) {
|
|
2407
|
+
clusterByAsset.set(aid, c);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
for (const a of MAP.assets) {
|
|
2411
|
+
assetIndex.set(a.id, a);
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// \u2500\u2500 Canvas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2415
|
+
const canvas = document.getElementById('hexmap');
|
|
2416
|
+
const ctx = canvas.getContext('2d');
|
|
2417
|
+
const wrap = document.getElementById('canvas-wrap');
|
|
2418
|
+
let W = 0, H = 0;
|
|
2419
|
+
|
|
2420
|
+
function resize() {
|
|
2421
|
+
const dpr = window.devicePixelRatio || 1;
|
|
2422
|
+
W = wrap.clientWidth; H = wrap.clientHeight;
|
|
2423
|
+
canvas.width = W * dpr; canvas.height = H * dpr;
|
|
2424
|
+
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
|
|
2425
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2426
|
+
draw();
|
|
2427
|
+
}
|
|
2428
|
+
window.addEventListener('resize', resize);
|
|
2429
|
+
|
|
2430
|
+
// \u2500\u2500 Viewport \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2431
|
+
let vx = 0, vy = 0, scale = 1;
|
|
2432
|
+
let detailLevel = 2, showLabels = true, showQuality = false;
|
|
2433
|
+
let isDark = document.body.classList.contains('dark');
|
|
2434
|
+
let connectMode = false, connectFirst = null;
|
|
2435
|
+
let hoveredAssetId = null, selectedAssetId = null;
|
|
2436
|
+
let searchQuery = '';
|
|
2437
|
+
let localConnections = [...MAP.connections];
|
|
2438
|
+
|
|
2439
|
+
// Flat-top hex math
|
|
2440
|
+
function htp_x(q, r) { return HEX_SIZE * (3/2 * q); }
|
|
2441
|
+
function htp_y(q, r) { return HEX_SIZE * (Math.sqrt(3)/2 * q + Math.sqrt(3) * r); }
|
|
2442
|
+
function w2s(wx, wy) { return { x: wx*scale+vx, y: wy*scale+vy }; }
|
|
2443
|
+
function s2w(sx, sy) { return { x: (sx-vx)/scale, y: (sy-vy)/scale }; }
|
|
2444
|
+
|
|
2445
|
+
function fitToView() {
|
|
2446
|
+
if (IS_EMPTY || MAP.assets.length === 0) { vx = 0; vy = 0; scale = 1; return; }
|
|
2447
|
+
let mnx=Infinity,mny=Infinity,mxx=-Infinity,mxy=-Infinity;
|
|
2448
|
+
for (const a of MAP.assets) {
|
|
2449
|
+
const px=htp_x(a.q,a.r), py=htp_y(a.q,a.r);
|
|
2450
|
+
if(px<mnx)mnx=px;if(py<mny)mny=py;if(px>mxx)mxx=px;if(py>mxy)mxy=py;
|
|
2451
|
+
}
|
|
2452
|
+
const pw=mxx-mnx+HEX_SIZE*4, ph=mxy-mny+HEX_SIZE*4;
|
|
2453
|
+
scale = Math.min(W/pw, H/ph, 2) * 0.85;
|
|
2454
|
+
vx = W/2 - ((mnx+mxx)/2)*scale;
|
|
2455
|
+
vy = H/2 - ((mny+mxy)/2)*scale;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// \u2500\u2500 Drawing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2459
|
+
function hexPath(cx, cy, r) {
|
|
2460
|
+
ctx.beginPath();
|
|
2461
|
+
for (let i=0;i<6;i++) {
|
|
2462
|
+
const angle = Math.PI/180*(60*i);
|
|
2463
|
+
const x=cx+r*Math.cos(angle), y=cy+r*Math.sin(angle);
|
|
2464
|
+
i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);
|
|
2465
|
+
}
|
|
2466
|
+
ctx.closePath();
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
function shadeV(hex, amt) {
|
|
2470
|
+
if(!hex||hex.length<7)return hex;
|
|
2471
|
+
const n=parseInt(hex.replace('#',''),16);
|
|
2472
|
+
const r=Math.min(255,(n>>16)+amt), g=Math.min(255,((n>>8)&0xff)+amt), b=Math.min(255,(n&0xff)+amt);
|
|
2473
|
+
return '#'+r.toString(16).padStart(2,'0')+g.toString(16).padStart(2,'0')+b.toString(16).padStart(2,'0');
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
function draw() {
|
|
2477
|
+
ctx.clearRect(0,0,W,H);
|
|
2478
|
+
ctx.fillStyle = isDark ? '#0f172a' : '#f8fafc';
|
|
2479
|
+
ctx.fillRect(0,0,W,H);
|
|
2480
|
+
if (IS_EMPTY) return;
|
|
2481
|
+
|
|
2482
|
+
const size = HEX_SIZE * scale;
|
|
2483
|
+
const matchedIds = getSearchMatches();
|
|
2484
|
+
const hasSearch = searchQuery.length > 0;
|
|
2485
|
+
|
|
2486
|
+
// Draw connections
|
|
2487
|
+
ctx.save();
|
|
2488
|
+
ctx.strokeStyle = isDark ? 'rgba(148,163,184,0.35)' : 'rgba(100,116,139,0.25)';
|
|
2489
|
+
ctx.lineWidth = 1.5;
|
|
2490
|
+
ctx.setLineDash([4,4]);
|
|
2491
|
+
for (const conn of localConnections) {
|
|
2492
|
+
const src = assetIndex.get(conn.sourceAssetId);
|
|
2493
|
+
const tgt = assetIndex.get(conn.targetAssetId);
|
|
2494
|
+
if (!src||!tgt) continue;
|
|
2495
|
+
const sp=w2s(htp_x(src.q,src.r),htp_y(src.q,src.r));
|
|
2496
|
+
const tp=w2s(htp_x(tgt.q,tgt.r),htp_y(tgt.q,tgt.r));
|
|
2497
|
+
ctx.beginPath();ctx.moveTo(sp.x,sp.y);ctx.lineTo(tp.x,tp.y);ctx.stroke();
|
|
2498
|
+
}
|
|
2499
|
+
ctx.setLineDash([]);
|
|
2500
|
+
ctx.restore();
|
|
2501
|
+
|
|
2502
|
+
// Draw hexagons per cluster
|
|
2503
|
+
for (const cluster of MAP.clusters) {
|
|
2504
|
+
const baseColor = cluster.color;
|
|
2505
|
+
const clusterAssets = cluster.assetIds.map(id=>assetIndex.get(id)).filter(Boolean);
|
|
2506
|
+
const isClusterMatch = !hasSearch || clusterAssets.some(a => matchedIds.has(a.id));
|
|
2507
|
+
const clusterDim = hasSearch && !isClusterMatch;
|
|
2508
|
+
|
|
2509
|
+
for (let ai=0; ai<clusterAssets.length; ai++) {
|
|
2510
|
+
const asset = clusterAssets[ai];
|
|
2511
|
+
const wx=htp_x(asset.q,asset.r), wy=htp_y(asset.q,asset.r);
|
|
2512
|
+
const s=w2s(wx,wy);
|
|
2513
|
+
const cx=s.x, cy=s.y;
|
|
2514
|
+
|
|
2515
|
+
// Frustum cull
|
|
2516
|
+
if(cx+size<0||cx-size>W||cy+size<0||cy-size>H) continue;
|
|
2517
|
+
|
|
2518
|
+
// Shade variation
|
|
2519
|
+
const shade = ai%3===0?18:ai%3===1?8:0;
|
|
2520
|
+
let fillColor = shadeV(baseColor, shade);
|
|
2521
|
+
|
|
2522
|
+
// Quality overlay
|
|
2523
|
+
if (showQuality && asset.qualityScore !== null && asset.qualityScore !== undefined) {
|
|
2524
|
+
const q = asset.qualityScore;
|
|
2525
|
+
if (q < 40) fillColor = '#ef4444';
|
|
2526
|
+
else if (q < 70) fillColor = '#f97316';
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
const alpha = clusterDim ? 0.18 : 1;
|
|
2530
|
+
const isHovered = asset.id === hoveredAssetId;
|
|
2531
|
+
const isSelected = asset.id === selectedAssetId;
|
|
2532
|
+
const isConnectFirst = asset.id === connectFirst;
|
|
2533
|
+
|
|
2534
|
+
ctx.save();
|
|
2535
|
+
ctx.globalAlpha = alpha;
|
|
2536
|
+
hexPath(cx, cy, size*0.92);
|
|
2537
|
+
|
|
2538
|
+
if (isDark && (isHovered||isSelected||isConnectFirst)) {
|
|
2539
|
+
ctx.shadowColor = fillColor;
|
|
2540
|
+
ctx.shadowBlur = isSelected ? 16 : 8;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
ctx.fillStyle = fillColor;
|
|
2544
|
+
ctx.fill();
|
|
2545
|
+
|
|
2546
|
+
if (isSelected||isConnectFirst) {
|
|
2547
|
+
ctx.strokeStyle = isConnectFirst ? '#f59e0b' : '#fff';
|
|
2548
|
+
ctx.lineWidth = 2.5;
|
|
2549
|
+
ctx.stroke();
|
|
2550
|
+
} else if (isHovered) {
|
|
2551
|
+
ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)';
|
|
2552
|
+
ctx.lineWidth = 1.5;
|
|
2553
|
+
ctx.stroke();
|
|
2554
|
+
} else {
|
|
2555
|
+
ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.4)';
|
|
2556
|
+
ctx.lineWidth = 1;
|
|
2557
|
+
ctx.stroke();
|
|
2558
|
+
}
|
|
2559
|
+
ctx.restore();
|
|
2560
|
+
|
|
2561
|
+
// Quality dot
|
|
2562
|
+
if (showQuality && asset.qualityScore!==null && asset.qualityScore!==undefined && size>8) {
|
|
2563
|
+
const q = asset.qualityScore;
|
|
2564
|
+
if (q < 70) {
|
|
2565
|
+
ctx.beginPath();
|
|
2566
|
+
ctx.arc(cx+size*0.4, cy-size*0.4, Math.max(3,size*0.14), 0, Math.PI*2);
|
|
2567
|
+
ctx.fillStyle = q<40?'#ef4444':'#f97316';
|
|
2568
|
+
ctx.fill();
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// Asset labels (detail 4, or 3 at high zoom)
|
|
2573
|
+
const showAssetLabel = showLabels && !clusterDim &&
|
|
2574
|
+
((detailLevel>=4)||(detailLevel===3 && scale>=0.8));
|
|
2575
|
+
if (showAssetLabel && size>14) {
|
|
2576
|
+
const label = asset.name.length>12 ? asset.name.substring(0,11)+'...' : asset.name;
|
|
2577
|
+
ctx.save();
|
|
2578
|
+
ctx.font = Math.max(8,Math.min(11,size*0.38))+'px -apple-system,sans-serif';
|
|
2579
|
+
ctx.fillStyle = isDark ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.9)';
|
|
2580
|
+
ctx.textAlign='center';ctx.textBaseline='middle';
|
|
2581
|
+
ctx.fillText(label, cx, cy);
|
|
2582
|
+
ctx.restore();
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// Cluster labels (pill badges)
|
|
2588
|
+
if (showLabels && detailLevel>=1) {
|
|
2589
|
+
for (const cluster of MAP.clusters) {
|
|
2590
|
+
if (cluster.assetIds.length===0) continue;
|
|
2591
|
+
if (hasSearch && !cluster.assetIds.some(id=>matchedIds.has(id))) continue;
|
|
2592
|
+
const s=w2s(cluster.centroid.x, cluster.centroid.y);
|
|
2593
|
+
drawPill(s.x, s.y-size*1.2, cluster.label, cluster.color, 14);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// Sub-domain labels (detail 2+)
|
|
2598
|
+
if (showLabels && detailLevel>=2) {
|
|
2599
|
+
const subGroups = new Map();
|
|
2600
|
+
for (const a of MAP.assets) {
|
|
2601
|
+
if (!a.subDomain) continue;
|
|
2602
|
+
const key = a.domain+'|'+a.subDomain;
|
|
2603
|
+
if (!subGroups.has(key)) subGroups.set(key, []);
|
|
2604
|
+
subGroups.get(key).push(a);
|
|
2605
|
+
}
|
|
2606
|
+
for (const [, group] of subGroups) {
|
|
2607
|
+
let sx=0,sy=0;
|
|
2608
|
+
for (const a of group) { sx+=htp_x(a.q,a.r); sy+=htp_y(a.q,a.r); }
|
|
2609
|
+
const cx=sx/group.length, cy=sy/group.length;
|
|
2610
|
+
const s = w2s(cx, cy);
|
|
2611
|
+
drawPill(s.x, s.y+size*1.5, group[0].subDomain, '#64748b', 11);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
function drawPill(x, y, text, color, fontSize) {
|
|
2617
|
+
if(!text) return;
|
|
2618
|
+
ctx.save();
|
|
2619
|
+
ctx.font = '600 '+fontSize+'px -apple-system,sans-serif';
|
|
2620
|
+
const tw=ctx.measureText(text).width;
|
|
2621
|
+
const ph=fontSize+8, pw=tw+20;
|
|
2622
|
+
ctx.beginPath();
|
|
2623
|
+
if (ctx.roundRect) ctx.roundRect(x-pw/2, y-ph/2, pw, ph, ph/2);
|
|
2624
|
+
else { ctx.rect(x-pw/2, y-ph/2, pw, ph); }
|
|
2625
|
+
ctx.fillStyle = isDark ? 'rgba(30,41,59,0.9)' : 'rgba(255,255,255,0.92)';
|
|
2626
|
+
ctx.shadowColor='rgba(0,0,0,0.15)'; ctx.shadowBlur=6;
|
|
2627
|
+
ctx.fill(); ctx.shadowBlur=0;
|
|
2628
|
+
ctx.fillStyle = isDark ? '#e2e8f0' : '#0f172a';
|
|
2629
|
+
ctx.textAlign='center'; ctx.textBaseline='middle';
|
|
2630
|
+
ctx.fillText(text, x, y);
|
|
2631
|
+
ctx.restore();
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
// \u2500\u2500 Hit testing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2635
|
+
function getAssetAt(sx, sy) {
|
|
2636
|
+
const w=s2w(sx,sy);
|
|
2637
|
+
for (const a of MAP.assets) {
|
|
2638
|
+
const wx=htp_x(a.q,a.r), wy=htp_y(a.q,a.r);
|
|
2639
|
+
const dx=Math.abs(w.x-wx), dy=Math.abs(w.y-wy);
|
|
2640
|
+
if (dx>HEX_SIZE||dy>HEX_SIZE) continue;
|
|
2641
|
+
if (dx*dx+dy*dy < HEX_SIZE*HEX_SIZE) return a;
|
|
2642
|
+
}
|
|
2643
|
+
return null;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// \u2500\u2500 Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2647
|
+
function getSearchMatches() {
|
|
2648
|
+
if(!searchQuery) return new Set();
|
|
2649
|
+
const q=searchQuery.toLowerCase();
|
|
2650
|
+
const m=new Set();
|
|
2651
|
+
for(const a of MAP.assets){
|
|
2652
|
+
if(a.name.toLowerCase().includes(q)||(a.domain&&a.domain.toLowerCase().includes(q))||
|
|
2653
|
+
(a.subDomain&&a.subDomain.toLowerCase().includes(q))) m.add(a.id);
|
|
2654
|
+
}
|
|
2655
|
+
return m;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
// \u2500\u2500 Pan & Zoom \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2659
|
+
let dragging=false, lastMX=0, lastMY=0;
|
|
2660
|
+
|
|
2661
|
+
wrap.addEventListener('mousedown', e=>{
|
|
2662
|
+
if(e.button!==0)return;
|
|
2663
|
+
dragging=true; lastMX=e.clientX; lastMY=e.clientY;
|
|
2664
|
+
wrap.classList.add('dragging');
|
|
2665
|
+
});
|
|
2666
|
+
window.addEventListener('mouseup', ()=>{dragging=false;wrap.classList.remove('dragging');});
|
|
2667
|
+
window.addEventListener('mousemove', e=>{
|
|
2668
|
+
if(dragging){
|
|
2669
|
+
vx+=e.clientX-lastMX; vy+=e.clientY-lastMY;
|
|
2670
|
+
lastMX=e.clientX; lastMY=e.clientY; draw(); return;
|
|
2671
|
+
}
|
|
2672
|
+
const rect=wrap.getBoundingClientRect();
|
|
2673
|
+
const sx=e.clientX-rect.left, sy=e.clientY-rect.top;
|
|
2674
|
+
const asset=getAssetAt(sx,sy);
|
|
2675
|
+
const newId=asset?asset.id:null;
|
|
2676
|
+
if(newId!==hoveredAssetId){hoveredAssetId=newId;draw();}
|
|
2677
|
+
const tt=document.getElementById('tooltip');
|
|
2678
|
+
if(asset){
|
|
2679
|
+
document.getElementById('tt-name').textContent=asset.name;
|
|
2680
|
+
document.getElementById('tt-domain').textContent=asset.domain+(asset.subDomain?' > '+asset.subDomain:'');
|
|
2681
|
+
document.getElementById('tt-quality').textContent=asset.qualityScore!==null?'Quality: '+asset.qualityScore+'/100':'';
|
|
2682
|
+
tt.style.display='block';tt.style.left=(e.clientX+12)+'px';tt.style.top=(e.clientY-8)+'px';
|
|
2683
|
+
} else { tt.style.display='none'; }
|
|
2684
|
+
});
|
|
2685
|
+
|
|
2686
|
+
wrap.addEventListener('click', e=>{
|
|
2687
|
+
const rect=wrap.getBoundingClientRect();
|
|
2688
|
+
const sx=e.clientX-rect.left, sy=e.clientY-rect.top;
|
|
2689
|
+
const asset=getAssetAt(sx,sy);
|
|
2690
|
+
if(connectMode){
|
|
2691
|
+
if(!asset) return;
|
|
2692
|
+
if(!connectFirst){connectFirst=asset.id;draw();}
|
|
2693
|
+
else if(connectFirst!==asset.id){
|
|
2694
|
+
localConnections.push({id:crypto.randomUUID(),sourceAssetId:connectFirst,targetAssetId:asset.id,type:'connection'});
|
|
2695
|
+
connectFirst=null;draw();
|
|
2696
|
+
}
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
if(asset){selectedAssetId=asset.id;showDetailPanel(asset);}
|
|
2700
|
+
else{selectedAssetId=null;document.getElementById('detail-panel').classList.remove('open');}
|
|
2701
|
+
draw();
|
|
2702
|
+
});
|
|
2703
|
+
|
|
2704
|
+
// Touch
|
|
2705
|
+
let lastTouches=[];
|
|
2706
|
+
wrap.addEventListener('touchstart',e=>{lastTouches=[...e.touches];},{passive:true});
|
|
2707
|
+
wrap.addEventListener('touchmove',e=>{
|
|
2708
|
+
if(e.touches.length===1){
|
|
2709
|
+
vx+=e.touches[0].clientX-lastTouches[0].clientX;
|
|
2710
|
+
vy+=e.touches[0].clientY-lastTouches[0].clientY;draw();
|
|
2711
|
+
} else if(e.touches.length===2){
|
|
2712
|
+
const d0=Math.hypot(lastTouches[0].clientX-lastTouches[1].clientX,lastTouches[0].clientY-lastTouches[1].clientY);
|
|
2713
|
+
const d1=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);
|
|
2714
|
+
const mx=(e.touches[0].clientX+e.touches[1].clientX)/2;
|
|
2715
|
+
const my=(e.touches[0].clientY+e.touches[1].clientY)/2;
|
|
2716
|
+
applyZoom(d1/d0,mx,my);
|
|
2717
|
+
}
|
|
2718
|
+
lastTouches=[...e.touches];
|
|
2719
|
+
},{passive:true});
|
|
2720
|
+
|
|
2721
|
+
wrap.addEventListener('wheel',e=>{
|
|
2722
|
+
e.preventDefault();
|
|
2723
|
+
const rect=wrap.getBoundingClientRect();
|
|
2724
|
+
applyZoom(e.deltaY<0?1.12:1/1.12,e.clientX-rect.left,e.clientY-rect.top);
|
|
2725
|
+
},{passive:false});
|
|
2726
|
+
|
|
2727
|
+
function applyZoom(factor,sx,sy){
|
|
2728
|
+
const ns=Math.max(0.05,Math.min(8,scale*factor));
|
|
2729
|
+
const wx=(sx-vx)/scale,wy=(sy-vy)/scale;
|
|
2730
|
+
scale=ns;vx=sx-wx*scale;vy=sy-wy*scale;
|
|
2731
|
+
document.getElementById('zoom-pct').textContent=Math.round(scale*100)+'%';draw();
|
|
2732
|
+
}
|
|
2733
|
+
document.getElementById('zoom-in').addEventListener('click',()=>applyZoom(1.25,W/2,H/2));
|
|
2734
|
+
document.getElementById('zoom-out').addEventListener('click',()=>applyZoom(1/1.25,W/2,H/2));
|
|
2735
|
+
|
|
2736
|
+
// Keyboard
|
|
2737
|
+
wrap.addEventListener('keydown',e=>{
|
|
2738
|
+
const step=40;
|
|
2739
|
+
if(e.key==='ArrowLeft'){vx+=step;draw();}
|
|
2740
|
+
else if(e.key==='ArrowRight'){vx-=step;draw();}
|
|
2741
|
+
else if(e.key==='ArrowUp'){vy+=step;draw();}
|
|
2742
|
+
else if(e.key==='ArrowDown'){vy-=step;draw();}
|
|
2743
|
+
else if(e.key==='+'||e.key==='=')applyZoom(1.2,W/2,H/2);
|
|
2744
|
+
else if(e.key==='-')applyZoom(1/1.2,W/2,H/2);
|
|
2745
|
+
else if(e.key==='Escape'){
|
|
2746
|
+
selectedAssetId=null;document.getElementById('detail-panel').classList.remove('open');
|
|
2747
|
+
if(connectMode)toggleConnect();draw();
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
2750
|
+
|
|
2751
|
+
// \u2500\u2500 Detail Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2752
|
+
function showDetailPanel(asset) {
|
|
2753
|
+
document.getElementById('dp-name').textContent=asset.name;
|
|
2754
|
+
const body=document.getElementById('dp-body');
|
|
2755
|
+
const rows=[['Domain',asset.domain],['Sub-domain',asset.subDomain],
|
|
2756
|
+
['Quality Score',asset.qualityScore!==null?renderQuality(asset.qualityScore):null],
|
|
2757
|
+
...Object.entries(asset.metadata||{}).slice(0,8).map(([k,v])=>[k,String(v)])
|
|
2758
|
+
].filter(([,v])=>v!==null&&v!==undefined&&v!=='');
|
|
2759
|
+
body.innerHTML=rows.map(([l,v])=>'<div class="meta-row"><div class="meta-label">'+esc(String(l))+'</div><div class="meta-value">'+v+'</div></div>').join('');
|
|
2760
|
+
const related=localConnections.filter(c=>c.sourceAssetId===asset.id||c.targetAssetId===asset.id);
|
|
2761
|
+
if(related.length>0){
|
|
2762
|
+
body.innerHTML+='<div class="meta-row"><div class="meta-label">Connections ('+related.length+')</div><div>'+
|
|
2763
|
+
related.map(c=>{const oid=c.sourceAssetId===asset.id?c.targetAssetId:c.sourceAssetId;
|
|
2764
|
+
const o=assetIndex.get(oid);return '<div class="meta-value" style="margin-top:4px;font-size:12px">'+(o?esc(o.name):oid)+'</div>';}).join('')+'</div></div>';
|
|
2765
|
+
}
|
|
2766
|
+
document.getElementById('detail-panel').classList.add('open');
|
|
2767
|
+
}
|
|
2768
|
+
function renderQuality(s){
|
|
2769
|
+
const c=s>=70?'#22c55e':s>=40?'#f97316':'#ef4444';
|
|
2770
|
+
return s+'/100 <div class="quality-bar"><div class="quality-fill" style="width:'+s+'%;background:'+c+'"></div></div>';
|
|
2771
|
+
}
|
|
2772
|
+
function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
2773
|
+
document.getElementById('dp-close').addEventListener('click',()=>{
|
|
2774
|
+
document.getElementById('detail-panel').classList.remove('open');selectedAssetId=null;draw();
|
|
2775
|
+
});
|
|
2776
|
+
|
|
2777
|
+
// \u2500\u2500 Toolbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2778
|
+
[1,2,3,4].forEach(n=>{
|
|
2779
|
+
document.getElementById('dl-'+n).addEventListener('click',()=>{
|
|
2780
|
+
detailLevel=n;document.querySelectorAll('.detail-btn').forEach(b=>b.classList.remove('active'));
|
|
2781
|
+
document.getElementById('dl-'+n).classList.add('active');draw();
|
|
2782
|
+
});
|
|
2783
|
+
});
|
|
2784
|
+
document.getElementById('btn-labels').addEventListener('click',()=>{
|
|
2785
|
+
showLabels=!showLabels;document.getElementById('btn-labels').classList.toggle('active',showLabels);draw();
|
|
2786
|
+
});
|
|
2787
|
+
document.getElementById('btn-quality').addEventListener('click',()=>{
|
|
2788
|
+
showQuality=!showQuality;document.getElementById('btn-quality').classList.toggle('active',showQuality);draw();
|
|
2789
|
+
});
|
|
2790
|
+
function toggleConnect(){
|
|
2791
|
+
connectMode=!connectMode;connectFirst=null;
|
|
2792
|
+
document.getElementById('connect-btn').classList.toggle('active',connectMode);
|
|
2793
|
+
wrap.classList.toggle('connecting',connectMode);
|
|
2794
|
+
document.getElementById('connect-hint').style.display=connectMode?'block':'none';draw();
|
|
2795
|
+
}
|
|
2796
|
+
document.getElementById('connect-btn').addEventListener('click',toggleConnect);
|
|
2797
|
+
document.getElementById('theme-btn').addEventListener('click',()=>{
|
|
2798
|
+
isDark=!isDark;
|
|
2799
|
+
document.body.classList.toggle('dark',isDark);document.body.classList.toggle('light',!isDark);
|
|
2800
|
+
document.getElementById('theme-btn').innerHTML=isDark?'☼':'☾';draw();
|
|
2801
|
+
});
|
|
2802
|
+
document.getElementById('search-input').addEventListener('input',e=>{searchQuery=e.target.value.trim();draw();});
|
|
2803
|
+
|
|
2804
|
+
// \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2805
|
+
resize(); fitToView();
|
|
2806
|
+
document.getElementById('zoom-pct').textContent=Math.round(scale*100)+'%';
|
|
2807
|
+
draw();
|
|
2808
|
+
})();
|
|
2809
|
+
</script>
|
|
2810
|
+
</body>
|
|
2811
|
+
</html>`;
|
|
2812
|
+
}
|
|
2813
|
+
function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "map", "sops"]) {
|
|
1884
2814
|
mkdirSync2(outputDir, { recursive: true });
|
|
1885
2815
|
mkdirSync2(join2(outputDir, "sops"), { recursive: true });
|
|
1886
2816
|
mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
|
|
@@ -1903,6 +2833,10 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
|
|
|
1903
2833
|
writeFileSync(join2(outputDir, "topology.html"), exportHTML(nodes, edges));
|
|
1904
2834
|
process.stderr.write("\u2713 topology.html\n");
|
|
1905
2835
|
}
|
|
2836
|
+
if (formats.includes("map")) {
|
|
2837
|
+
writeFileSync(join2(outputDir, "cartography-map.html"), exportCartographyMap(nodes, edges));
|
|
2838
|
+
process.stderr.write("\u2713 cartography-map.html\n");
|
|
2839
|
+
}
|
|
1906
2840
|
if (formats.includes("sops")) {
|
|
1907
2841
|
const sops = db.getSOPs(sessionId);
|
|
1908
2842
|
for (const sop of sops) {
|
|
@@ -1919,9 +2853,10 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
|
|
|
1919
2853
|
}
|
|
1920
2854
|
|
|
1921
2855
|
// src/cli.ts
|
|
1922
|
-
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
2856
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1923
2857
|
import { resolve } from "path";
|
|
1924
2858
|
import { createInterface } from "readline";
|
|
2859
|
+
import { execSync as execSync2 } from "child_process";
|
|
1925
2860
|
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
1926
2861
|
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
1927
2862
|
var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
@@ -1934,7 +2869,7 @@ function main() {
|
|
|
1934
2869
|
const program = new Command();
|
|
1935
2870
|
const CMD = "datasynx-cartography";
|
|
1936
2871
|
const VERSION = "0.2.3";
|
|
1937
|
-
program.name(CMD).description("AI-powered Infrastructure Cartography").version(VERSION);
|
|
2872
|
+
program.name(CMD).description("AI-powered Infrastructure Cartography & SOP Generation").version(VERSION);
|
|
1938
2873
|
program.command("discover").description("Scan and map your infrastructure").option("--entry <hosts...>", "Entry points", ["localhost"]).option("--depth <n>", "Max crawl depth", "8").option("--max-turns <n>", "Max agent turns", "50").option("--model <m>", "Agent model", "claude-sonnet-4-5-20250929").option("--org <name>", "Organization name (for Backstage)").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--db <path>", "DB path").option("-v, --verbose", "Show agent reasoning", false).action(async (opts) => {
|
|
1939
2874
|
checkPrerequisites();
|
|
1940
2875
|
const config = defaultConfig({
|
|
@@ -2146,23 +3081,22 @@ function main() {
|
|
|
2146
3081
|
exportAll(db, sessionId, config.outputDir);
|
|
2147
3082
|
const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
|
|
2148
3083
|
const htmlPath = resolve(config.outputDir, "topology.html");
|
|
3084
|
+
const mapPath = resolve(config.outputDir, "cartography-map.html");
|
|
2149
3085
|
const topoPath = resolve(config.outputDir, "topology.mermaid");
|
|
2150
3086
|
w("\n");
|
|
2151
|
-
if (existsSync2(
|
|
2152
|
-
|
|
2153
|
-
w(` ${green("\u2192")} ${osc8(fileUrl, bold("Open topology.html"))}
|
|
3087
|
+
if (existsSync2(mapPath)) {
|
|
3088
|
+
w(` ${green("\u2192")} ${osc8(`file://${mapPath}`, bold("Open cartography-map.html"))} ${dim("\u2190 Hex Map")}
|
|
2154
3089
|
`);
|
|
2155
|
-
|
|
3090
|
+
}
|
|
3091
|
+
if (existsSync2(htmlPath)) {
|
|
3092
|
+
w(` ${green("\u2192")} ${osc8(`file://${htmlPath}`, bold("Open topology.html"))}
|
|
2156
3093
|
`);
|
|
2157
3094
|
}
|
|
2158
3095
|
if (existsSync2(topoPath)) {
|
|
2159
3096
|
try {
|
|
2160
3097
|
const code = readFileSync2(topoPath, "utf8");
|
|
2161
|
-
const b64 = Buffer.from(JSON.stringify({ code, mermaid: { theme: "dark" } })).toString("
|
|
2162
|
-
|
|
2163
|
-
w(` ${cyan("\u2192")} ${osc8(liveUrl, bold("Open in mermaid.live"))}
|
|
2164
|
-
`);
|
|
2165
|
-
w(` ${dim(liveUrl)}
|
|
3098
|
+
const b64 = Buffer.from(JSON.stringify({ code, mermaid: { theme: "dark" } })).toString("base64");
|
|
3099
|
+
w(` ${cyan("\u2192")} ${osc8(`https://mermaid.live/view#base64:${b64}`, bold("Open in mermaid.live"))}
|
|
2166
3100
|
`);
|
|
2167
3101
|
} catch {
|
|
2168
3102
|
}
|
|
@@ -2217,7 +3151,7 @@ function main() {
|
|
|
2217
3151
|
}
|
|
2218
3152
|
db.close();
|
|
2219
3153
|
});
|
|
2220
|
-
program.command("export [session-id]").description("Generate all output files").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--format <fmt...>", "Formats: mermaid,json,yaml,html,sops").action((sessionId, opts) => {
|
|
3154
|
+
program.command("export [session-id]").description("Generate all output files").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--format <fmt...>", "Formats: mermaid,json,yaml,html,map,sops").action((sessionId, opts) => {
|
|
2221
3155
|
const config = defaultConfig({ outputDir: opts.output });
|
|
2222
3156
|
const db = new CartographyDB(config.dbPath);
|
|
2223
3157
|
const session = sessionId ? db.getSession(sessionId) : db.getLatestSession();
|
|
@@ -2227,12 +3161,43 @@ function main() {
|
|
|
2227
3161
|
process.exitCode = 1;
|
|
2228
3162
|
return;
|
|
2229
3163
|
}
|
|
2230
|
-
const formats = opts.format ?? ["mermaid", "json", "yaml", "html", "sops"];
|
|
3164
|
+
const formats = opts.format ?? ["mermaid", "json", "yaml", "html", "map", "sops"];
|
|
2231
3165
|
exportAll(db, session.id, opts.output, formats);
|
|
2232
3166
|
process.stderr.write(`\u2713 Exported to: ${opts.output}
|
|
2233
3167
|
`);
|
|
2234
3168
|
db.close();
|
|
2235
3169
|
});
|
|
3170
|
+
program.command("map [session-id]").description("Open the interactive Data Cartography hex map in your browser").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--theme <theme>", "Theme: light or dark", "light").action((sessionId, opts) => {
|
|
3171
|
+
const config = defaultConfig({ outputDir: opts.output });
|
|
3172
|
+
const db = new CartographyDB(config.dbPath);
|
|
3173
|
+
const session = sessionId ? db.getSession(sessionId) : db.getLatestSession();
|
|
3174
|
+
if (!session) {
|
|
3175
|
+
process.stderr.write("No session found. Run discover first.\n");
|
|
3176
|
+
db.close();
|
|
3177
|
+
process.exitCode = 1;
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
const nodes = db.getNodes(session.id);
|
|
3181
|
+
const edges = db.getEdges(session.id);
|
|
3182
|
+
const outDir = resolve(opts.output);
|
|
3183
|
+
mkdirSync3(outDir, { recursive: true });
|
|
3184
|
+
const outPath = resolve(outDir, "cartography-map.html");
|
|
3185
|
+
writeFileSync2(outPath, exportCartographyMap(nodes, edges, { theme: opts.theme }));
|
|
3186
|
+
db.close();
|
|
3187
|
+
const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
|
|
3188
|
+
const fileUrl = `file://${outPath}`;
|
|
3189
|
+
process.stderr.write(`
|
|
3190
|
+
${green("OK")} ${osc8(fileUrl, bold("Open cartography-map.html"))}
|
|
3191
|
+
`);
|
|
3192
|
+
process.stderr.write(` ${dim(fileUrl)}
|
|
3193
|
+
|
|
3194
|
+
`);
|
|
3195
|
+
try {
|
|
3196
|
+
const cmd = process.platform === "darwin" ? `open "${outPath}"` : process.platform === "win32" ? `start "" "${outPath}"` : `xdg-open "${outPath}"`;
|
|
3197
|
+
execSync2(cmd, { stdio: "ignore" });
|
|
3198
|
+
} catch {
|
|
3199
|
+
}
|
|
3200
|
+
});
|
|
2236
3201
|
program.command("show [session-id]").description("Show session details").action((sessionId) => {
|
|
2237
3202
|
const config = defaultConfig();
|
|
2238
3203
|
const db = new CartographyDB(config.dbPath);
|
|
@@ -2476,6 +3441,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2476
3441
|
out(dim(" topology.mermaid Infrastructure topology (graph TB)\n"));
|
|
2477
3442
|
out(dim(" dependencies.mermaid Service dependencies (graph LR)\n"));
|
|
2478
3443
|
out(dim(" topology.html Interactive D3.js force graph\n"));
|
|
3444
|
+
out(dim(" cartography-map.html Hex grid data cartography map\n"));
|
|
2479
3445
|
out(dim(" sops/ Generated SOPs as Markdown\n"));
|
|
2480
3446
|
out(dim(" workflows/ Workflow flowcharts as Mermaid\n"));
|
|
2481
3447
|
out("\n");
|
|
@@ -2617,7 +3583,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2617
3583
|
`);
|
|
2618
3584
|
return;
|
|
2619
3585
|
}
|
|
2620
|
-
const { NODE_TYPES: NODE_TYPES2 } = await import("./types-
|
|
3586
|
+
const { NODE_TYPES: NODE_TYPES2 } = await import("./types-ZD6G5JKR.js");
|
|
2621
3587
|
if (!process.stdin.isTTY) {
|
|
2622
3588
|
w(red("\n \u2717 Interactive mode requires a terminal (use --file for non-interactive)\n\n"));
|
|
2623
3589
|
process.exitCode = 1;
|
|
@@ -2692,7 +3658,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2692
3658
|
`);
|
|
2693
3659
|
});
|
|
2694
3660
|
program.command("doctor").description("Check all requirements and cloud CLIs").action(async () => {
|
|
2695
|
-
const { execSync:
|
|
3661
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
2696
3662
|
const { existsSync: existsSync3, readFileSync: readFileSync3 } = await import("fs");
|
|
2697
3663
|
const { join: join3 } = await import("path");
|
|
2698
3664
|
const out = (s) => process.stdout.write(s);
|
|
@@ -2715,7 +3681,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2715
3681
|
allGood = false;
|
|
2716
3682
|
}
|
|
2717
3683
|
try {
|
|
2718
|
-
const v =
|
|
3684
|
+
const v = execSync3("claude --version", { stdio: "pipe" }).toString().trim();
|
|
2719
3685
|
ok(`Claude CLI ${dim2(v)}`);
|
|
2720
3686
|
} catch {
|
|
2721
3687
|
err("Claude CLI not found \u2014 npm i -g @anthropic-ai/claude-code");
|
|
@@ -2739,7 +3705,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2739
3705
|
allGood = false;
|
|
2740
3706
|
}
|
|
2741
3707
|
try {
|
|
2742
|
-
const v =
|
|
3708
|
+
const v = execSync3("kubectl version --client --short 2>/dev/null || kubectl version --client", { stdio: "pipe" }).toString().split("\n")[0]?.trim() ?? "";
|
|
2743
3709
|
ok(`kubectl ${dim2(v || "(client OK)")}`);
|
|
2744
3710
|
} catch {
|
|
2745
3711
|
warn(`kubectl not found ${dim2("\u2014 install: https://kubernetes.io/docs/tasks/tools/")}`);
|
|
@@ -2751,7 +3717,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2751
3717
|
];
|
|
2752
3718
|
for (const [name, cmd, hint] of cloudClis) {
|
|
2753
3719
|
try {
|
|
2754
|
-
|
|
3720
|
+
execSync3(cmd, { stdio: "pipe" });
|
|
2755
3721
|
ok(`${name} ${dim2("(cloud scanning available)")}`);
|
|
2756
3722
|
} catch {
|
|
2757
3723
|
warn(`${name} not found ${dim2("\u2014 cloud scan skipped | " + hint)}`);
|
|
@@ -2763,7 +3729,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2763
3729
|
];
|
|
2764
3730
|
for (const [name, cmd] of localTools) {
|
|
2765
3731
|
try {
|
|
2766
|
-
|
|
3732
|
+
execSync3(cmd, { stdio: "pipe" });
|
|
2767
3733
|
ok(`${name} ${dim2("(discovery tool)")}`);
|
|
2768
3734
|
} catch {
|
|
2769
3735
|
warn(`${name} not found ${dim2("\u2014 discovery without " + name + " will be limited")}`);
|