codebase-onboarder 0.1.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.
Files changed (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -0
  3. package/bin/onboarder.js +14 -0
  4. package/bin/postinstall.js +6 -0
  5. package/cli/commands.js +436 -0
  6. package/cli/main.js +129 -0
  7. package/cli/prompt.js +101 -0
  8. package/cli/ui.js +55 -0
  9. package/cli/wizard.js +325 -0
  10. package/package.json +48 -0
  11. package/public/app.js +1339 -0
  12. package/public/index.html +455 -0
  13. package/public/js/about.js +284 -0
  14. package/public/js/aiDraft.js +163 -0
  15. package/public/js/analysisPanel.js +270 -0
  16. package/public/js/analysisReport.js +154 -0
  17. package/public/js/api.js +264 -0
  18. package/public/js/atlas.js +98 -0
  19. package/public/js/blameView.js +41 -0
  20. package/public/js/codeTab.js +256 -0
  21. package/public/js/codeViewer.js +184 -0
  22. package/public/js/components/FileChip.js +102 -0
  23. package/public/js/components/MetricSparkline.js +89 -0
  24. package/public/js/components/RiskBadge.js +105 -0
  25. package/public/js/deepAnalysisView.js +496 -0
  26. package/public/js/diagramPane.js +154 -0
  27. package/public/js/diffView.js +352 -0
  28. package/public/js/docsView.js +566 -0
  29. package/public/js/fileSourceBrowser.js +47 -0
  30. package/public/js/flameGraph.js +75 -0
  31. package/public/js/forceGraph.js +592 -0
  32. package/public/js/heatmap.js +185 -0
  33. package/public/js/highlight.js +122 -0
  34. package/public/js/html.js +50 -0
  35. package/public/js/insightsView.js +236 -0
  36. package/public/js/inspector.js +535 -0
  37. package/public/js/llm.js +345 -0
  38. package/public/js/mapView.js +159 -0
  39. package/public/js/markdown.js +119 -0
  40. package/public/js/mindmap.js +289 -0
  41. package/public/js/repoFiles.js +44 -0
  42. package/public/js/sbomView.js +142 -0
  43. package/public/js/scanCache.js +76 -0
  44. package/public/js/search.js +874 -0
  45. package/public/js/serverSettings.js +169 -0
  46. package/public/js/state.js +192 -0
  47. package/public/js/tour.js +17 -0
  48. package/public/js/transitions.js +15 -0
  49. package/public/js/tree.js +200 -0
  50. package/public/js/workflowsView.js +95 -0
  51. package/public/styles.css +2767 -0
  52. package/public/vendor/mermaid.min.js +3587 -0
  53. package/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  54. package/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
  55. package/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  56. package/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  57. package/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  58. package/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  59. package/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  60. package/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  61. package/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  62. package/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  63. package/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  64. package/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  65. package/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  66. package/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  67. package/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  68. package/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  69. package/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  70. package/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  71. package/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  72. package/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  73. package/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  74. package/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  75. package/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  76. package/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  77. package/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  78. package/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  79. package/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  80. package/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  81. package/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  82. package/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  83. package/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  84. package/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  85. package/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  86. package/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  87. package/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  88. package/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  89. package/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  90. package/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  91. package/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  92. package/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  93. package/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  94. package/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  95. package/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  96. package/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  97. package/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  98. package/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  99. package/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  100. package/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  101. package/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  102. package/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  103. package/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  104. package/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  105. package/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  106. package/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  107. package/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  108. package/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  109. package/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  110. package/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  111. package/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  112. package/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  113. package/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  114. package/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  115. package/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  116. package/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  117. package/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  118. package/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  119. package/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  120. package/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  121. package/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  122. package/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  123. package/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  124. package/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  125. package/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  126. package/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  127. package/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  128. package/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  129. package/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  130. package/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  131. package/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
  132. package/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  133. package/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  134. package/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  135. package/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  136. package/public/vendor/monaco/vs/editor/editor.main.css +8 -0
  137. package/public/vendor/monaco/vs/editor/editor.main.js +798 -0
  138. package/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
  139. package/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
  140. package/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  141. package/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
  142. package/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
  143. package/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
  144. package/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  145. package/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
  146. package/public/vendor/monaco/vs/loader.js +11 -0
  147. package/public/vendor/monaco/worker-boot.js +9 -0
  148. package/server/.fuse_hidden0000000800000001 +36 -0
  149. package/server/apiDiff.js +25 -0
  150. package/server/apiDocs.js +67 -0
  151. package/server/apiFile.js +36 -0
  152. package/server/apiGitBlame.js +63 -0
  153. package/server/apiMcp.js +110 -0
  154. package/server/apiScan.js +119 -0
  155. package/server/apiSearch.js +253 -0
  156. package/server/apiSettings.js +118 -0
  157. package/server/apiTools.js +90 -0
  158. package/server/config.js +227 -0
  159. package/server/fileSourceNode.js +44 -0
  160. package/server/gitClone.js +95 -0
  161. package/server/gitDiff.js +184 -0
  162. package/server/gitHistory.js +110 -0
  163. package/server/htmlText.js +44 -0
  164. package/server/http.js +55 -0
  165. package/server/httpGuards.js +87 -0
  166. package/server/index.js +156 -0
  167. package/server/llmProxy.js +162 -0
  168. package/server/logger.js +29 -0
  169. package/server/mcp/analysis.js +209 -0
  170. package/server/mcp/http.js +213 -0
  171. package/server/mcp/runner.js +278 -0
  172. package/server/mcp/server.js +241 -0
  173. package/server/mcp/standalone.js +42 -0
  174. package/server/mcp/tools.js +683 -0
  175. package/server/paths.js +39 -0
  176. package/server/router.js +218 -0
  177. package/server/searchIndex.js +118 -0
  178. package/server/sessions.js +163 -0
  179. package/server/static.js +59 -0
  180. package/server/tools/install.js +246 -0
  181. package/server/tools/parse.js +170 -0
  182. package/server/tools/platform.js +91 -0
  183. package/server/tools/registry.js +212 -0
  184. package/server/tools/scan.js +136 -0
  185. package/server/tools.js +212 -0
  186. package/server/tunnel.js +92 -0
  187. package/shared/analyzer/docs.js +135 -0
  188. package/shared/analyzer/explainLocal.js +163 -0
  189. package/shared/analyzer/graph.js +461 -0
  190. package/shared/analyzer/health.js +215 -0
  191. package/shared/analyzer/history.js +146 -0
  192. package/shared/analyzer/languages/csharp.js +39 -0
  193. package/shared/analyzer/languages/generic.js +131 -0
  194. package/shared/analyzer/languages/go.js +70 -0
  195. package/shared/analyzer/languages/index.js +43 -0
  196. package/shared/analyzer/languages/java.js +39 -0
  197. package/shared/analyzer/languages/javascript.js +240 -0
  198. package/shared/analyzer/languages/python.js +120 -0
  199. package/shared/analyzer/languages/rust.js +42 -0
  200. package/shared/analyzer/languages/typescript.js +138 -0
  201. package/shared/analyzer/licenses.js +151 -0
  202. package/shared/analyzer/metrics.js +120 -0
  203. package/shared/analyzer/pathUtil.js +69 -0
  204. package/shared/analyzer/patterns.js +272 -0
  205. package/shared/analyzer/scan.js +473 -0
  206. package/shared/analyzer/security.js +187 -0
  207. package/shared/analyzer/services.js +157 -0
  208. package/shared/analyzer/stack.js +173 -0
  209. package/shared/analyzer/tour.js +69 -0
  210. package/shared/analyzer/util.js +123 -0
  211. package/shared/analyzer/workflows.js +143 -0
  212. package/shared/diagram/aiFacts.js +145 -0
  213. package/shared/diagram/aiMermaid.js +59 -0
  214. package/shared/diagram/atlas.js +93 -0
  215. package/shared/diagram/mermaid.js +459 -0
  216. package/shared/search/query.js +518 -0
package/cli/ui.js ADDED
@@ -0,0 +1,55 @@
1
+ // Terminal paint: the few ANSI touches the CLI uses, all behind one flag.
2
+ //
3
+ // Everything goes through `paint` so --no-color, NO_COLOR, and a piped stdout
4
+ // all strip styling in one place. Nothing here is load-bearing — every message
5
+ // has to read fine as plain text, because that is how logs and CI see it.
6
+
7
+ export const supportsColor = process.stdout.isTTY && !process.env.NO_COLOR;
8
+
9
+ const CODES = {
10
+ reset: 0, bold: 1, dim: 2, italic: 3,
11
+ red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, gray: 90,
12
+ };
13
+
14
+ export function paint(text, ...styles) {
15
+ if (!supportsColor || !styles.length) return String(text);
16
+ const open = styles.map((s) => `\x1b[${CODES[s]}m`).join('');
17
+ return `${open}${text}\x1b[0m`;
18
+ }
19
+
20
+ export const ok = (s) => paint(s, 'green');
21
+ export const warn = (s) => paint(s, 'yellow');
22
+ export const bad = (s) => paint(s, 'red');
23
+ export const dim = (s) => paint(s, 'gray');
24
+ export const bold = (s) => paint(s, 'bold');
25
+ export const cyan = (s) => paint(s, 'cyan');
26
+
27
+ // The wizard's welcome — one screen, says what the tool is and what the wizard
28
+ // is about to touch (one file), and how to leave. Modeled on the honesty of a
29
+ // good installer: no art, no spinner, no mystery.
30
+ export function welcomeBanner(version, configFile) {
31
+ return [
32
+ '',
33
+ bold(' 🧭 Onboarder setup') + dim(` v${version}`),
34
+ dim(' Drop a path. Get a map.'),
35
+ '',
36
+ ' This wizard asks a handful of questions and writes one file:',
37
+ ' ' + cyan(configFile),
38
+ dim(' Nothing is sent anywhere. Ctrl-C at any question cancels without writing.'),
39
+ '',
40
+ ].join('\n');
41
+ }
42
+
43
+ export function section(title) {
44
+ return '\n' + bold(` ── ${title} ` + '─'.repeat(Math.max(2, 46 - title.length)));
45
+ }
46
+
47
+ // Label/value rows, aligned — the banner, `config show`, and the doctor all
48
+ // print in this shape so output greps the same everywhere.
49
+ export function kv(label, value) {
50
+ return ' ' + paint((label + ' ').padEnd(12), 'gray') + value;
51
+ }
52
+
53
+ export const tick = ok(' ✓ ');
54
+ export const cross = bad(' ✗ ');
55
+ export const dash = dim(' – ');
package/cli/wizard.js ADDED
@@ -0,0 +1,325 @@
1
+ // The setup wizard's brain, with no I/O in it.
2
+ //
3
+ // `buildSteps` turns the current settings (and any --flags) into the ordered
4
+ // list of questions, each with its default, its choices, its validator, and a
5
+ // `when` that decides from earlier answers whether the question exists at all
6
+ // (local mode never asks for a domain). `answersToSettings` folds the answers
7
+ // back into a settings object that `server/config.js` then validates — the
8
+ // same schema the HTTP settings API enforces, so the wizard can never write a
9
+ // config the server would refuse. `applyFlags` is the non-interactive twin of
10
+ // the same fold. All three are pure: the tests drive them directly, and the
11
+ // readline renderer in `prompt.js` is a thin shell over them.
12
+
13
+ import os from 'node:os';
14
+
15
+ import { DEFAULT_SETTINGS, generateAccessKey, normalizeSettings } from '../server/config.js';
16
+
17
+ // Provider presets for the AI account step. `none` is the honest default: the
18
+ // app works fully offline, and the browser can still hold its own key.
19
+ export const PROVIDER_PRESETS = {
20
+ none: { label: 'Skip — set it later (the app works offline)', baseUrl: '' },
21
+ 'openai-compatible': { label: 'OpenAI or any compatible endpoint', baseUrl: 'https://api.openai.com/v1' },
22
+ ollama: { label: 'Ollama (local models)', baseUrl: 'http://localhost:11434/v1' },
23
+ openrouter: { label: 'OpenRouter', baseUrl: 'https://openrouter.ai/api/v1' },
24
+ custom: { label: 'Custom base URL', baseUrl: '' },
25
+ };
26
+
27
+ export const TUNNEL_CHOICES = [
28
+ { value: 'none', label: 'No tunnel', hint: 'reach the server directly' },
29
+ { value: 'cloudflare', label: 'Cloudflare quick tunnel', hint: 'cloudflared — a public https://*.trycloudflare.com name' },
30
+ { value: 'tailscale', label: 'Tailscale serve', hint: 'your devices only, https://machine.tailnet.ts.net' },
31
+ { value: 'both', label: 'Both', hint: 'public via Cloudflare, private via tailnet' },
32
+ ];
33
+
34
+ export function validPort(value) {
35
+ const n = Number(String(value).trim());
36
+ if (!Number.isInteger(n) || n < 1 || n > 65535) return 'A whole number from 1 to 65535.';
37
+ return null;
38
+ }
39
+
40
+ export function validDomain(value) {
41
+ const domain = String(value || '').trim();
42
+ if (!domain) return null; // optional in some flows; required ones check separately
43
+ if (domain.length > 253 || /[\s/@\\]/.test(domain) || domain.startsWith('.') || !domain.includes('.')) {
44
+ return 'A hostname such as map.example.com.';
45
+ }
46
+ return null;
47
+ }
48
+
49
+ export function validEmail(value) {
50
+ const email = String(value || '').trim();
51
+ if (!email) return null; // optional
52
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? null : 'That does not look like an email address.';
53
+ }
54
+
55
+ function providerOf(account) {
56
+ if (!account?.baseUrl) return 'none';
57
+ for (const [id, preset] of Object.entries(PROVIDER_PRESETS)) {
58
+ if (id !== 'none' && id !== 'custom' && preset.baseUrl === account.baseUrl) return id;
59
+ }
60
+ return 'custom';
61
+ }
62
+
63
+ function tunnelChoiceOf(tunnel) {
64
+ if (tunnel?.cloudflare && tunnel?.tailscale) return 'both';
65
+ if (tunnel?.cloudflare) return 'cloudflare';
66
+ if (tunnel?.tailscale) return 'tailscale';
67
+ return 'none';
68
+ }
69
+
70
+ // The ordered question list. Every step's `when` reads the answers collected
71
+ // so far, which is what makes the wizard a tree rather than a script: answer
72
+ // "local" and the whole network/security branch never appears.
73
+ export function buildSteps(current = DEFAULT_SETTINGS) {
74
+ const account = current.account || DEFAULT_SETTINGS.account;
75
+ return [
76
+ {
77
+ id: 'name', section: 'Profile', type: 'text',
78
+ question: 'Your name',
79
+ hint: 'shown in the settings drawer; never leaves this machine',
80
+ default: account.name || safeUsername(),
81
+ },
82
+ {
83
+ id: 'email', section: 'Profile', type: 'text',
84
+ question: 'Your email (optional)',
85
+ hint: 'for your own reference only',
86
+ default: account.email || '',
87
+ validate: validEmail,
88
+ },
89
+ {
90
+ id: 'mode', section: 'Server', type: 'choice',
91
+ question: 'How will you run Onboarder?',
92
+ choices: [
93
+ { value: 'local', label: 'Local — localhost only', hint: 'private by construction; nothing on the network can reach it' },
94
+ { value: 'self-hosted', label: 'Self-hosted — domain, LAN, or tunnel', hint: 'reach it from other devices; every API call needs an access key' },
95
+ ],
96
+ default: current.mode,
97
+ },
98
+ {
99
+ id: 'host', section: 'Server', type: 'choice',
100
+ question: 'What should the server bind to?',
101
+ choices: [
102
+ { value: '127.0.0.1', label: 'Loopback, behind a tunnel (recommended)', hint: 'Cloudflare or Tailscale terminates TLS and dials 127.0.0.1' },
103
+ { value: '0.0.0.0', label: 'Every interface (LAN)', hint: 'other machines on this network reach it directly — plain HTTP, so front it with a domain + proxy for TLS' },
104
+ ],
105
+ default: current.host === '0.0.0.0' ? '0.0.0.0' : '127.0.0.1',
106
+ when: (a) => a.mode === 'self-hosted',
107
+ },
108
+ {
109
+ id: 'port', section: 'Server', type: 'text',
110
+ question: 'Port',
111
+ default: String(current.port),
112
+ validate: validPort,
113
+ },
114
+ {
115
+ id: 'domain', section: 'Server', type: 'text',
116
+ question: 'Domain (optional behind a tunnel)',
117
+ hint: 'e.g. map.example.com — required for a direct LAN bind, optional when a tunnel provides the name',
118
+ default: current.domain || '',
119
+ validate: validDomain,
120
+ when: (a) => a.mode === 'self-hosted',
121
+ },
122
+ {
123
+ id: 'keyChoice', section: 'Security', type: 'choice',
124
+ question: 'Access key — the one credential remote devices must hold',
125
+ choices: [
126
+ { value: 'generate', label: 'Generate a strong key (recommended)', hint: 'shown once at the end, stored in the config file' },
127
+ ...(current.accessKey ? [{ value: 'keep', label: `Keep the existing key (${current.accessKey.slice(0, 6)}…)`, hint: '' }] : []),
128
+ { value: 'enter', label: 'Enter my own', hint: '' },
129
+ ],
130
+ default: current.accessKey ? 'keep' : 'generate',
131
+ when: (a) => a.mode === 'self-hosted',
132
+ },
133
+ {
134
+ id: 'keyValue', section: 'Security', type: 'text',
135
+ question: 'Your access key',
136
+ hint: 'at least 16 characters; anyone holding it can drive the API',
137
+ validate: (v) => (String(v || '').trim().length >= 16 ? null : 'At least 16 characters.'),
138
+ when: (a) => a.mode === 'self-hosted' && a.keyChoice === 'enter',
139
+ },
140
+ {
141
+ id: 'provider', section: 'AI provider (optional)', type: 'choice',
142
+ question: 'Default AI provider for explain/chat features',
143
+ choices: Object.entries(PROVIDER_PRESETS).map(([value, p]) => ({ value, label: p.label, hint: p.baseUrl })),
144
+ default: providerOf(account),
145
+ },
146
+ {
147
+ id: 'baseUrl', section: 'AI provider (optional)', type: 'text',
148
+ question: 'Base URL',
149
+ default: (a) => PROVIDER_PRESETS[a.provider]?.baseUrl || account.baseUrl || '',
150
+ validate: (v) => (v && !/^https?:\/\//.test(v) ? 'An http(s) URL, e.g. https://api.openai.com/v1.' : null),
151
+ when: (a) => a.provider && a.provider !== 'none',
152
+ },
153
+ {
154
+ id: 'model', section: 'AI provider (optional)', type: 'text',
155
+ question: 'Model (optional)',
156
+ hint: 'e.g. gpt-4o-mini, llama3.1',
157
+ default: account.model || '',
158
+ when: (a) => a.provider && a.provider !== 'none',
159
+ },
160
+ {
161
+ id: 'tunnelChoice', section: 'Remote access', type: 'choice',
162
+ question: 'Expose it through a tunnel?',
163
+ hint: 'the wizard prints the exact command afterwards; missing CLIs get an install hint',
164
+ choices: TUNNEL_CHOICES,
165
+ default: tunnelChoiceOf(current.tunnel),
166
+ when: (a) => a.mode === 'self-hosted',
167
+ },
168
+ {
169
+ id: 'autoOpen', section: 'Finish', type: 'confirm',
170
+ question: 'Open the app in a browser when the server starts?',
171
+ default: current.autoOpen !== false,
172
+ },
173
+ ];
174
+ }
175
+
176
+
177
+ function safeUsername() {
178
+ try {
179
+ return os.userInfo().username || '';
180
+ } catch {
181
+ return '';
182
+ }
183
+ }
184
+
185
+ // Steps for one run, given what is already answered: flags pre-answer questions
186
+ // (a flagged question is never asked), and `when` prunes dead branches. The
187
+ // answers object grows as the run proceeds, so a later step's `when` sees both
188
+ // flag answers and typed answers through the same lens.
189
+ export function pendingSteps(steps, answers = {}) {
190
+ return steps.filter((s) => !(s.id in answers) && (!s.when || s.when(answers)));
191
+ }
192
+
193
+ // Defaults may depend on earlier answers (`default` as a function).
194
+ export function defaultOf(step, answers) {
195
+ return typeof step.default === 'function' ? step.default(answers) : step.default;
196
+ }
197
+
198
+ // Answers → settings. The key rule: this builds a plain object and hands it to
199
+ // normalizeSettings, so an impossible combination (local mode on 0.0.0.0, a
200
+ // LAN bind with no domain) fails with the schema's own sentence here, in the
201
+ // API, and in `config set` — one law, three courts.
202
+ export function answersToSettings(current, answers) {
203
+ const selfHosted = answers.mode === 'self-hosted';
204
+ const provider = answers.provider || 'none';
205
+ const account = {
206
+ name: answers.name ?? current.account?.name ?? '',
207
+ email: answers.email ?? current.account?.email ?? '',
208
+ provider: provider === 'none' ? 'openai-compatible' : provider,
209
+ baseUrl: provider === 'none' ? '' : String(answers.baseUrl ?? '').trim().replace(/\/+$/, ''),
210
+ model: provider === 'none' ? '' : String(answers.model ?? '').trim(),
211
+ };
212
+ let accessKey = current.accessKey || '';
213
+ if (selfHosted) {
214
+ if (answers.keyChoice === 'generate' || (!answers.keyChoice && !accessKey)) accessKey = generateAccessKey();
215
+ else if (answers.keyChoice === 'enter') accessKey = String(answers.keyValue || '').trim();
216
+ }
217
+ const choice = answers.tunnelChoice || tunnelChoiceOf(current.tunnel);
218
+ const tunnel = selfHosted
219
+ ? { cloudflare: choice === 'cloudflare' || choice === 'both', tailscale: choice === 'tailscale' || choice === 'both' }
220
+ : { ...(current.tunnel || DEFAULT_SETTINGS.tunnel) };
221
+ return normalizeSettings({
222
+ version: current.version,
223
+ mode: selfHosted ? 'self-hosted' : 'local',
224
+ host: selfHosted ? (answers.host || current.host || '127.0.0.1') : '127.0.0.1',
225
+ port: Number(answers.port ?? current.port ?? DEFAULT_SETTINGS.port),
226
+ domain: selfHosted ? String(answers.domain ?? current.domain ?? '').trim().toLowerCase() : '',
227
+ accessKey,
228
+ autoOpen: answers.autoOpen ?? current.autoOpen ?? false,
229
+ account,
230
+ tunnel,
231
+ });
232
+ }
233
+
234
+
235
+ // The non-interactive twin: --flags become the same patch the wizard would
236
+ // have collected. Anything unflagged falls back to the current settings, and
237
+ // the whole result goes through the same normalizeSettings validation, so
238
+ // `onboarder setup --non-interactive --mode self-hosted --port 8080` can never
239
+ // produce a config the interactive wizard would refuse.
240
+ export function applyFlags(current, flags = {}) {
241
+ const patch = {};
242
+ if (flags.mode !== undefined) {
243
+ if (!['local', 'self-hosted'].includes(flags.mode)) throw new Error('--mode must be "local" or "self-hosted".');
244
+ patch.mode = flags.mode;
245
+ }
246
+ const mode = patch.mode || current.mode;
247
+ if (flags.host !== undefined) patch.host = flags.host;
248
+ if (flags.port !== undefined) {
249
+ const err = validPort(flags.port);
250
+ if (err) throw new Error('--port: ' + err);
251
+ patch.port = Number(flags.port);
252
+ }
253
+ if (flags.domain !== undefined) {
254
+ const err = validDomain(flags.domain);
255
+ if (err) throw new Error('--domain: ' + err);
256
+ patch.domain = String(flags.domain).trim().toLowerCase();
257
+ }
258
+ if (flags.accessKey === 'generate') patch.accessKey = generateAccessKey();
259
+ else if (flags.accessKey !== undefined) {
260
+ if (String(flags.accessKey).trim().length < 16) throw new Error('--access-key must be at least 16 characters, or "generate".');
261
+ patch.accessKey = String(flags.accessKey).trim();
262
+ } else if (mode === 'self-hosted' && !current.accessKey) {
263
+ // Self-hosted without a key is a locked door with no handle — generate one
264
+ // rather than write a config that refuses every call.
265
+ patch.accessKey = generateAccessKey();
266
+ }
267
+ if (flags.autoOpen !== undefined) patch.autoOpen = Boolean(flags.autoOpen);
268
+ const account = {};
269
+ if (flags.name !== undefined) account.name = String(flags.name);
270
+ if (flags.email !== undefined) {
271
+ const err = validEmail(flags.email);
272
+ if (err) throw new Error('--email: ' + err);
273
+ account.email = String(flags.email);
274
+ }
275
+ if (flags.provider !== undefined) {
276
+ if (!(flags.provider in PROVIDER_PRESETS)) throw new Error('--provider must be one of: ' + Object.keys(PROVIDER_PRESETS).join(', '));
277
+ account.provider = flags.provider === 'none' ? 'openai-compatible' : flags.provider;
278
+ account.baseUrl = PROVIDER_PRESETS[flags.provider]?.baseUrl || '';
279
+ if (flags.provider === 'none') account.model = '';
280
+ }
281
+ if (flags.baseUrl !== undefined) account.baseUrl = String(flags.baseUrl).trim().replace(/\/+$/, '');
282
+ if (flags.model !== undefined) account.model = String(flags.model).trim();
283
+ if (Object.keys(account).length) patch.account = account;
284
+ const tunnel = {};
285
+ if (flags.cloudflare !== undefined) tunnel.cloudflare = Boolean(flags.cloudflare);
286
+ if (flags.tailscale !== undefined) tunnel.tailscale = Boolean(flags.tailscale);
287
+ if (Object.keys(tunnel).length) patch.tunnel = tunnel;
288
+ const merged = {
289
+ ...current, ...patch,
290
+ account: { ...(current.account || DEFAULT_SETTINGS.account), ...account },
291
+ tunnel: { ...(current.tunnel || DEFAULT_SETTINGS.tunnel), ...tunnel },
292
+ };
293
+ // Mode drove host/domain rules inside normalize; when flags move a config to
294
+ // local mode the network fields have to come along rather than linger.
295
+ if (merged.mode === 'local') {
296
+ if (!['127.0.0.1', 'localhost', '::1', '[::1]'].includes(merged.host)) merged.host = '127.0.0.1';
297
+ if (flags.domain === undefined) merged.domain = '';
298
+ }
299
+ return { patch, settings: normalizeSettings(merged) };
300
+ }
301
+
302
+ // What the wizard's last screen shows before writing. `reveal` is the one
303
+ // moment the key is printed — rotation through the API has the same rule.
304
+ export function summaryLines(settings, { revealKey = false } = {}) {
305
+ const lines = [
306
+ ['Mode', settings.mode],
307
+ ['Bind', `${settings.host}:${settings.port}`],
308
+ ];
309
+ if (settings.domain) lines.push(['Domain', 'https://' + settings.domain]);
310
+ if (settings.mode === 'self-hosted') {
311
+ lines.push(['Access key', revealKey
312
+ ? settings.accessKey
313
+ : (settings.accessKey ? settings.accessKey.slice(0, 6) + '… (hidden)' : '(none — the API will refuse every call)')]);
314
+ }
315
+ const who = [settings.account.name, settings.account.email].filter(Boolean).join(' · ');
316
+ if (who) lines.push(['Profile', who]);
317
+ if (settings.account.baseUrl) {
318
+ lines.push(['AI', `${settings.account.provider} — ${settings.account.baseUrl}${settings.account.model ? ' — ' + settings.account.model : ''}`]);
319
+ }
320
+ const tunnels = [settings.tunnel.cloudflare && 'cloudflare', settings.tunnel.tailscale && 'tailscale'].filter(Boolean);
321
+ if (tunnels.length) lines.push(['Tunnels', tunnels.join(' + ')]);
322
+ lines.push(['Auto-open', settings.autoOpen ? 'yes' : 'no']);
323
+ return lines;
324
+ }
325
+
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "codebase-onboarder",
3
+ "version": "0.1.0",
4
+ "description": "Drop a path. Get a map. A zero-dependency codebase visualizer with a CLI onboarding wizard, a web UI, and an optional key-gated self-hosted mode.",
5
+ "type": "module",
6
+ "bin": {
7
+ "onboarder": "bin/onboarder.js",
8
+ "codebase-onboarder": "bin/onboarder.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "cli",
13
+ "server",
14
+ "shared",
15
+ "public",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "start": "node server/index.js",
21
+ "mcp": "node server/mcp/standalone.js",
22
+ "test": "node --test",
23
+ "postinstall": "node bin/postinstall.js",
24
+ "prepublishOnly": "npm test"
25
+ },
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "keywords": [
30
+ "codebase",
31
+ "visualization",
32
+ "architecture",
33
+ "onboarding",
34
+ "diagram",
35
+ "mermaid",
36
+ "mcp",
37
+ "self-hosted",
38
+ "developer-tools"
39
+ ],
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/Amitpandey88/onboarder.git"
43
+ },
44
+ "homepage": "https://github.com/Amitpandey88/onboarder#readme",
45
+ "bugs": "https://github.com/Amitpandey88/onboarder/issues",
46
+ "author": "Amit Pandey",
47
+ "license": "MIT"
48
+ }