@tecture/core 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +30 -0
  3. package/dist/cli.js +77 -3
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts +10 -0
  6. package/dist/index.js +8 -4
  7. package/dist/index.js.map +1 -1
  8. package/dist/public/assets/{_baseUniq-Dhw8ANLN.js → _baseUniq-CUITAAZ7.js} +2 -2
  9. package/dist/public/assets/{_baseUniq-Dhw8ANLN.js.map → _baseUniq-CUITAAZ7.js.map} +1 -1
  10. package/dist/public/assets/{arc-DHr716eG.js → arc-DyFMcdgg.js} +2 -2
  11. package/dist/public/assets/{arc-DHr716eG.js.map → arc-DyFMcdgg.js.map} +1 -1
  12. package/dist/public/assets/{architectureDiagram-Q4EWVU46-CiupZxBl.js → architectureDiagram-Q4EWVU46-DZTi6y6q.js} +2 -2
  13. package/dist/public/assets/{architectureDiagram-Q4EWVU46-CiupZxBl.js.map → architectureDiagram-Q4EWVU46-DZTi6y6q.js.map} +1 -1
  14. package/dist/public/assets/{blockDiagram-DXYQGD6D-CZsYmXdZ.js → blockDiagram-DXYQGD6D-C5DgUxsA.js} +2 -2
  15. package/dist/public/assets/{blockDiagram-DXYQGD6D-CZsYmXdZ.js.map → blockDiagram-DXYQGD6D-C5DgUxsA.js.map} +1 -1
  16. package/dist/public/assets/{c4Diagram-AHTNJAMY-oYpz_hY-.js → c4Diagram-AHTNJAMY-BF_SaG8K.js} +2 -2
  17. package/dist/public/assets/{c4Diagram-AHTNJAMY-oYpz_hY-.js.map → c4Diagram-AHTNJAMY-BF_SaG8K.js.map} +1 -1
  18. package/dist/public/assets/channel-Chx65KuQ.js +2 -0
  19. package/dist/public/assets/{channel-D4DHyqBz.js.map → channel-Chx65KuQ.js.map} +1 -1
  20. package/dist/public/assets/{chunk-4BX2VUAB-BJASz7t9.js → chunk-4BX2VUAB-JU2t15qH.js} +2 -2
  21. package/dist/public/assets/{chunk-4BX2VUAB-BJASz7t9.js.map → chunk-4BX2VUAB-JU2t15qH.js.map} +1 -1
  22. package/dist/public/assets/{chunk-4TB4RGXK-BmBuFub9.js → chunk-4TB4RGXK-BqjiENIB.js} +2 -2
  23. package/dist/public/assets/{chunk-4TB4RGXK-BmBuFub9.js.map → chunk-4TB4RGXK-BqjiENIB.js.map} +1 -1
  24. package/dist/public/assets/chunk-55IACEB6-CwPOewiq.js +2 -0
  25. package/dist/public/assets/{chunk-55IACEB6-DpRX_xgZ.js.map → chunk-55IACEB6-CwPOewiq.js.map} +1 -1
  26. package/dist/public/assets/{chunk-EDXVE4YY-CjyvbsHL.js → chunk-EDXVE4YY-CeAdKClT.js} +2 -2
  27. package/dist/public/assets/{chunk-EDXVE4YY-CjyvbsHL.js.map → chunk-EDXVE4YY-CeAdKClT.js.map} +1 -1
  28. package/dist/public/assets/{chunk-FMBD7UC4-CopKIX9d.js → chunk-FMBD7UC4-BmV52drU.js} +2 -2
  29. package/dist/public/assets/{chunk-FMBD7UC4-CopKIX9d.js.map → chunk-FMBD7UC4-BmV52drU.js.map} +1 -1
  30. package/dist/public/assets/{chunk-OYMX7WX6-Qy94MD6F.js → chunk-OYMX7WX6-DMQ6OBmH.js} +2 -2
  31. package/dist/public/assets/{chunk-OYMX7WX6-Qy94MD6F.js.map → chunk-OYMX7WX6-DMQ6OBmH.js.map} +1 -1
  32. package/dist/public/assets/{chunk-QZHKN3VN-q0VhU4fD.js → chunk-QZHKN3VN-BbECxIVF.js} +2 -2
  33. package/dist/public/assets/{chunk-QZHKN3VN-q0VhU4fD.js.map → chunk-QZHKN3VN-BbECxIVF.js.map} +1 -1
  34. package/dist/public/assets/{chunk-YZCP3GAM-CLHu-ytS.js → chunk-YZCP3GAM-LV4b5o7A.js} +2 -2
  35. package/dist/public/assets/{chunk-YZCP3GAM-CLHu-ytS.js.map → chunk-YZCP3GAM-LV4b5o7A.js.map} +1 -1
  36. package/dist/public/assets/classDiagram-6PBFFD2Q-CuGjbxVu.js +2 -0
  37. package/dist/public/assets/{classDiagram-6PBFFD2Q-C_GcL8CY.js.map → classDiagram-6PBFFD2Q-CuGjbxVu.js.map} +1 -1
  38. package/dist/public/assets/classDiagram-v2-HSJHXN6E-CuGjbxVu.js +2 -0
  39. package/dist/public/assets/{classDiagram-v2-HSJHXN6E-C_GcL8CY.js.map → classDiagram-v2-HSJHXN6E-CuGjbxVu.js.map} +1 -1
  40. package/dist/public/assets/clone-B5zl9853.js +2 -0
  41. package/dist/public/assets/{clone-Cpn4E8-V.js.map → clone-B5zl9853.js.map} +1 -1
  42. package/dist/public/assets/{cose-bilkent-S5V4N54A-BCNrUYFS.js → cose-bilkent-S5V4N54A-NxYN3t_X.js} +2 -2
  43. package/dist/public/assets/{cose-bilkent-S5V4N54A-BCNrUYFS.js.map → cose-bilkent-S5V4N54A-NxYN3t_X.js.map} +1 -1
  44. package/dist/public/assets/{dagre-KV5264BT-BVapQShq.js → dagre-KV5264BT-DTQ94K2e.js} +2 -2
  45. package/dist/public/assets/{dagre-KV5264BT-BVapQShq.js.map → dagre-KV5264BT-DTQ94K2e.js.map} +1 -1
  46. package/dist/public/assets/{diagram-5BDNPKRD-D6B3PE9C.js → diagram-5BDNPKRD-BI7YTuYk.js} +2 -2
  47. package/dist/public/assets/{diagram-5BDNPKRD-D6B3PE9C.js.map → diagram-5BDNPKRD-BI7YTuYk.js.map} +1 -1
  48. package/dist/public/assets/{diagram-G4DWMVQ6-BaWJIFI_.js → diagram-G4DWMVQ6-CRfC6IFe.js} +2 -2
  49. package/dist/public/assets/{diagram-G4DWMVQ6-BaWJIFI_.js.map → diagram-G4DWMVQ6-CRfC6IFe.js.map} +1 -1
  50. package/dist/public/assets/{diagram-MMDJMWI5-CCy9j0cK.js → diagram-MMDJMWI5-CfWU0Xcy.js} +2 -2
  51. package/dist/public/assets/{diagram-MMDJMWI5-CCy9j0cK.js.map → diagram-MMDJMWI5-CfWU0Xcy.js.map} +1 -1
  52. package/dist/public/assets/{diagram-TYMM5635-Dlu7NmTQ.js → diagram-TYMM5635-DUUA764g.js} +2 -2
  53. package/dist/public/assets/{diagram-TYMM5635-Dlu7NmTQ.js.map → diagram-TYMM5635-DUUA764g.js.map} +1 -1
  54. package/dist/public/assets/{erDiagram-SMLLAGMA-X6FgEuRR.js → erDiagram-SMLLAGMA-CC6HpSJv.js} +2 -2
  55. package/dist/public/assets/{erDiagram-SMLLAGMA-X6FgEuRR.js.map → erDiagram-SMLLAGMA-CC6HpSJv.js.map} +1 -1
  56. package/dist/public/assets/{flowDiagram-DWJPFMVM-Bu3JFQlu.js → flowDiagram-DWJPFMVM-BcUwc3Qv.js} +2 -2
  57. package/dist/public/assets/{flowDiagram-DWJPFMVM-Bu3JFQlu.js.map → flowDiagram-DWJPFMVM-BcUwc3Qv.js.map} +1 -1
  58. package/dist/public/assets/{ganttDiagram-T4ZO3ILL-n168zCl5.js → ganttDiagram-T4ZO3ILL-BQ0OUFDb.js} +2 -2
  59. package/dist/public/assets/{ganttDiagram-T4ZO3ILL-n168zCl5.js.map → ganttDiagram-T4ZO3ILL-BQ0OUFDb.js.map} +1 -1
  60. package/dist/public/assets/{gitGraphDiagram-UUTBAWPF-DArjnyo2.js → gitGraphDiagram-UUTBAWPF-BMtxQA7c.js} +2 -2
  61. package/dist/public/assets/{gitGraphDiagram-UUTBAWPF-DArjnyo2.js.map → gitGraphDiagram-UUTBAWPF-BMtxQA7c.js.map} +1 -1
  62. package/dist/public/assets/{graph-Bfm3kqd3.js → graph-D-_LAAxI.js} +2 -2
  63. package/dist/public/assets/{graph-Bfm3kqd3.js.map → graph-D-_LAAxI.js.map} +1 -1
  64. package/dist/public/assets/{index-DMY07AUl.js → index-B08bCLVX.js} +3 -3
  65. package/dist/public/assets/{index-DMY07AUl.js.map → index-B08bCLVX.js.map} +1 -1
  66. package/dist/public/assets/{infoDiagram-42DDH7IO-pBrtdPq_.js → infoDiagram-42DDH7IO-Bamzqbzk.js} +2 -2
  67. package/dist/public/assets/{infoDiagram-42DDH7IO-pBrtdPq_.js.map → infoDiagram-42DDH7IO-Bamzqbzk.js.map} +1 -1
  68. package/dist/public/assets/{ishikawaDiagram-UXIWVN3A-BDIZnWTx.js → ishikawaDiagram-UXIWVN3A-zKsJsf_v.js} +2 -2
  69. package/dist/public/assets/{ishikawaDiagram-UXIWVN3A-BDIZnWTx.js.map → ishikawaDiagram-UXIWVN3A-zKsJsf_v.js.map} +1 -1
  70. package/dist/public/assets/{journeyDiagram-VCZTEJTY-DmSjb-PU.js → journeyDiagram-VCZTEJTY-C-6pvS6y.js} +2 -2
  71. package/dist/public/assets/{journeyDiagram-VCZTEJTY-DmSjb-PU.js.map → journeyDiagram-VCZTEJTY-C-6pvS6y.js.map} +1 -1
  72. package/dist/public/assets/{kanban-definition-6JOO6SKY-C8FgUjbs.js → kanban-definition-6JOO6SKY-DS7zW-ig.js} +2 -2
  73. package/dist/public/assets/{kanban-definition-6JOO6SKY-C8FgUjbs.js.map → kanban-definition-6JOO6SKY-DS7zW-ig.js.map} +1 -1
  74. package/dist/public/assets/{layout-DJh2njKG.js → layout-CVDJjYNO.js} +2 -2
  75. package/dist/public/assets/{layout-DJh2njKG.js.map → layout-CVDJjYNO.js.map} +1 -1
  76. package/dist/public/assets/{linear-kYTx6SLx.js → linear-lUY0N4kd.js} +2 -2
  77. package/dist/public/assets/{linear-kYTx6SLx.js.map → linear-lUY0N4kd.js.map} +1 -1
  78. package/dist/public/assets/{mermaid.core-sEG8CMeA.js → mermaid.core-H7Dmb05b.js} +5 -5
  79. package/dist/public/assets/{mermaid.core-sEG8CMeA.js.map → mermaid.core-H7Dmb05b.js.map} +1 -1
  80. package/dist/public/assets/{min-Cxji_8ZQ.js → min-CXznxZeM.js} +2 -2
  81. package/dist/public/assets/{min-Cxji_8ZQ.js.map → min-CXznxZeM.js.map} +1 -1
  82. package/dist/public/assets/{mindmap-definition-QFDTVHPH-B0-J9mrb.js → mindmap-definition-QFDTVHPH-jXt705eB.js} +2 -2
  83. package/dist/public/assets/{mindmap-definition-QFDTVHPH-B0-J9mrb.js.map → mindmap-definition-QFDTVHPH-jXt705eB.js.map} +1 -1
  84. package/dist/public/assets/{pieDiagram-DEJITSTG-CDZHsOGg.js → pieDiagram-DEJITSTG-Cs44d6OE.js} +2 -2
  85. package/dist/public/assets/{pieDiagram-DEJITSTG-CDZHsOGg.js.map → pieDiagram-DEJITSTG-Cs44d6OE.js.map} +1 -1
  86. package/dist/public/assets/{quadrantDiagram-34T5L4WZ-Cr3XLn8k.js → quadrantDiagram-34T5L4WZ-CpymTeDv.js} +2 -2
  87. package/dist/public/assets/{quadrantDiagram-34T5L4WZ-Cr3XLn8k.js.map → quadrantDiagram-34T5L4WZ-CpymTeDv.js.map} +1 -1
  88. package/dist/public/assets/{requirementDiagram-MS252O5E-DhckNPK2.js → requirementDiagram-MS252O5E-RyLqPw4W.js} +2 -2
  89. package/dist/public/assets/{requirementDiagram-MS252O5E-DhckNPK2.js.map → requirementDiagram-MS252O5E-RyLqPw4W.js.map} +1 -1
  90. package/dist/public/assets/{sankeyDiagram-XADWPNL6-Cst7lZQ5.js → sankeyDiagram-XADWPNL6-DamuB2BV.js} +2 -2
  91. package/dist/public/assets/{sankeyDiagram-XADWPNL6-Cst7lZQ5.js.map → sankeyDiagram-XADWPNL6-DamuB2BV.js.map} +1 -1
  92. package/dist/public/assets/{sequenceDiagram-FGHM5R23-BSz6hk1R.js → sequenceDiagram-FGHM5R23-6-TOylZP.js} +2 -2
  93. package/dist/public/assets/{sequenceDiagram-FGHM5R23-BSz6hk1R.js.map → sequenceDiagram-FGHM5R23-6-TOylZP.js.map} +1 -1
  94. package/dist/public/assets/{stateDiagram-FHFEXIEX-racSpEdx.js → stateDiagram-FHFEXIEX-B6cKPsIC.js} +2 -2
  95. package/dist/public/assets/{stateDiagram-FHFEXIEX-racSpEdx.js.map → stateDiagram-FHFEXIEX-B6cKPsIC.js.map} +1 -1
  96. package/dist/public/assets/stateDiagram-v2-QKLJ7IA2-CXb0Znc0.js +2 -0
  97. package/dist/public/assets/{stateDiagram-v2-QKLJ7IA2-C8oha-gC.js.map → stateDiagram-v2-QKLJ7IA2-CXb0Znc0.js.map} +1 -1
  98. package/dist/public/assets/{timeline-definition-GMOUNBTQ-pBR5gxYm.js → timeline-definition-GMOUNBTQ-CdeBXF6y.js} +2 -2
  99. package/dist/public/assets/{timeline-definition-GMOUNBTQ-pBR5gxYm.js.map → timeline-definition-GMOUNBTQ-CdeBXF6y.js.map} +1 -1
  100. package/dist/public/assets/{vennDiagram-DHZGUBPP-OCE87T_5.js → vennDiagram-DHZGUBPP-D8DqpbrW.js} +2 -2
  101. package/dist/public/assets/{vennDiagram-DHZGUBPP-OCE87T_5.js.map → vennDiagram-DHZGUBPP-D8DqpbrW.js.map} +1 -1
  102. package/dist/public/assets/{wardley-RL74JXVD-691yvuUx.js → wardley-RL74JXVD-B1MU-JlC.js} +2 -2
  103. package/dist/public/assets/{wardley-RL74JXVD-691yvuUx.js.map → wardley-RL74JXVD-B1MU-JlC.js.map} +1 -1
  104. package/dist/public/assets/{wardleyDiagram-NUSXRM2D-CewxpYfR.js → wardleyDiagram-NUSXRM2D-MYKM6CF8.js} +2 -2
  105. package/dist/public/assets/{wardleyDiagram-NUSXRM2D-CewxpYfR.js.map → wardleyDiagram-NUSXRM2D-MYKM6CF8.js.map} +1 -1
  106. package/dist/public/assets/{xychartDiagram-5P7HB3ND-C_kdNFR1.js → xychartDiagram-5P7HB3ND-Brec7d4F.js} +2 -2
  107. package/dist/public/assets/{xychartDiagram-5P7HB3ND-C_kdNFR1.js.map → xychartDiagram-5P7HB3ND-Brec7d4F.js.map} +1 -1
  108. package/dist/public/index.html +1 -1
  109. package/package.json +1 -1
  110. package/dist/public/assets/channel-D4DHyqBz.js +0 -2
  111. package/dist/public/assets/chunk-55IACEB6-DpRX_xgZ.js +0 -2
  112. package/dist/public/assets/classDiagram-6PBFFD2Q-C_GcL8CY.js +0 -2
  113. package/dist/public/assets/classDiagram-v2-HSJHXN6E-C_GcL8CY.js +0 -2
  114. package/dist/public/assets/clone-Cpn4E8-V.js +0 -2
  115. package/dist/public/assets/stateDiagram-v2-QKLJ7IA2-C8oha-gC.js +0 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  All notable changes to `@tecture/core` are documented here.
4
4
 
5
+ ## [0.0.8] — 2026-06-16
6
+
7
+ ### Added
8
+
9
+ - **Anonymous, opt-out usage telemetry** for the standalone viewer. Records only
10
+ the C4 diagram level and counts — never names, paths, or architecture content.
11
+ Disable with `TECTURE_TELEMETRY=0` or `DO_NOT_TRACK=1`; a one-time notice
12
+ prints on first run. (Only the standalone viewer reports; the VS Code webview
13
+ uses the extension's own telemetry, so there's no double-counting.)
14
+ - First `@tecture/core` README, documenting usage and the telemetry policy.
15
+
16
+ ## [0.0.7] — 2026-06-02
17
+
18
+ ### Changed
19
+
20
+ - **Copy deep-dive prompt** now copies a slash command —
21
+ `/architecture-docs deep-dive <id>` — instead of the longer
22
+ "Use the architecture-docs skill to deep-dive…" sentence. Shorter to
23
+ paste, and it invokes the skill directly. The id alone resolves the
24
+ component.
25
+ - Renamed the bundled skill from `tecture` to `architecture-docs`, so
26
+ the deep-dive prompt and skill invocation use the new name.
27
+
5
28
  ## [0.0.6] — 2026-06-02
6
29
 
7
30
  ### Added
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @tecture/core
2
+
3
+ Standalone browser viewer for [Tecture](https://github.com/tecture-io/tecture)
4
+ architecture docs — render the file-based architecture in `./architecture/`
5
+ (JSON + Markdown) as interactive, navigable diagrams.
6
+
7
+ ## Usage
8
+
9
+ From a repo that has an `architecture/` folder:
10
+
11
+ ```bash
12
+ npx @tecture/core
13
+ # → http://localhost:3000
14
+ ```
15
+
16
+ Options: `--port <n>`, `--architecture-path <dir>`, `--tecture-path <dir>`.
17
+
18
+ To generate the architecture in the first place, install the skill into your
19
+ coding agent with `npx @tecture/skill@latest` and run `/architecture-docs`.
20
+
21
+ ## Telemetry
22
+
23
+ The viewer sends **anonymous** usage stats (which C4 diagram **level** is
24
+ viewed, and how often) to help prioritize improvements. It records only the
25
+ level (1/2/3) and counts — **never** your diagram names, node names, file
26
+ paths, or any architecture content. Events are keyed to a random id stored at
27
+ `~/.config/tecture/telemetry-id`.
28
+
29
+ It's opt-out: set `TECTURE_TELEMETRY=0` (or the standard `DO_NOT_TRACK=1`) to
30
+ disable it entirely. A one-time notice is printed on first run.
package/dist/cli.js CHANGED
@@ -150,7 +150,7 @@ function send400(res, body) {
150
150
  }
151
151
  function createArchitectureRouter(opts) {
152
152
  const router = Router2();
153
- const { resolveSource, resolveLayoutStore } = opts;
153
+ const { resolveSource, resolveLayoutStore, report } = opts;
154
154
  router.get("/", async (req, res, next) => {
155
155
  try {
156
156
  const source = await resolveSource(req);
@@ -195,6 +195,10 @@ function createArchitectureRouter(opts) {
195
195
  edges: diagram.edges ?? []
196
196
  };
197
197
  res.json(body);
198
+ report?.capture(
199
+ "diagram.viewed",
200
+ typeof diagram.level === "number" ? { level: diagram.level } : {}
201
+ );
198
202
  } catch (err) {
199
203
  if (err instanceof DiagramNotFoundError) {
200
204
  return send404(res, { error: "diagram_not_found", slug: err.slug });
@@ -353,7 +357,8 @@ function createApp(options) {
353
357
  "/api/architecture",
354
358
  createArchitectureRouter({
355
359
  resolveSource: toSourceResolver(options.source),
356
- resolveLayoutStore: toLayoutStoreResolver(options.layoutStore)
360
+ resolveLayoutStore: toLayoutStoreResolver(options.layoutStore),
361
+ report: options.report
357
362
  })
358
363
  );
359
364
  app2.use("/api/architecture", architectureErrorHandler);
@@ -479,6 +484,72 @@ var FsLayoutStore = class {
479
484
  }
480
485
  };
481
486
 
487
+ // src/telemetry.ts
488
+ import { randomUUID } from "crypto";
489
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
490
+ import { homedir } from "os";
491
+ import { join as join2 } from "path";
492
+
493
+ // package.json
494
+ var version = "0.0.8";
495
+
496
+ // src/telemetry.ts
497
+ var POSTHOG_KEY = "phc_wTpBpKNGhboxPitLJYLSi8aGGCaYJ7fHbvLAT93edmHp";
498
+ var POSTHOG_HOST = "https://us.i.posthog.com";
499
+ var NOOP = { capture() {
500
+ } };
501
+ function disabledByEnv() {
502
+ const dnt = process.env.DO_NOT_TRACK;
503
+ if (dnt === "1" || dnt === "true") return true;
504
+ const t = process.env.TECTURE_TELEMETRY?.toLowerCase();
505
+ return t === "0" || t === "false" || t === "off" || t === "no";
506
+ }
507
+ function createTelemetry() {
508
+ if (POSTHOG_KEY.length === 0 || disabledByEnv()) return NOOP;
509
+ let distinctId;
510
+ let firstRun = false;
511
+ try {
512
+ const dir = join2(homedir(), ".config", "tecture");
513
+ const idFile = join2(dir, "telemetry-id");
514
+ if (existsSync2(idFile)) {
515
+ distinctId = readFileSync(idFile, "utf8").trim() || randomUUID();
516
+ } else {
517
+ distinctId = randomUUID();
518
+ mkdirSync(dir, { recursive: true });
519
+ writeFileSync(idFile, distinctId);
520
+ firstRun = true;
521
+ }
522
+ } catch {
523
+ distinctId = randomUUID();
524
+ }
525
+ if (firstRun) {
526
+ console.error(
527
+ "\nTecture collects anonymous usage stats (which diagram levels are viewed, how often)\nto help improve the tool. It never sends your architecture content, names, or paths.\nOpt out any time: set TECTURE_TELEMETRY=0 (or DO_NOT_TRACK=1).\n"
528
+ );
529
+ }
530
+ const superProps = {
531
+ coreVersion: version,
532
+ nodeVersion: process.version,
533
+ platform: process.platform
534
+ };
535
+ return {
536
+ capture(event, properties = {}) {
537
+ const body = JSON.stringify({
538
+ api_key: POSTHOG_KEY,
539
+ event,
540
+ distinct_id: distinctId,
541
+ properties: { ...superProps, ...properties, $process_person_profile: false }
542
+ });
543
+ void fetch(`${POSTHOG_HOST}/capture/`, {
544
+ method: "POST",
545
+ headers: { "content-type": "application/json" },
546
+ body
547
+ }).catch(() => {
548
+ });
549
+ }
550
+ };
551
+ }
552
+
482
553
  // src/cli.ts
483
554
  function parseArgs(argv) {
484
555
  let port2 = Number(process.env.PORT ?? 3e3);
@@ -519,13 +590,16 @@ function parseArgs(argv) {
519
590
  };
520
591
  }
521
592
  var { port, architecturePath, tecturePath } = parseArgs(process.argv.slice(2));
593
+ var telemetry = createTelemetry();
522
594
  var app = createApp({
523
595
  source: new FsArchitectureDataSource(architecturePath),
524
- layoutStore: new FsLayoutStore(tecturePath)
596
+ layoutStore: new FsLayoutStore(tecturePath),
597
+ report: telemetry
525
598
  });
526
599
  app.listen(port, () => {
527
600
  console.log(`Tecture IO running at http://localhost:${port}`);
528
601
  console.log(`Architecture root: ${architecturePath}`);
529
602
  console.log(`Tecture state: ${tecturePath}`);
603
+ telemetry.capture("viewer.opened");
530
604
  });
531
605
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/server.ts","../src/routes/health.ts","../src/routes/architecture.ts","../../shared/src/validators.ts","../src/source/fs.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { createApp } from \"./server.js\";\nimport { FsArchitectureDataSource, FsLayoutStore } from \"./source/fs.js\";\n\ninterface CliOptions {\n port: number;\n architecturePath: string;\n tecturePath: string;\n}\n\nfunction parseArgs(argv: string[]): CliOptions {\n let port = Number(process.env.PORT ?? 3000);\n let architecturePath = process.env.ARCHITECTURE_PATH;\n let tecturePath = process.env.TECTURE_PATH;\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === \"--port\" || arg === \"-p\") {\n const next = argv[i + 1];\n if (!next) throw new Error(`Missing value for ${arg}`);\n const parsed = Number(next);\n if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {\n throw new Error(`Invalid port: ${next}`);\n }\n port = parsed;\n i++;\n } else if (arg === \"--architecture-path\" || arg === \"-a\") {\n const next = argv[i + 1];\n if (!next) throw new Error(`Missing value for ${arg}`);\n architecturePath = next;\n i++;\n } else if (arg === \"--tecture-path\" || arg === \"-t\") {\n const next = argv[i + 1];\n if (!next) throw new Error(`Missing value for ${arg}`);\n tecturePath = next;\n i++;\n } else if (arg === \"--help\" || arg === \"-h\") {\n console.log(\n \"Usage: tecture-io [--port <number>] [--architecture-path <path>] [--tecture-path <path>]\",\n );\n process.exit(0);\n }\n }\n return {\n port,\n architecturePath: resolve(architecturePath ?? \"architecture\"),\n tecturePath: resolve(tecturePath ?? \"architecture/.tecture\"),\n };\n}\n\nconst { port, architecturePath, tecturePath } = parseArgs(process.argv.slice(2));\nconst app = createApp({\n source: new FsArchitectureDataSource(architecturePath),\n layoutStore: new FsLayoutStore(tecturePath),\n});\n\napp.listen(port, () => {\n console.log(`Tecture IO running at http://localhost:${port}`);\n console.log(`Architecture root: ${architecturePath}`);\n console.log(`Tecture state: ${tecturePath}`);\n});\n","import { existsSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport express, { type Express, type Request } from \"express\";\nimport { healthRouter } from \"./routes/health.js\";\nimport {\n architectureErrorHandler,\n createArchitectureRouter,\n type LayoutStoreResolver,\n type SourceResolver,\n} from \"./routes/architecture.js\";\nimport type {\n ArchitectureDataSource,\n LayoutStore,\n} from \"./source/types.js\";\n\nexport interface CreateAppOptions {\n source: ArchitectureDataSource | SourceResolver;\n layoutStore?: LayoutStore | LayoutStoreResolver | null;\n}\n\nfunction toSourceResolver(\n value: ArchitectureDataSource | SourceResolver,\n): SourceResolver {\n if (typeof value === \"function\") return value as SourceResolver;\n return () => value;\n}\n\nfunction toLayoutStoreResolver(\n value: LayoutStore | LayoutStoreResolver | null | undefined,\n): LayoutStoreResolver | undefined {\n if (value == null) return undefined;\n if (typeof value === \"function\") return value as LayoutStoreResolver;\n return () => value;\n}\n\nexport function createApp(options: CreateAppOptions): Express {\n const app = express();\n\n app.use(express.json({ limit: \"256kb\" }));\n app.use(\"/api/health\", healthRouter);\n app.use(\n \"/api/architecture\",\n createArchitectureRouter({\n resolveSource: toSourceResolver(options.source),\n resolveLayoutStore: toLayoutStoreResolver(options.layoutStore),\n }),\n );\n app.use(\"/api/architecture\", architectureErrorHandler);\n\n const publicDir = fileURLToPath(new URL(\"./public\", import.meta.url));\n\n if (existsSync(publicDir)) {\n app.use(express.static(publicDir));\n app.get(\"*\", (req: Request, res, next) => {\n if (req.path.startsWith(\"/api/\")) return next();\n res.sendFile(fileURLToPath(new URL(\"./public/index.html\", import.meta.url)));\n });\n }\n\n return app;\n}\n","import { Router } from \"express\";\nimport type { ApiHealthResponse } from \"@tecture/shared\";\n\nexport const healthRouter: Router = Router();\n\nhealthRouter.get(\"/\", (_req, res) => {\n const body: ApiHealthResponse = {\n status: \"ok\",\n uptime: process.uptime(),\n timestamp: new Date().toISOString(),\n };\n res.json(body);\n});\n","import { Router, type Request, type Response, type NextFunction } from \"express\";\nimport type {\n ApiArchitectureError,\n ApiDiagram,\n ApiDiagramEdges,\n ApiDiagramLayout,\n ApiDiagramLayoutUpdate,\n ApiDiagramNodes,\n ApiNodeDetail,\n ManifestFile,\n} from \"@tecture/shared\";\nimport {\n type ArchitectureDataSource,\n DescriptionNotFoundError,\n DiagramNotFoundError,\n type LayoutStore,\n LayoutInvalidError,\n NodeNotFoundError,\n SLUG_RE,\n buildArchitectureSummary,\n emptyLayout,\n findNode,\n} from \"../source/types.js\";\n\nexport type SourceResolver = (\n req: Request,\n) => ArchitectureDataSource | Promise<ArchitectureDataSource>;\n\nexport type LayoutStoreResolver = (\n req: Request,\n) => LayoutStore | null | Promise<LayoutStore | null>;\n\nexport interface ArchitectureRouterOptions {\n resolveSource: SourceResolver;\n resolveLayoutStore?: LayoutStoreResolver;\n}\n\nfunction send501(res: Response, body: ApiArchitectureError): void {\n res.status(501).json(body);\n}\n\nfunction send404(res: Response, body: ApiArchitectureError): void {\n res.status(404).json(body);\n}\n\nfunction send400(res: Response, body: ApiArchitectureError): void {\n res.status(400).json(body);\n}\n\nexport function createArchitectureRouter(\n opts: ArchitectureRouterOptions,\n): Router {\n const router = Router();\n const { resolveSource, resolveLayoutStore } = opts;\n\n router.get(\"/\", async (req, res, next) => {\n try {\n const source = await resolveSource(req);\n const summary = await buildArchitectureSummary(source);\n res.json(summary);\n } catch (err) {\n next(err);\n }\n });\n\n router.get(\"/manifest\", async (req, res, next) => {\n try {\n const source = await resolveSource(req);\n const manifest: ManifestFile = await source.loadManifest();\n res.json(manifest);\n } catch (err) {\n next(err);\n }\n });\n\n router.get(\"/diagrams\", async (req, res, next) => {\n try {\n const source = await resolveSource(req);\n const summary = await buildArchitectureSummary(source);\n res.json(summary.diagrams);\n } catch (err) {\n next(err);\n }\n });\n\n router.get(\"/diagrams/:diagramId\", async (req, res, next) => {\n const { diagramId } = req.params;\n if (!SLUG_RE.test(diagramId)) {\n return send404(res, { error: \"diagram_not_found\", slug: diagramId });\n }\n try {\n const source = await resolveSource(req);\n const diagram = await source.loadDiagram(diagramId);\n const body: ApiDiagram = {\n slug: diagramId,\n name: diagram.name,\n level: diagram.level,\n meta: diagram.meta,\n nodes: diagram.nodes,\n edges: diagram.edges ?? [],\n };\n res.json(body);\n } catch (err) {\n if (err instanceof DiagramNotFoundError) {\n return send404(res, { error: \"diagram_not_found\", slug: err.slug });\n }\n next(err);\n }\n });\n\n router.get(\"/diagrams/:diagramId/nodes\", async (req, res, next) => {\n const { diagramId } = req.params;\n if (!SLUG_RE.test(diagramId)) {\n return send404(res, { error: \"diagram_not_found\", slug: diagramId });\n }\n try {\n const source = await resolveSource(req);\n const diagram = await source.loadDiagram(diagramId);\n const body: ApiDiagramNodes = { diagramId, nodes: diagram.nodes };\n res.json(body);\n } catch (err) {\n if (err instanceof DiagramNotFoundError) {\n return send404(res, { error: \"diagram_not_found\", slug: err.slug });\n }\n next(err);\n }\n });\n\n router.get(\"/diagrams/:diagramId/edges\", async (req, res, next) => {\n const { diagramId } = req.params;\n if (!SLUG_RE.test(diagramId)) {\n return send404(res, { error: \"diagram_not_found\", slug: diagramId });\n }\n try {\n const source = await resolveSource(req);\n const diagram = await source.loadDiagram(diagramId);\n const body: ApiDiagramEdges = { diagramId, edges: diagram.edges ?? [] };\n res.json(body);\n } catch (err) {\n if (err instanceof DiagramNotFoundError) {\n return send404(res, { error: \"diagram_not_found\", slug: err.slug });\n }\n next(err);\n }\n });\n\n router.get(\"/diagrams/:diagramId/layout\", async (req, res, next) => {\n const { diagramId } = req.params;\n if (!SLUG_RE.test(diagramId)) {\n return send404(res, { error: \"diagram_not_found\", slug: diagramId });\n }\n try {\n const source = await resolveSource(req);\n await source.loadDiagram(diagramId);\n const store = resolveLayoutStore ? await resolveLayoutStore(req) : null;\n const layout: ApiDiagramLayout = store\n ? await store.loadLayout(diagramId)\n : emptyLayout(diagramId);\n res.json(layout);\n } catch (err) {\n if (err instanceof DiagramNotFoundError) {\n return send404(res, { error: \"diagram_not_found\", slug: err.slug });\n }\n next(err);\n }\n });\n\n router.put(\"/diagrams/:diagramId/layout\", async (req, res, next) => {\n const { diagramId } = req.params;\n if (!SLUG_RE.test(diagramId)) {\n return send404(res, { error: \"diagram_not_found\", slug: diagramId });\n }\n try {\n const source = await resolveSource(req);\n await source.loadDiagram(diagramId);\n const store = resolveLayoutStore ? await resolveLayoutStore(req) : null;\n if (!store) {\n return send501(res, {\n error: \"layout_not_supported\",\n message: \"Layout persistence is not configured for this data source\",\n });\n }\n const update = req.body as ApiDiagramLayoutUpdate;\n const saved = await store.saveLayout(diagramId, update);\n res.json(saved);\n } catch (err) {\n if (err instanceof DiagramNotFoundError) {\n return send404(res, { error: \"diagram_not_found\", slug: err.slug });\n }\n if (err instanceof LayoutInvalidError) {\n return send400(res, { error: \"layout_invalid\", message: err.message });\n }\n next(err);\n }\n });\n\n router.get(\"/nodes/:nodeId\", async (req, res, next) => {\n const { nodeId } = req.params;\n if (!SLUG_RE.test(nodeId)) {\n return send404(res, { error: \"node_not_found\", id: nodeId });\n }\n try {\n const source = await resolveSource(req);\n const { node, diagramId } = await findNode(source, nodeId);\n const description = await source.loadDescription(nodeId);\n const body: ApiNodeDetail = { ...node, diagramId, description };\n res.json(body);\n } catch (err) {\n if (err instanceof NodeNotFoundError) {\n return send404(res, { error: \"node_not_found\", id: err.id });\n }\n if (err instanceof DescriptionNotFoundError) {\n return send404(res, { error: \"description_not_found\", id: err.id });\n }\n next(err);\n }\n });\n\n router.get(\"/nodes/:nodeId/description\", async (req, res, next) => {\n const { nodeId } = req.params;\n if (!SLUG_RE.test(nodeId)) {\n return send404(res, { error: \"description_not_found\", id: nodeId });\n }\n try {\n const source = await resolveSource(req);\n await findNode(source, nodeId);\n const markdown = await source.loadDescription(nodeId);\n res.type(\"text/markdown; charset=utf-8\").send(markdown);\n } catch (err) {\n if (err instanceof NodeNotFoundError) {\n return send404(res, { error: \"node_not_found\", id: err.id });\n }\n if (err instanceof DescriptionNotFoundError) {\n return send404(res, { error: \"description_not_found\", id: err.id });\n }\n next(err);\n }\n });\n\n return router;\n}\n\nexport function architectureErrorHandler(\n err: unknown,\n _req: Request,\n res: Response,\n next: NextFunction,\n): void {\n if (res.headersSent) return next(err);\n const message = err instanceof Error ? err.message : String(err);\n const body: ApiArchitectureError = {\n error: \"architecture_unreadable\",\n message,\n };\n res.status(500).json(body);\n}\n","import type {\n ApiArchitectureSummary,\n ApiDiagramLayoutUpdate,\n ApiDiagramSummary,\n ArchitectureNode,\n DiagramFile,\n DiagramLayoutFile,\n ManifestFile,\n NodeLayoutEntry,\n SourceHost,\n} from \"./index\";\n\nexport const SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\nexport class DiagramNotFoundError extends Error {\n constructor(public readonly slug: string) {\n super(`Diagram not found: ${slug}`);\n this.name = \"DiagramNotFoundError\";\n }\n}\n\nexport class NodeNotFoundError extends Error {\n constructor(public readonly id: string) {\n super(`Node not found: ${id}`);\n this.name = \"NodeNotFoundError\";\n }\n}\n\nexport class DescriptionNotFoundError extends Error {\n constructor(public readonly id: string) {\n super(`Description not found: ${id}`);\n this.name = \"DescriptionNotFoundError\";\n }\n}\n\nexport class LayoutInvalidError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"LayoutInvalidError\";\n }\n}\n\nexport interface ArchitectureDataSource {\n loadManifest(): Promise<ManifestFile>;\n loadDiagram(slug: string): Promise<DiagramFile>;\n loadDescription(nodeId: string): Promise<string>;\n}\n\nexport interface LayoutStore {\n loadLayout(slug: string): Promise<DiagramLayoutFile>;\n saveLayout(\n slug: string,\n update: ApiDiagramLayoutUpdate,\n ): Promise<DiagramLayoutFile>;\n}\n\nexport function isFiniteNumber(v: unknown): v is number {\n return typeof v === \"number\" && Number.isFinite(v);\n}\n\nexport function isValidLayoutEntry(v: unknown): v is NodeLayoutEntry {\n if (!v || typeof v !== \"object\") return false;\n const e = v as Record<string, unknown>;\n if (!isFiniteNumber(e.x) || !isFiniteNumber(e.y)) return false;\n if (\"width\" in e && e.width !== undefined && !isFiniteNumber(e.width)) return false;\n if (\"height\" in e && e.height !== undefined && !isFiniteNumber(e.height)) return false;\n return true;\n}\n\nexport function emptyLayout(slug: string): DiagramLayoutFile {\n return { version: 1, diagramId: slug, updatedAt: \"\", nodes: {} };\n}\n\nexport function normalizeLayoutUpdate(\n slug: string,\n update: ApiDiagramLayoutUpdate,\n): DiagramLayoutFile {\n if (!SLUG_RE.test(slug)) {\n throw new LayoutInvalidError(`invalid diagram slug: ${slug}`);\n }\n if (\n !update ||\n typeof update !== \"object\" ||\n !update.nodes ||\n typeof update.nodes !== \"object\"\n ) {\n throw new LayoutInvalidError(\n \"body must be { nodes: Record<string, { x, y, width?, height? }> }\",\n );\n }\n const nodes: Record<string, NodeLayoutEntry> = {};\n for (const [id, entry] of Object.entries(update.nodes)) {\n if (!SLUG_RE.test(id)) {\n throw new LayoutInvalidError(`invalid node id: ${id}`);\n }\n if (!isValidLayoutEntry(entry)) {\n throw new LayoutInvalidError(`invalid layout entry for ${id}`);\n }\n const out: NodeLayoutEntry = { x: entry.x, y: entry.y };\n if (isFiniteNumber(entry.width)) out.width = entry.width;\n if (isFiniteNumber(entry.height)) out.height = entry.height;\n nodes[id] = out;\n }\n return {\n version: 1,\n diagramId: slug,\n updatedAt: new Date().toISOString(),\n nodes,\n };\n}\n\nexport async function buildArchitectureSummary(\n source: ArchitectureDataSource,\n): Promise<ApiArchitectureSummary> {\n const manifest = await source.loadManifest();\n const diagrams = await Promise.all(\n manifest.diagrams.map(async (slug): Promise<ApiDiagramSummary> => {\n const diagram = await source.loadDiagram(slug);\n return {\n slug,\n name: diagram.name,\n level: diagram.level,\n nodeCount: diagram.nodes?.length ?? 0,\n edgeCount: diagram.edges?.length ?? 0,\n };\n }),\n );\n return {\n name: manifest.name,\n description: manifest.description,\n source: manifest.source,\n sourceHost: manifest.sourceHost,\n topDiagram: manifest.topDiagram,\n diagrams,\n };\n}\n\nconst HOST_DOMAINS: Array<{ host: SourceHost; match: RegExp }> = [\n { host: \"github\", match: /(^|\\.)github\\.com$/i },\n { host: \"gitlab\", match: /(^|\\.)gitlab\\.com$/i },\n { host: \"bitbucket\", match: /(^|\\.)bitbucket\\.org$/i },\n];\n\nfunction inferSourceHost(source: string): SourceHost | undefined {\n // Extract the hostname without relying on the URL global (kept lib-agnostic for shared):\n // scheme://[userinfo@]host[:port]/...\n const hostname = /^[a-z][a-z0-9+.-]*:\\/\\/(?:[^@/]*@)?([^:/?#]+)/i.exec(source)?.[1];\n if (!hostname) return undefined;\n return HOST_DOMAINS.find(({ match }) => match.test(hostname))?.host;\n}\n\n/**\n * Build a web URL that views `path` (a repo-root-relative file or directory, trailing \"/\" = dir)\n * on the source repo host. `HEAD` resolves to the default branch so links stay current. Falls back\n * to the repo root when the host is unknown, and returns undefined when there is no source.\n */\nexport function buildSourceUrl(\n source: string | undefined,\n host: SourceHost | undefined,\n path: string,\n): string | undefined {\n if (!source) return undefined;\n const base = source.replace(/\\/+$/, \"\").replace(/\\.git$/i, \"\");\n const resolvedHost = host ?? inferSourceHost(base);\n const isDir = path.endsWith(\"/\");\n const encoded = path\n .replace(/^\\/+/, \"\")\n .replace(/\\/+$/, \"\")\n .split(\"/\")\n .map(encodeURIComponent)\n .join(\"/\");\n if (!encoded) return base;\n switch (resolvedHost) {\n case \"github\":\n return `${base}/${isDir ? \"tree\" : \"blob\"}/HEAD/${encoded}`;\n case \"gitlab\":\n return `${base}/-/${isDir ? \"tree\" : \"blob\"}/HEAD/${encoded}`;\n case \"bitbucket\":\n return `${base}/src/HEAD/${encoded}`;\n default:\n return base;\n }\n}\n\nexport async function findNode(\n source: ArchitectureDataSource,\n nodeId: string,\n): Promise<{ node: ArchitectureNode; diagramId: string }> {\n if (!SLUG_RE.test(nodeId)) throw new NodeNotFoundError(nodeId);\n const manifest = await source.loadManifest();\n for (const slug of manifest.diagrams) {\n let diagram: DiagramFile;\n try {\n diagram = await source.loadDiagram(slug);\n } catch (err) {\n if (err instanceof DiagramNotFoundError) continue;\n throw err;\n }\n const match = diagram.nodes?.find((n) => n.id === nodeId);\n if (match) return { node: match, diagramId: slug };\n }\n throw new NodeNotFoundError(nodeId);\n}\n","import { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { dirname, join, resolve, sep } from \"node:path\";\nimport type {\n ApiDiagramLayoutUpdate,\n DiagramFile,\n DiagramLayoutFile,\n ManifestFile,\n NodeLayoutEntry,\n} from \"@tecture/shared\";\nimport {\n type ArchitectureDataSource,\n DescriptionNotFoundError,\n DiagramNotFoundError,\n type LayoutStore,\n SLUG_RE,\n emptyLayout,\n isFiniteNumber,\n isValidLayoutEntry,\n normalizeLayoutUpdate,\n} from \"./types.js\";\n\nexport function safeJoin(root: string, ...segments: string[]): string {\n const absRoot = resolve(root);\n const candidate = resolve(join(absRoot, ...segments));\n const guard = absRoot.endsWith(sep) ? absRoot : absRoot + sep;\n if (candidate !== absRoot && !candidate.startsWith(guard)) {\n throw new Error(`Path escapes architecture root: ${candidate}`);\n }\n return candidate;\n}\n\nfunction isNodeErrnoError(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && \"code\" in err;\n}\n\nasync function loadJsonFile<T>(path: string): Promise<T> {\n const raw = await readFile(path, \"utf8\");\n return JSON.parse(raw) as T;\n}\n\nexport class FsArchitectureDataSource implements ArchitectureDataSource {\n constructor(private readonly root: string) {}\n\n async loadManifest(): Promise<ManifestFile> {\n const path = safeJoin(this.root, \"manifest.json\");\n return loadJsonFile<ManifestFile>(path);\n }\n\n async loadDiagram(slug: string): Promise<DiagramFile> {\n if (!SLUG_RE.test(slug)) throw new DiagramNotFoundError(slug);\n const path = safeJoin(this.root, \"diagrams\", `${slug}.json`);\n try {\n return await loadJsonFile<DiagramFile>(path);\n } catch (err) {\n if (isNodeErrnoError(err) && err.code === \"ENOENT\") {\n throw new DiagramNotFoundError(slug);\n }\n throw err;\n }\n }\n\n async loadDescription(nodeId: string): Promise<string> {\n if (!SLUG_RE.test(nodeId)) throw new DescriptionNotFoundError(nodeId);\n const path = safeJoin(this.root, \"descriptions\", `${nodeId}.md`);\n try {\n return await readFile(path, \"utf8\");\n } catch (err) {\n if (isNodeErrnoError(err) && err.code === \"ENOENT\") {\n throw new DescriptionNotFoundError(nodeId);\n }\n throw err;\n }\n }\n}\n\nexport class FsLayoutStore implements LayoutStore {\n constructor(private readonly tectureRoot: string) {}\n\n private path(slug: string): string {\n return safeJoin(this.tectureRoot, \"layouts\", `${slug}.json`);\n }\n\n async loadLayout(slug: string): Promise<DiagramLayoutFile> {\n if (!SLUG_RE.test(slug)) return emptyLayout(slug);\n const path = this.path(slug);\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (err) {\n if (isNodeErrnoError(err) && err.code === \"ENOENT\") return emptyLayout(slug);\n console.warn(`[tecture] failed to read layout ${path}: ${String(err)}`);\n return emptyLayout(slug);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n console.warn(`[tecture] malformed layout JSON at ${path}: ${String(err)}`);\n return emptyLayout(slug);\n }\n\n if (!parsed || typeof parsed !== \"object\") return emptyLayout(slug);\n const p = parsed as Record<string, unknown>;\n const nodes: Record<string, NodeLayoutEntry> = {};\n if (p.nodes && typeof p.nodes === \"object\") {\n for (const [id, entry] of Object.entries(p.nodes as Record<string, unknown>)) {\n if (!SLUG_RE.test(id)) continue;\n if (!isValidLayoutEntry(entry)) continue;\n const out: NodeLayoutEntry = { x: entry.x, y: entry.y };\n if (isFiniteNumber(entry.width)) out.width = entry.width;\n if (isFiniteNumber(entry.height)) out.height = entry.height;\n nodes[id] = out;\n }\n }\n return {\n version: 1,\n diagramId: slug,\n updatedAt: typeof p.updatedAt === \"string\" ? p.updatedAt : \"\",\n nodes,\n };\n }\n\n async saveLayout(\n slug: string,\n update: ApiDiagramLayoutUpdate,\n ): Promise<DiagramLayoutFile> {\n const file = normalizeLayoutUpdate(slug, update);\n const path = this.path(slug);\n await mkdir(dirname(path), { recursive: true });\n const tmp = `${path}.tmp`;\n await writeFile(tmp, `${JSON.stringify(file, null, 2)}\\n`, \"utf8\");\n await rename(tmp, path);\n return file;\n }\n}\n"],"mappings":"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","names":["resolve","Router","Router","app","port","architecturePath","tecturePath","resolve"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/server.ts","../src/routes/health.ts","../src/routes/architecture.ts","../../shared/src/validators.ts","../src/source/fs.ts","../src/telemetry.ts","../package.json"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { createApp } from \"./server.js\";\nimport { FsArchitectureDataSource, FsLayoutStore } from \"./source/fs.js\";\nimport { createTelemetry } from \"./telemetry.js\";\n\ninterface CliOptions {\n port: number;\n architecturePath: string;\n tecturePath: string;\n}\n\nfunction parseArgs(argv: string[]): CliOptions {\n let port = Number(process.env.PORT ?? 3000);\n let architecturePath = process.env.ARCHITECTURE_PATH;\n let tecturePath = process.env.TECTURE_PATH;\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === \"--port\" || arg === \"-p\") {\n const next = argv[i + 1];\n if (!next) throw new Error(`Missing value for ${arg}`);\n const parsed = Number(next);\n if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {\n throw new Error(`Invalid port: ${next}`);\n }\n port = parsed;\n i++;\n } else if (arg === \"--architecture-path\" || arg === \"-a\") {\n const next = argv[i + 1];\n if (!next) throw new Error(`Missing value for ${arg}`);\n architecturePath = next;\n i++;\n } else if (arg === \"--tecture-path\" || arg === \"-t\") {\n const next = argv[i + 1];\n if (!next) throw new Error(`Missing value for ${arg}`);\n tecturePath = next;\n i++;\n } else if (arg === \"--help\" || arg === \"-h\") {\n console.log(\n \"Usage: tecture-io [--port <number>] [--architecture-path <path>] [--tecture-path <path>]\",\n );\n process.exit(0);\n }\n }\n return {\n port,\n architecturePath: resolve(architecturePath ?? \"architecture\"),\n tecturePath: resolve(tecturePath ?? \"architecture/.tecture\"),\n };\n}\n\nconst { port, architecturePath, tecturePath } = parseArgs(process.argv.slice(2));\nconst telemetry = createTelemetry();\nconst app = createApp({\n source: new FsArchitectureDataSource(architecturePath),\n layoutStore: new FsLayoutStore(tecturePath),\n report: telemetry,\n});\n\napp.listen(port, () => {\n console.log(`Tecture IO running at http://localhost:${port}`);\n console.log(`Architecture root: ${architecturePath}`);\n console.log(`Tecture state: ${tecturePath}`);\n telemetry.capture(\"viewer.opened\");\n});\n","import { existsSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport express, { type Express, type Request } from \"express\";\nimport { healthRouter } from \"./routes/health.js\";\nimport {\n architectureErrorHandler,\n createArchitectureRouter,\n type LayoutStoreResolver,\n type SourceResolver,\n} from \"./routes/architecture.js\";\nimport type {\n ArchitectureDataSource,\n LayoutStore,\n} from \"./source/types.js\";\nimport type { Reporter } from \"./telemetry.js\";\n\nexport interface CreateAppOptions {\n source: ArchitectureDataSource | SourceResolver;\n layoutStore?: LayoutStore | LayoutStoreResolver | null;\n /** Optional anonymous usage reporter (standalone viewer only). */\n report?: Reporter;\n}\n\nfunction toSourceResolver(\n value: ArchitectureDataSource | SourceResolver,\n): SourceResolver {\n if (typeof value === \"function\") return value as SourceResolver;\n return () => value;\n}\n\nfunction toLayoutStoreResolver(\n value: LayoutStore | LayoutStoreResolver | null | undefined,\n): LayoutStoreResolver | undefined {\n if (value == null) return undefined;\n if (typeof value === \"function\") return value as LayoutStoreResolver;\n return () => value;\n}\n\nexport function createApp(options: CreateAppOptions): Express {\n const app = express();\n\n app.use(express.json({ limit: \"256kb\" }));\n app.use(\"/api/health\", healthRouter);\n app.use(\n \"/api/architecture\",\n createArchitectureRouter({\n resolveSource: toSourceResolver(options.source),\n resolveLayoutStore: toLayoutStoreResolver(options.layoutStore),\n report: options.report,\n }),\n );\n app.use(\"/api/architecture\", architectureErrorHandler);\n\n const publicDir = fileURLToPath(new URL(\"./public\", import.meta.url));\n\n if (existsSync(publicDir)) {\n app.use(express.static(publicDir));\n app.get(\"*\", (req: Request, res, next) => {\n if (req.path.startsWith(\"/api/\")) return next();\n res.sendFile(fileURLToPath(new URL(\"./public/index.html\", import.meta.url)));\n });\n }\n\n return app;\n}\n","import { Router } from \"express\";\nimport type { ApiHealthResponse } from \"@tecture/shared\";\n\nexport const healthRouter: Router = Router();\n\nhealthRouter.get(\"/\", (_req, res) => {\n const body: ApiHealthResponse = {\n status: \"ok\",\n uptime: process.uptime(),\n timestamp: new Date().toISOString(),\n };\n res.json(body);\n});\n","import { Router, type Request, type Response, type NextFunction } from \"express\";\nimport type {\n ApiArchitectureError,\n ApiDiagram,\n ApiDiagramEdges,\n ApiDiagramLayout,\n ApiDiagramLayoutUpdate,\n ApiDiagramNodes,\n ApiNodeDetail,\n ManifestFile,\n} from \"@tecture/shared\";\nimport {\n type ArchitectureDataSource,\n DescriptionNotFoundError,\n DiagramNotFoundError,\n type LayoutStore,\n LayoutInvalidError,\n NodeNotFoundError,\n SLUG_RE,\n buildArchitectureSummary,\n emptyLayout,\n findNode,\n} from \"../source/types.js\";\nimport type { Reporter } from \"../telemetry.js\";\n\nexport type SourceResolver = (\n req: Request,\n) => ArchitectureDataSource | Promise<ArchitectureDataSource>;\n\nexport type LayoutStoreResolver = (\n req: Request,\n) => LayoutStore | null | Promise<LayoutStore | null>;\n\nexport interface ArchitectureRouterOptions {\n resolveSource: SourceResolver;\n resolveLayoutStore?: LayoutStoreResolver;\n /** Optional anonymous usage reporter (standalone viewer only). */\n report?: Reporter;\n}\n\nfunction send501(res: Response, body: ApiArchitectureError): void {\n res.status(501).json(body);\n}\n\nfunction send404(res: Response, body: ApiArchitectureError): void {\n res.status(404).json(body);\n}\n\nfunction send400(res: Response, body: ApiArchitectureError): void {\n res.status(400).json(body);\n}\n\nexport function createArchitectureRouter(\n opts: ArchitectureRouterOptions,\n): Router {\n const router = Router();\n const { resolveSource, resolveLayoutStore, report } = opts;\n\n router.get(\"/\", async (req, res, next) => {\n try {\n const source = await resolveSource(req);\n const summary = await buildArchitectureSummary(source);\n res.json(summary);\n } catch (err) {\n next(err);\n }\n });\n\n router.get(\"/manifest\", async (req, res, next) => {\n try {\n const source = await resolveSource(req);\n const manifest: ManifestFile = await source.loadManifest();\n res.json(manifest);\n } catch (err) {\n next(err);\n }\n });\n\n router.get(\"/diagrams\", async (req, res, next) => {\n try {\n const source = await resolveSource(req);\n const summary = await buildArchitectureSummary(source);\n res.json(summary.diagrams);\n } catch (err) {\n next(err);\n }\n });\n\n router.get(\"/diagrams/:diagramId\", async (req, res, next) => {\n const { diagramId } = req.params;\n if (!SLUG_RE.test(diagramId)) {\n return send404(res, { error: \"diagram_not_found\", slug: diagramId });\n }\n try {\n const source = await resolveSource(req);\n const diagram = await source.loadDiagram(diagramId);\n const body: ApiDiagram = {\n slug: diagramId,\n name: diagram.name,\n level: diagram.level,\n meta: diagram.meta,\n nodes: diagram.nodes,\n edges: diagram.edges ?? [],\n };\n res.json(body);\n // Anonymous usage signal: C4 level only, never the slug/name/content.\n report?.capture(\n \"diagram.viewed\",\n typeof diagram.level === \"number\" ? { level: diagram.level } : {},\n );\n } catch (err) {\n if (err instanceof DiagramNotFoundError) {\n return send404(res, { error: \"diagram_not_found\", slug: err.slug });\n }\n next(err);\n }\n });\n\n router.get(\"/diagrams/:diagramId/nodes\", async (req, res, next) => {\n const { diagramId } = req.params;\n if (!SLUG_RE.test(diagramId)) {\n return send404(res, { error: \"diagram_not_found\", slug: diagramId });\n }\n try {\n const source = await resolveSource(req);\n const diagram = await source.loadDiagram(diagramId);\n const body: ApiDiagramNodes = { diagramId, nodes: diagram.nodes };\n res.json(body);\n } catch (err) {\n if (err instanceof DiagramNotFoundError) {\n return send404(res, { error: \"diagram_not_found\", slug: err.slug });\n }\n next(err);\n }\n });\n\n router.get(\"/diagrams/:diagramId/edges\", async (req, res, next) => {\n const { diagramId } = req.params;\n if (!SLUG_RE.test(diagramId)) {\n return send404(res, { error: \"diagram_not_found\", slug: diagramId });\n }\n try {\n const source = await resolveSource(req);\n const diagram = await source.loadDiagram(diagramId);\n const body: ApiDiagramEdges = { diagramId, edges: diagram.edges ?? [] };\n res.json(body);\n } catch (err) {\n if (err instanceof DiagramNotFoundError) {\n return send404(res, { error: \"diagram_not_found\", slug: err.slug });\n }\n next(err);\n }\n });\n\n router.get(\"/diagrams/:diagramId/layout\", async (req, res, next) => {\n const { diagramId } = req.params;\n if (!SLUG_RE.test(diagramId)) {\n return send404(res, { error: \"diagram_not_found\", slug: diagramId });\n }\n try {\n const source = await resolveSource(req);\n await source.loadDiagram(diagramId);\n const store = resolveLayoutStore ? await resolveLayoutStore(req) : null;\n const layout: ApiDiagramLayout = store\n ? await store.loadLayout(diagramId)\n : emptyLayout(diagramId);\n res.json(layout);\n } catch (err) {\n if (err instanceof DiagramNotFoundError) {\n return send404(res, { error: \"diagram_not_found\", slug: err.slug });\n }\n next(err);\n }\n });\n\n router.put(\"/diagrams/:diagramId/layout\", async (req, res, next) => {\n const { diagramId } = req.params;\n if (!SLUG_RE.test(diagramId)) {\n return send404(res, { error: \"diagram_not_found\", slug: diagramId });\n }\n try {\n const source = await resolveSource(req);\n await source.loadDiagram(diagramId);\n const store = resolveLayoutStore ? await resolveLayoutStore(req) : null;\n if (!store) {\n return send501(res, {\n error: \"layout_not_supported\",\n message: \"Layout persistence is not configured for this data source\",\n });\n }\n const update = req.body as ApiDiagramLayoutUpdate;\n const saved = await store.saveLayout(diagramId, update);\n res.json(saved);\n } catch (err) {\n if (err instanceof DiagramNotFoundError) {\n return send404(res, { error: \"diagram_not_found\", slug: err.slug });\n }\n if (err instanceof LayoutInvalidError) {\n return send400(res, { error: \"layout_invalid\", message: err.message });\n }\n next(err);\n }\n });\n\n router.get(\"/nodes/:nodeId\", async (req, res, next) => {\n const { nodeId } = req.params;\n if (!SLUG_RE.test(nodeId)) {\n return send404(res, { error: \"node_not_found\", id: nodeId });\n }\n try {\n const source = await resolveSource(req);\n const { node, diagramId } = await findNode(source, nodeId);\n const description = await source.loadDescription(nodeId);\n const body: ApiNodeDetail = { ...node, diagramId, description };\n res.json(body);\n } catch (err) {\n if (err instanceof NodeNotFoundError) {\n return send404(res, { error: \"node_not_found\", id: err.id });\n }\n if (err instanceof DescriptionNotFoundError) {\n return send404(res, { error: \"description_not_found\", id: err.id });\n }\n next(err);\n }\n });\n\n router.get(\"/nodes/:nodeId/description\", async (req, res, next) => {\n const { nodeId } = req.params;\n if (!SLUG_RE.test(nodeId)) {\n return send404(res, { error: \"description_not_found\", id: nodeId });\n }\n try {\n const source = await resolveSource(req);\n await findNode(source, nodeId);\n const markdown = await source.loadDescription(nodeId);\n res.type(\"text/markdown; charset=utf-8\").send(markdown);\n } catch (err) {\n if (err instanceof NodeNotFoundError) {\n return send404(res, { error: \"node_not_found\", id: err.id });\n }\n if (err instanceof DescriptionNotFoundError) {\n return send404(res, { error: \"description_not_found\", id: err.id });\n }\n next(err);\n }\n });\n\n return router;\n}\n\nexport function architectureErrorHandler(\n err: unknown,\n _req: Request,\n res: Response,\n next: NextFunction,\n): void {\n if (res.headersSent) return next(err);\n const message = err instanceof Error ? err.message : String(err);\n const body: ApiArchitectureError = {\n error: \"architecture_unreadable\",\n message,\n };\n res.status(500).json(body);\n}\n","import type {\n ApiArchitectureSummary,\n ApiDiagramLayoutUpdate,\n ApiDiagramSummary,\n ArchitectureNode,\n DiagramFile,\n DiagramLayoutFile,\n ManifestFile,\n NodeLayoutEntry,\n SourceHost,\n} from \"./index\";\n\nexport const SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\nexport class DiagramNotFoundError extends Error {\n constructor(public readonly slug: string) {\n super(`Diagram not found: ${slug}`);\n this.name = \"DiagramNotFoundError\";\n }\n}\n\nexport class NodeNotFoundError extends Error {\n constructor(public readonly id: string) {\n super(`Node not found: ${id}`);\n this.name = \"NodeNotFoundError\";\n }\n}\n\nexport class DescriptionNotFoundError extends Error {\n constructor(public readonly id: string) {\n super(`Description not found: ${id}`);\n this.name = \"DescriptionNotFoundError\";\n }\n}\n\nexport class LayoutInvalidError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"LayoutInvalidError\";\n }\n}\n\nexport interface ArchitectureDataSource {\n loadManifest(): Promise<ManifestFile>;\n loadDiagram(slug: string): Promise<DiagramFile>;\n loadDescription(nodeId: string): Promise<string>;\n}\n\nexport interface LayoutStore {\n loadLayout(slug: string): Promise<DiagramLayoutFile>;\n saveLayout(\n slug: string,\n update: ApiDiagramLayoutUpdate,\n ): Promise<DiagramLayoutFile>;\n}\n\nexport function isFiniteNumber(v: unknown): v is number {\n return typeof v === \"number\" && Number.isFinite(v);\n}\n\nexport function isValidLayoutEntry(v: unknown): v is NodeLayoutEntry {\n if (!v || typeof v !== \"object\") return false;\n const e = v as Record<string, unknown>;\n if (!isFiniteNumber(e.x) || !isFiniteNumber(e.y)) return false;\n if (\"width\" in e && e.width !== undefined && !isFiniteNumber(e.width)) return false;\n if (\"height\" in e && e.height !== undefined && !isFiniteNumber(e.height)) return false;\n return true;\n}\n\nexport function emptyLayout(slug: string): DiagramLayoutFile {\n return { version: 1, diagramId: slug, updatedAt: \"\", nodes: {} };\n}\n\nexport function normalizeLayoutUpdate(\n slug: string,\n update: ApiDiagramLayoutUpdate,\n): DiagramLayoutFile {\n if (!SLUG_RE.test(slug)) {\n throw new LayoutInvalidError(`invalid diagram slug: ${slug}`);\n }\n if (\n !update ||\n typeof update !== \"object\" ||\n !update.nodes ||\n typeof update.nodes !== \"object\"\n ) {\n throw new LayoutInvalidError(\n \"body must be { nodes: Record<string, { x, y, width?, height? }> }\",\n );\n }\n const nodes: Record<string, NodeLayoutEntry> = {};\n for (const [id, entry] of Object.entries(update.nodes)) {\n if (!SLUG_RE.test(id)) {\n throw new LayoutInvalidError(`invalid node id: ${id}`);\n }\n if (!isValidLayoutEntry(entry)) {\n throw new LayoutInvalidError(`invalid layout entry for ${id}`);\n }\n const out: NodeLayoutEntry = { x: entry.x, y: entry.y };\n if (isFiniteNumber(entry.width)) out.width = entry.width;\n if (isFiniteNumber(entry.height)) out.height = entry.height;\n nodes[id] = out;\n }\n return {\n version: 1,\n diagramId: slug,\n updatedAt: new Date().toISOString(),\n nodes,\n };\n}\n\nexport async function buildArchitectureSummary(\n source: ArchitectureDataSource,\n): Promise<ApiArchitectureSummary> {\n const manifest = await source.loadManifest();\n const diagrams = await Promise.all(\n manifest.diagrams.map(async (slug): Promise<ApiDiagramSummary> => {\n const diagram = await source.loadDiagram(slug);\n return {\n slug,\n name: diagram.name,\n level: diagram.level,\n nodeCount: diagram.nodes?.length ?? 0,\n edgeCount: diagram.edges?.length ?? 0,\n };\n }),\n );\n return {\n name: manifest.name,\n description: manifest.description,\n source: manifest.source,\n sourceHost: manifest.sourceHost,\n topDiagram: manifest.topDiagram,\n diagrams,\n };\n}\n\nconst HOST_DOMAINS: Array<{ host: SourceHost; match: RegExp }> = [\n { host: \"github\", match: /(^|\\.)github\\.com$/i },\n { host: \"gitlab\", match: /(^|\\.)gitlab\\.com$/i },\n { host: \"bitbucket\", match: /(^|\\.)bitbucket\\.org$/i },\n];\n\nfunction inferSourceHost(source: string): SourceHost | undefined {\n // Extract the hostname without relying on the URL global (kept lib-agnostic for shared):\n // scheme://[userinfo@]host[:port]/...\n const hostname = /^[a-z][a-z0-9+.-]*:\\/\\/(?:[^@/]*@)?([^:/?#]+)/i.exec(source)?.[1];\n if (!hostname) return undefined;\n return HOST_DOMAINS.find(({ match }) => match.test(hostname))?.host;\n}\n\n/**\n * Build a web URL that views `path` (a repo-root-relative file or directory, trailing \"/\" = dir)\n * on the source repo host. `HEAD` resolves to the default branch so links stay current. Falls back\n * to the repo root when the host is unknown, and returns undefined when there is no source.\n */\nexport function buildSourceUrl(\n source: string | undefined,\n host: SourceHost | undefined,\n path: string,\n): string | undefined {\n if (!source) return undefined;\n const base = source.replace(/\\/+$/, \"\").replace(/\\.git$/i, \"\");\n const resolvedHost = host ?? inferSourceHost(base);\n const isDir = path.endsWith(\"/\");\n const encoded = path\n .replace(/^\\/+/, \"\")\n .replace(/\\/+$/, \"\")\n .split(\"/\")\n .map(encodeURIComponent)\n .join(\"/\");\n if (!encoded) return base;\n switch (resolvedHost) {\n case \"github\":\n return `${base}/${isDir ? \"tree\" : \"blob\"}/HEAD/${encoded}`;\n case \"gitlab\":\n return `${base}/-/${isDir ? \"tree\" : \"blob\"}/HEAD/${encoded}`;\n case \"bitbucket\":\n return `${base}/src/HEAD/${encoded}`;\n default:\n return base;\n }\n}\n\nexport async function findNode(\n source: ArchitectureDataSource,\n nodeId: string,\n): Promise<{ node: ArchitectureNode; diagramId: string }> {\n if (!SLUG_RE.test(nodeId)) throw new NodeNotFoundError(nodeId);\n const manifest = await source.loadManifest();\n for (const slug of manifest.diagrams) {\n let diagram: DiagramFile;\n try {\n diagram = await source.loadDiagram(slug);\n } catch (err) {\n if (err instanceof DiagramNotFoundError) continue;\n throw err;\n }\n const match = diagram.nodes?.find((n) => n.id === nodeId);\n if (match) return { node: match, diagramId: slug };\n }\n throw new NodeNotFoundError(nodeId);\n}\n","import { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { dirname, join, resolve, sep } from \"node:path\";\nimport type {\n ApiDiagramLayoutUpdate,\n DiagramFile,\n DiagramLayoutFile,\n ManifestFile,\n NodeLayoutEntry,\n} from \"@tecture/shared\";\nimport {\n type ArchitectureDataSource,\n DescriptionNotFoundError,\n DiagramNotFoundError,\n type LayoutStore,\n SLUG_RE,\n emptyLayout,\n isFiniteNumber,\n isValidLayoutEntry,\n normalizeLayoutUpdate,\n} from \"./types.js\";\n\nexport function safeJoin(root: string, ...segments: string[]): string {\n const absRoot = resolve(root);\n const candidate = resolve(join(absRoot, ...segments));\n const guard = absRoot.endsWith(sep) ? absRoot : absRoot + sep;\n if (candidate !== absRoot && !candidate.startsWith(guard)) {\n throw new Error(`Path escapes architecture root: ${candidate}`);\n }\n return candidate;\n}\n\nfunction isNodeErrnoError(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && \"code\" in err;\n}\n\nasync function loadJsonFile<T>(path: string): Promise<T> {\n const raw = await readFile(path, \"utf8\");\n return JSON.parse(raw) as T;\n}\n\nexport class FsArchitectureDataSource implements ArchitectureDataSource {\n constructor(private readonly root: string) {}\n\n async loadManifest(): Promise<ManifestFile> {\n const path = safeJoin(this.root, \"manifest.json\");\n return loadJsonFile<ManifestFile>(path);\n }\n\n async loadDiagram(slug: string): Promise<DiagramFile> {\n if (!SLUG_RE.test(slug)) throw new DiagramNotFoundError(slug);\n const path = safeJoin(this.root, \"diagrams\", `${slug}.json`);\n try {\n return await loadJsonFile<DiagramFile>(path);\n } catch (err) {\n if (isNodeErrnoError(err) && err.code === \"ENOENT\") {\n throw new DiagramNotFoundError(slug);\n }\n throw err;\n }\n }\n\n async loadDescription(nodeId: string): Promise<string> {\n if (!SLUG_RE.test(nodeId)) throw new DescriptionNotFoundError(nodeId);\n const path = safeJoin(this.root, \"descriptions\", `${nodeId}.md`);\n try {\n return await readFile(path, \"utf8\");\n } catch (err) {\n if (isNodeErrnoError(err) && err.code === \"ENOENT\") {\n throw new DescriptionNotFoundError(nodeId);\n }\n throw err;\n }\n }\n}\n\nexport class FsLayoutStore implements LayoutStore {\n constructor(private readonly tectureRoot: string) {}\n\n private path(slug: string): string {\n return safeJoin(this.tectureRoot, \"layouts\", `${slug}.json`);\n }\n\n async loadLayout(slug: string): Promise<DiagramLayoutFile> {\n if (!SLUG_RE.test(slug)) return emptyLayout(slug);\n const path = this.path(slug);\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (err) {\n if (isNodeErrnoError(err) && err.code === \"ENOENT\") return emptyLayout(slug);\n console.warn(`[tecture] failed to read layout ${path}: ${String(err)}`);\n return emptyLayout(slug);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n console.warn(`[tecture] malformed layout JSON at ${path}: ${String(err)}`);\n return emptyLayout(slug);\n }\n\n if (!parsed || typeof parsed !== \"object\") return emptyLayout(slug);\n const p = parsed as Record<string, unknown>;\n const nodes: Record<string, NodeLayoutEntry> = {};\n if (p.nodes && typeof p.nodes === \"object\") {\n for (const [id, entry] of Object.entries(p.nodes as Record<string, unknown>)) {\n if (!SLUG_RE.test(id)) continue;\n if (!isValidLayoutEntry(entry)) continue;\n const out: NodeLayoutEntry = { x: entry.x, y: entry.y };\n if (isFiniteNumber(entry.width)) out.width = entry.width;\n if (isFiniteNumber(entry.height)) out.height = entry.height;\n nodes[id] = out;\n }\n }\n return {\n version: 1,\n diagramId: slug,\n updatedAt: typeof p.updatedAt === \"string\" ? p.updatedAt : \"\",\n nodes,\n };\n }\n\n async saveLayout(\n slug: string,\n update: ApiDiagramLayoutUpdate,\n ): Promise<DiagramLayoutFile> {\n const file = normalizeLayoutUpdate(slug, update);\n const path = this.path(slug);\n await mkdir(dirname(path), { recursive: true });\n const tmp = `${path}.tmp`;\n await writeFile(tmp, `${JSON.stringify(file, null, 2)}\\n`, \"utf8\");\n await rename(tmp, path);\n return file;\n }\n}\n","import { randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { version as coreVersion } from \"../package.json\";\n\n// PostHog *project* API key — a public client key, safe to embed. Same project\n// as the VS Code extension + website, so standalone-viewer usage joins the same\n// funnel. Empty string disables telemetry entirely.\nconst POSTHOG_KEY = \"phc_wTpBpKNGhboxPitLJYLSi8aGGCaYJ7fHbvLAT93edmHp\";\nconst POSTHOG_HOST = \"https://us.i.posthog.com\";\n\ntype EventProps = Record<string, string | number | boolean>;\n\nexport interface Reporter {\n /** Send an anonymous usage event. No-op when telemetry is disabled. */\n capture(event: string, properties?: EventProps): void;\n}\n\nconst NOOP: Reporter = { capture() {} };\n\nfunction disabledByEnv(): boolean {\n const dnt = process.env.DO_NOT_TRACK;\n if (dnt === \"1\" || dnt === \"true\") return true;\n const t = process.env.TECTURE_TELEMETRY?.toLowerCase();\n return t === \"0\" || t === \"false\" || t === \"off\" || t === \"no\";\n}\n\n/**\n * Anonymous, opt-out usage telemetry for the standalone viewer (only ever runs\n * for `npx @tecture/core` — the VS Code extension talks to its own host, not\n * this server, so there is no double-counting).\n *\n * - Opt-out: honors `DO_NOT_TRACK=1` and `TECTURE_TELEMETRY=0`, and prints a\n * one-time notice on first run.\n * - Anonymous: a random id persisted under `~/.config/tecture/telemetry-id`\n * (so repeat runs count as one user). No machine name, user, or repo data.\n * - NEVER sends architecture content: only the C4 level (1/2/3) and counts.\n */\nexport function createTelemetry(): Reporter {\n if (POSTHOG_KEY.length === 0 || disabledByEnv()) return NOOP;\n\n let distinctId: string;\n let firstRun = false;\n try {\n const dir = join(homedir(), \".config\", \"tecture\");\n const idFile = join(dir, \"telemetry-id\");\n if (existsSync(idFile)) {\n distinctId = readFileSync(idFile, \"utf8\").trim() || randomUUID();\n } else {\n distinctId = randomUUID();\n mkdirSync(dir, { recursive: true });\n writeFileSync(idFile, distinctId);\n firstRun = true;\n }\n } catch {\n distinctId = randomUUID(); // ephemeral fallback if the config dir isn't writable\n }\n\n if (firstRun) {\n console.error(\n \"\\nTecture collects anonymous usage stats (which diagram levels are viewed, how often)\\n\" +\n \"to help improve the tool. It never sends your architecture content, names, or paths.\\n\" +\n \"Opt out any time: set TECTURE_TELEMETRY=0 (or DO_NOT_TRACK=1).\\n\",\n );\n }\n\n const superProps: EventProps = {\n coreVersion,\n nodeVersion: process.version,\n platform: process.platform,\n };\n\n return {\n capture(event, properties = {}) {\n const body = JSON.stringify({\n api_key: POSTHOG_KEY,\n event,\n distinct_id: distinctId,\n properties: { ...superProps, ...properties, $process_person_profile: false },\n });\n // Fire-and-forget: never block the server or throw on a dead network.\n void fetch(`${POSTHOG_HOST}/capture/`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body,\n }).catch(() => {});\n },\n };\n}\n","{\n \"name\": \"@tecture/core\",\n \"version\": \"0.0.8\",\n \"description\": \"Next generation architecture documentation generated by coding agents — render file-based architecture diagrams (JSON + Markdown) in your browser.\",\n \"keywords\": [\n \"c4\",\n \"architecture\",\n \"diagram\",\n \"documentation\",\n \"visualization\",\n \"reactflow\",\n \"agents\",\n \"llm\",\n \"coding-agents\"\n ],\n \"license\": \"MIT\",\n \"author\": \"Shanika Wijerathna <shanikacj@gmail.com>\",\n \"homepage\": \"https://github.com/tecture-io/tecture#readme\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/tecture-io/tecture.git\",\n \"directory\": \"packages/server\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/tecture-io/tecture/issues\"\n },\n \"type\": \"module\",\n \"main\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n },\n \"./cli\": \"./dist/cli.js\"\n },\n \"bin\": \"./dist/cli.js\",\n \"files\": [\n \"dist\",\n \"CHANGELOG.md\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"dev\": \"tsx watch src/cli.ts --port 3001 --architecture-path ../../architecture --tecture-path ../../architecture/.tecture\",\n \"build\": \"tsup && node scripts/copy-web-assets.mjs\",\n \"start\": \"node dist/cli.js\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"dependencies\": {\n \"express\": \"^4.21.1\"\n },\n \"devDependencies\": {\n \"@tecture/shared\": \"workspace:*\",\n \"@tecture/web\": \"workspace:*\",\n \"@types/express\": \"^4.17.21\",\n \"@types/node\": \"^20.17.6\",\n \"tsup\": \"^8.3.5\",\n \"tsx\": \"^4.19.2\",\n \"typescript\": \"^5.6.3\"\n }\n}\n"],"mappings":"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","names":["resolve","Router","Router","app","existsSync","join","join","existsSync","port","architecturePath","tecturePath","resolve"]}
package/dist/index.d.ts CHANGED
@@ -3,11 +3,19 @@ import { ArchitectureDataSource, LayoutStore, ManifestFile, DiagramFile, Diagram
3
3
  export * from '@tecture/shared';
4
4
  export { ArchitectureDataSource, DescriptionNotFoundError, DiagramNotFoundError, LayoutInvalidError, LayoutStore, NodeNotFoundError, SLUG_RE, buildArchitectureSummary, emptyLayout, findNode, isFiniteNumber, isValidLayoutEntry, normalizeLayoutUpdate } from '@tecture/shared';
5
5
 
6
+ type EventProps = Record<string, string | number | boolean>;
7
+ interface Reporter {
8
+ /** Send an anonymous usage event. No-op when telemetry is disabled. */
9
+ capture(event: string, properties?: EventProps): void;
10
+ }
11
+
6
12
  type SourceResolver = (req: Request) => ArchitectureDataSource | Promise<ArchitectureDataSource>;
7
13
  type LayoutStoreResolver = (req: Request) => LayoutStore | null | Promise<LayoutStore | null>;
8
14
  interface ArchitectureRouterOptions {
9
15
  resolveSource: SourceResolver;
10
16
  resolveLayoutStore?: LayoutStoreResolver;
17
+ /** Optional anonymous usage reporter (standalone viewer only). */
18
+ report?: Reporter;
11
19
  }
12
20
  declare function createArchitectureRouter(opts: ArchitectureRouterOptions): Router;
13
21
  declare function architectureErrorHandler(err: unknown, _req: Request, res: Response, next: NextFunction): void;
@@ -15,6 +23,8 @@ declare function architectureErrorHandler(err: unknown, _req: Request, res: Resp
15
23
  interface CreateAppOptions {
16
24
  source: ArchitectureDataSource | SourceResolver;
17
25
  layoutStore?: LayoutStore | LayoutStoreResolver | null;
26
+ /** Optional anonymous usage reporter (standalone viewer only). */
27
+ report?: Reporter;
18
28
  }
19
29
  declare function createApp(options: CreateAppOptions): Express;
20
30
 
package/dist/index.js CHANGED
@@ -173,8 +173,7 @@ function isDeepDivable(node) {
173
173
  return type !== void 0 && CODE_NODE_TYPES.has(type);
174
174
  }
175
175
  function buildDeepDivePrompt(node) {
176
- const named = node.label && node.label !== node.id ? ` (${node.label})` : "";
177
- return `Use the tecture skill to deep-dive the architecture component \`${node.id}\`${named}.`;
176
+ return `/architecture-docs deep-dive ${node.id}`;
178
177
  }
179
178
 
180
179
  // src/routes/architecture.ts
@@ -189,7 +188,7 @@ function send400(res, body) {
189
188
  }
190
189
  function createArchitectureRouter(opts) {
191
190
  const router = Router2();
192
- const { resolveSource, resolveLayoutStore } = opts;
191
+ const { resolveSource, resolveLayoutStore, report } = opts;
193
192
  router.get("/", async (req, res, next) => {
194
193
  try {
195
194
  const source = await resolveSource(req);
@@ -234,6 +233,10 @@ function createArchitectureRouter(opts) {
234
233
  edges: diagram.edges ?? []
235
234
  };
236
235
  res.json(body);
236
+ report?.capture(
237
+ "diagram.viewed",
238
+ typeof diagram.level === "number" ? { level: diagram.level } : {}
239
+ );
237
240
  } catch (err) {
238
241
  if (err instanceof DiagramNotFoundError) {
239
242
  return send404(res, { error: "diagram_not_found", slug: err.slug });
@@ -392,7 +395,8 @@ function createApp(options) {
392
395
  "/api/architecture",
393
396
  createArchitectureRouter({
394
397
  resolveSource: toSourceResolver(options.source),
395
- resolveLayoutStore: toLayoutStoreResolver(options.layoutStore)
398
+ resolveLayoutStore: toLayoutStoreResolver(options.layoutStore),
399
+ report: options.report
396
400
  })
397
401
  );
398
402
  app.use("/api/architecture", architectureErrorHandler);