codebase-onboarder 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -47
- package/bin/build-if-source.js +23 -0
- package/cli/commands.d.ts +26 -0
- package/cli/commands.js +836 -811
- package/cli/commands.js.map +1 -0
- package/cli/explorer/advanced.d.ts +14 -0
- package/cli/explorer/advanced.js +449 -0
- package/cli/explorer/advanced.js.map +1 -0
- package/cli/explorer/app.d.ts +1 -0
- package/cli/explorer/app.js +396 -229
- package/cli/explorer/app.js.map +1 -0
- package/cli/explorer/assistant.d.ts +9 -0
- package/cli/explorer/assistant.js +58 -0
- package/cli/explorer/assistant.js.map +1 -0
- package/cli/explorer/commands.d.ts +17 -0
- package/cli/explorer/commands.js +400 -162
- package/cli/explorer/commands.js.map +1 -0
- package/cli/explorer/featureViews.d.ts +124 -0
- package/cli/explorer/featureViews.js +179 -0
- package/cli/explorer/featureViews.js.map +1 -0
- package/cli/explorer/github.d.ts +21 -0
- package/cli/explorer/github.js +73 -0
- package/cli/explorer/github.js.map +1 -0
- package/cli/explorer/graphs.d.ts +7 -0
- package/cli/explorer/graphs.js +173 -0
- package/cli/explorer/graphs.js.map +1 -0
- package/cli/explorer/picker.d.ts +22 -0
- package/cli/explorer/picker.js +150 -0
- package/cli/explorer/picker.js.map +1 -0
- package/cli/explorer/report.d.ts +85 -0
- package/cli/explorer/report.js +60 -0
- package/cli/explorer/report.js.map +1 -0
- package/cli/explorer/session.d.ts +449 -0
- package/cli/explorer/session.js +184 -95
- package/cli/explorer/session.js.map +1 -0
- package/cli/explorer/views.d.ts +19 -0
- package/cli/explorer/views.js +496 -412
- package/cli/explorer/views.js.map +1 -0
- package/cli/explorer/wrap.d.ts +1 -0
- package/cli/explorer/wrap.js +33 -0
- package/cli/explorer/wrap.js.map +1 -0
- package/cli/main.d.ts +1 -0
- package/cli/main.js +235 -138
- package/cli/main.js.map +1 -0
- package/cli/prompt.d.ts +5 -0
- package/cli/prompt.js +84 -80
- package/cli/prompt.js.map +1 -0
- package/cli/scanOptions.d.ts +2 -0
- package/cli/scanOptions.js +11 -0
- package/cli/scanOptions.js.map +1 -0
- package/cli/ui.d.ts +33 -0
- package/cli/ui.js +55 -35
- package/cli/ui.js.map +1 -0
- package/cli/wizard.d.ts +187 -0
- package/cli/wizard.js +304 -284
- package/cli/wizard.js.map +1 -0
- package/package.json +15 -4
- package/public/app.d.ts +1 -0
- package/public/app.js +1057 -1009
- package/public/app.js.map +1 -0
- package/public/browser.d.ts +68 -0
- package/public/browser.js +2 -0
- package/public/browser.js.map +1 -0
- package/public/index.html +30 -13
- package/public/js/about.d.ts +6 -0
- package/public/js/about.js +165 -172
- package/public/js/about.js.map +1 -0
- package/public/js/aiDraft.d.ts +3 -0
- package/public/js/aiDraft.js +116 -124
- package/public/js/aiDraft.js.map +1 -0
- package/public/js/analysisPanel.d.ts +7 -0
- package/public/js/analysisPanel.js +166 -166
- package/public/js/analysisPanel.js.map +1 -0
- package/public/js/analysisReport.d.ts +38 -0
- package/public/js/analysisReport.js +105 -110
- package/public/js/analysisReport.js.map +1 -0
- package/public/js/api.d.ts +26 -0
- package/public/js/api.js +176 -167
- package/public/js/api.js.map +1 -0
- package/public/js/atlas.d.ts +6 -0
- package/public/js/atlas.js +50 -58
- package/public/js/atlas.js.map +1 -0
- package/public/js/blameView.d.ts +2 -0
- package/public/js/blameView.js +36 -37
- package/public/js/blameView.js.map +1 -0
- package/public/js/codeTab.d.ts +3 -0
- package/public/js/codeTab.js +175 -180
- package/public/js/codeTab.js.map +1 -0
- package/public/js/codeViewer.d.ts +5 -0
- package/public/js/codeViewer.js +162 -150
- package/public/js/codeViewer.js.map +1 -0
- package/public/js/components/FileChip.d.ts +9 -0
- package/public/js/components/FileChip.js +45 -49
- package/public/js/components/FileChip.js.map +1 -0
- package/public/js/components/MetricSparkline.d.ts +7 -0
- package/public/js/components/MetricSparkline.js +35 -44
- package/public/js/components/MetricSparkline.js.map +1 -0
- package/public/js/components/RiskBadge.d.ts +9 -0
- package/public/js/components/RiskBadge.js +55 -63
- package/public/js/components/RiskBadge.js.map +1 -0
- package/public/js/deepAnalysisView.d.ts +99 -0
- package/public/js/deepAnalysisView.js +282 -302
- package/public/js/deepAnalysisView.js.map +1 -0
- package/public/js/diagramPane.d.ts +8 -0
- package/public/js/diagramPane.js +133 -135
- package/public/js/diagramPane.js.map +1 -0
- package/public/js/diffView.d.ts +6 -0
- package/public/js/diffView.js +222 -209
- package/public/js/diffView.js.map +1 -0
- package/public/js/docsView.d.ts +2 -0
- package/public/js/docsView.js +441 -442
- package/public/js/docsView.js.map +1 -0
- package/public/js/dom.d.ts +189 -0
- package/public/js/dom.js +5 -0
- package/public/js/dom.js.map +1 -0
- package/public/js/fileSourceBrowser.d.ts +5 -0
- package/public/js/fileSourceBrowser.js +33 -41
- package/public/js/fileSourceBrowser.js.map +1 -0
- package/public/js/flameGraph.d.ts +5 -0
- package/public/js/flameGraph.js +62 -72
- package/public/js/flameGraph.js.map +1 -0
- package/public/js/forceGraph.d.ts +10 -0
- package/public/js/forceGraph.js +581 -556
- package/public/js/forceGraph.js.map +1 -0
- package/public/js/heatmap.d.ts +4 -0
- package/public/js/heatmap.js +142 -164
- package/public/js/heatmap.js.map +1 -0
- package/public/js/highlight.d.ts +2 -0
- package/public/js/highlight.js +116 -107
- package/public/js/highlight.js.map +1 -0
- package/public/js/html.d.ts +2 -0
- package/public/js/html.js +14 -44
- package/public/js/html.js.map +1 -0
- package/public/js/insightsView.d.ts +5 -0
- package/public/js/insightsView.js +93 -111
- package/public/js/insightsView.js.map +1 -0
- package/public/js/inspector.d.ts +37 -0
- package/public/js/inspector.js +416 -445
- package/public/js/inspector.js.map +1 -0
- package/public/js/llm.d.ts +113 -0
- package/public/js/llm.js +233 -272
- package/public/js/llm.js.map +1 -0
- package/public/js/mapView.d.ts +3 -0
- package/public/js/mapView.js +121 -120
- package/public/js/mapView.js.map +1 -0
- package/public/js/markdown.d.ts +3 -0
- package/public/js/markdown.js +84 -91
- package/public/js/markdown.js.map +1 -0
- package/public/js/mindmap.d.ts +32 -0
- package/public/js/mindmap.js +244 -259
- package/public/js/mindmap.js.map +1 -0
- package/public/js/mobilePanels.d.ts +5 -0
- package/public/js/mobilePanels.js +50 -0
- package/public/js/mobilePanels.js.map +1 -0
- package/public/js/repoFiles.d.ts +12 -0
- package/public/js/repoFiles.js +28 -27
- package/public/js/repoFiles.js.map +1 -0
- package/public/js/reviewView.d.ts +28 -0
- package/public/js/reviewView.js +256 -0
- package/public/js/reviewView.js.map +1 -0
- package/public/js/sbomView.d.ts +3 -0
- package/public/js/sbomView.js +59 -67
- package/public/js/sbomView.js.map +1 -0
- package/public/js/scanCache.d.ts +16 -0
- package/public/js/scanCache.js +67 -65
- package/public/js/scanCache.js.map +1 -0
- package/public/js/search.d.ts +7 -0
- package/public/js/search.js +868 -800
- package/public/js/search.js.map +1 -0
- package/public/js/searchLocal.d.ts +42 -0
- package/public/js/searchLocal.js +87 -0
- package/public/js/searchLocal.js.map +1 -0
- package/public/js/serverSettings.d.ts +5 -0
- package/public/js/serverSettings.js +153 -164
- package/public/js/serverSettings.js.map +1 -0
- package/public/js/state.d.ts +798 -0
- package/public/js/state.js +114 -122
- package/public/js/state.js.map +1 -0
- package/public/js/tour.d.ts +6 -0
- package/public/js/tour.js +5 -7
- package/public/js/tour.js.map +1 -0
- package/public/js/transitions.d.ts +2 -0
- package/public/js/transitions.js +9 -10
- package/public/js/transitions.js.map +1 -0
- package/public/js/tree.d.ts +12 -0
- package/public/js/tree.js +157 -154
- package/public/js/tree.js.map +1 -0
- package/public/js/workflowsView.d.ts +3 -0
- package/public/js/workflowsView.js +15 -22
- package/public/js/workflowsView.js.map +1 -0
- package/public/search.css +495 -0
- package/public/styles.css +163 -346
- package/public/workbench.css +166 -0
- package/server/apiAuth.d.ts +3 -0
- package/server/apiAuth.js +18 -20
- package/server/apiAuth.js.map +1 -0
- package/server/apiDiff.d.ts +2 -0
- package/server/apiDiff.js +20 -18
- package/server/apiDiff.js.map +1 -0
- package/server/apiDocs.d.ts +2 -0
- package/server/apiDocs.js +38 -40
- package/server/apiDocs.js.map +1 -0
- package/server/apiFile.d.ts +1 -0
- package/server/apiFile.js +14 -17
- package/server/apiFile.js.map +1 -0
- package/server/apiGitBlame.d.ts +2 -0
- package/server/apiGitBlame.js +50 -51
- package/server/apiGitBlame.js.map +1 -0
- package/server/apiMcp.d.ts +4 -0
- package/server/apiMcp.js +64 -66
- package/server/apiMcp.js.map +1 -0
- package/server/apiReview.d.ts +2 -0
- package/server/apiReview.js +45 -0
- package/server/apiReview.js.map +1 -0
- package/server/apiScan.d.ts +5 -0
- package/server/apiScan.js +90 -71
- package/server/apiScan.js.map +1 -0
- package/server/apiSearch.d.ts +33 -0
- package/server/apiSearch.js +184 -200
- package/server/apiSearch.js.map +1 -0
- package/server/apiSettings.d.ts +4 -0
- package/server/apiSettings.js +84 -85
- package/server/apiSettings.js.map +1 -0
- package/server/apiTools.d.ts +3 -0
- package/server/apiTools.js +55 -56
- package/server/apiTools.js.map +1 -0
- package/server/auth.d.ts +5 -0
- package/server/auth.js +54 -51
- package/server/auth.js.map +1 -0
- package/server/config.d.ts +42 -0
- package/server/config.js +189 -194
- package/server/config.js.map +1 -0
- package/server/daemon.d.ts +12 -0
- package/server/daemon.js +124 -114
- package/server/daemon.js.map +1 -0
- package/server/fileSourceNode.d.ts +2 -0
- package/server/fileSourceNode.js +34 -36
- package/server/fileSourceNode.js.map +1 -0
- package/server/gitClone.d.ts +9 -0
- package/server/gitClone.js +59 -61
- package/server/gitClone.js.map +1 -0
- package/server/gitDiff.d.ts +36 -0
- package/server/gitDiff.js +271 -169
- package/server/gitDiff.js.map +1 -0
- package/server/gitHistory.d.ts +12 -0
- package/server/gitHistory.js +80 -80
- package/server/gitHistory.js.map +1 -0
- package/server/gitRemote.d.ts +12 -0
- package/server/gitRemote.js +60 -0
- package/server/gitRemote.js.map +1 -0
- package/server/htmlText.d.ts +4 -0
- package/server/htmlText.js +16 -23
- package/server/htmlText.js.map +1 -0
- package/server/http.d.ts +5 -0
- package/server/http.js +27 -30
- package/server/http.js.map +1 -0
- package/server/httpGuards.d.ts +2 -0
- package/server/httpGuards.js +47 -43
- package/server/httpGuards.js.map +1 -0
- package/server/https.d.ts +34 -0
- package/server/https.js +121 -111
- package/server/https.js.map +1 -0
- package/server/index.d.ts +40 -0
- package/server/index.js +139 -147
- package/server/index.js.map +1 -0
- package/server/layout.d.ts +26 -0
- package/server/layout.js +59 -37
- package/server/layout.js.map +1 -0
- package/server/llmProxy.d.ts +3 -0
- package/server/llmProxy.js +137 -135
- package/server/llmProxy.js.map +1 -0
- package/server/logger.d.ts +38 -0
- package/server/logger.js +198 -211
- package/server/logger.js.map +1 -0
- package/server/mcp/analysis.d.ts +29 -0
- package/server/mcp/analysis.js +99 -120
- package/server/mcp/analysis.js.map +1 -0
- package/server/mcp/http.d.ts +9 -0
- package/server/mcp/http.js +128 -137
- package/server/mcp/http.js.map +1 -0
- package/server/mcp/runner.d.ts +68 -0
- package/server/mcp/runner.js +208 -218
- package/server/mcp/runner.js.map +1 -0
- package/server/mcp/server.d.ts +29 -0
- package/server/mcp/server.js +166 -179
- package/server/mcp/server.js.map +1 -0
- package/server/mcp/standalone.d.ts +1 -0
- package/server/mcp/standalone.js +8 -10
- package/server/mcp/standalone.js.map +1 -0
- package/server/mcp/tools.d.ts +2020 -0
- package/server/mcp/tools.js +523 -574
- package/server/mcp/tools.js.map +1 -0
- package/server/paths.d.ts +3 -0
- package/server/paths.js +17 -15
- package/server/paths.js.map +1 -0
- package/server/pidfile.d.ts +9 -0
- package/server/pidfile.js +43 -45
- package/server/pidfile.js.map +1 -0
- package/server/review.d.ts +5 -0
- package/server/review.js +35 -0
- package/server/review.js.map +1 -0
- package/server/router.d.ts +33 -0
- package/server/router.js +191 -197
- package/server/router.js.map +1 -0
- package/server/scanRequest.d.ts +3 -0
- package/server/scanRequest.js +36 -0
- package/server/scanRequest.js.map +1 -0
- package/server/searchIndex.d.ts +17 -0
- package/server/searchIndex.js +83 -80
- package/server/searchIndex.js.map +1 -0
- package/server/sessions.d.ts +14 -0
- package/server/sessions.js +62 -70
- package/server/sessions.js.map +1 -0
- package/server/startup.d.ts +118 -0
- package/server/startup.js +143 -148
- package/server/startup.js.map +1 -0
- package/server/static.d.ts +6 -0
- package/server/static.js +28 -31
- package/server/static.js.map +1 -0
- package/server/tools/install.d.ts +4 -0
- package/server/tools/install.js +176 -185
- package/server/tools/install.js.map +1 -0
- package/server/tools/parse.d.ts +8 -0
- package/server/tools/parse.js +112 -114
- package/server/tools/parse.js.map +1 -0
- package/server/tools/platform.d.ts +9 -0
- package/server/tools/platform.js +49 -47
- package/server/tools/platform.js.map +1 -0
- package/server/tools/registry.d.ts +137 -0
- package/server/tools/registry.js +173 -172
- package/server/tools/registry.js.map +1 -0
- package/server/tools/scan.d.ts +34 -0
- package/server/tools/scan.js +89 -98
- package/server/tools/scan.js.map +1 -0
- package/server/tools.d.ts +34 -0
- package/server/tools.js +94 -107
- package/server/tools.js.map +1 -0
- package/server/tunnel.d.ts +29 -0
- package/server/tunnel.js +61 -66
- package/server/tunnel.js.map +1 -0
- package/shared/analyzer/docs.d.ts +16 -0
- package/shared/analyzer/docs.js +99 -93
- package/shared/analyzer/docs.js.map +1 -0
- package/shared/analyzer/explainLocal.d.ts +4 -0
- package/shared/analyzer/explainLocal.js +122 -142
- package/shared/analyzer/explainLocal.js.map +1 -0
- package/shared/analyzer/github.d.ts +37 -0
- package/shared/analyzer/github.js +79 -0
- package/shared/analyzer/github.js.map +1 -0
- package/shared/analyzer/graph.d.ts +41 -0
- package/shared/analyzer/graph.js +393 -400
- package/shared/analyzer/graph.js.map +1 -0
- package/shared/analyzer/health.d.ts +33 -0
- package/shared/analyzer/health.js +197 -193
- package/shared/analyzer/health.js.map +1 -0
- package/shared/analyzer/history.d.ts +29 -0
- package/shared/analyzer/history.js +110 -111
- package/shared/analyzer/history.js.map +1 -0
- package/shared/analyzer/jsonConfig.d.ts +2 -0
- package/shared/analyzer/jsonConfig.js +61 -0
- package/shared/analyzer/jsonConfig.js.map +1 -0
- package/shared/analyzer/languages/csharp.d.ts +26 -0
- package/shared/analyzer/languages/csharp.js +83 -88
- package/shared/analyzer/languages/csharp.js.map +1 -0
- package/shared/analyzer/languages/generic.d.ts +23 -0
- package/shared/analyzer/languages/generic.js +94 -105
- package/shared/analyzer/languages/generic.js.map +1 -0
- package/shared/analyzer/languages/go.d.ts +24 -0
- package/shared/analyzer/languages/go.js +45 -54
- package/shared/analyzer/languages/go.js.map +1 -0
- package/shared/analyzer/languages/index.d.ts +6 -0
- package/shared/analyzer/languages/index.js +19 -20
- package/shared/analyzer/languages/index.js.map +1 -0
- package/shared/analyzer/languages/java.d.ts +20 -0
- package/shared/analyzer/languages/java.js +73 -78
- package/shared/analyzer/languages/java.js.map +1 -0
- package/shared/analyzer/languages/javascript.d.ts +12 -0
- package/shared/analyzer/languages/javascript.js +258 -209
- package/shared/analyzer/languages/javascript.js.map +1 -0
- package/shared/analyzer/languages/python.d.ts +23 -0
- package/shared/analyzer/languages/python.js +92 -100
- package/shared/analyzer/languages/python.js.map +1 -0
- package/shared/analyzer/languages/rust.d.ts +17 -0
- package/shared/analyzer/languages/rust.js +96 -98
- package/shared/analyzer/languages/rust.js.map +1 -0
- package/shared/analyzer/languages/typescript.d.ts +11 -0
- package/shared/analyzer/languages/typescript.js +117 -116
- package/shared/analyzer/languages/typescript.js.map +1 -0
- package/shared/analyzer/licenses.d.ts +18 -0
- package/shared/analyzer/licenses.js +130 -133
- package/shared/analyzer/licenses.js.map +1 -0
- package/shared/analyzer/metrics.d.ts +20 -0
- package/shared/analyzer/metrics.js +112 -104
- package/shared/analyzer/metrics.js.map +1 -0
- package/shared/analyzer/pathUtil.d.ts +10 -0
- package/shared/analyzer/pathUtil.js +29 -35
- package/shared/analyzer/pathUtil.js.map +1 -0
- package/shared/analyzer/patterns.d.ts +11 -0
- package/shared/analyzer/patterns.js +231 -253
- package/shared/analyzer/patterns.js.map +1 -0
- package/shared/analyzer/scan.d.ts +62 -0
- package/shared/analyzer/scan.js +451 -442
- package/shared/analyzer/scan.js.map +1 -0
- package/shared/analyzer/scanControl.d.ts +11 -0
- package/shared/analyzer/scanControl.js +62 -0
- package/shared/analyzer/scanControl.js.map +1 -0
- package/shared/analyzer/security.d.ts +18 -0
- package/shared/analyzer/security.js +159 -159
- package/shared/analyzer/security.js.map +1 -0
- package/shared/analyzer/services.d.ts +5 -0
- package/shared/analyzer/services.js +141 -138
- package/shared/analyzer/services.js.map +1 -0
- package/shared/analyzer/stack.d.ts +6 -0
- package/shared/analyzer/stack.js +134 -141
- package/shared/analyzer/stack.js.map +1 -0
- package/shared/analyzer/tour.d.ts +2 -0
- package/shared/analyzer/tour.js +45 -53
- package/shared/analyzer/tour.js.map +1 -0
- package/shared/analyzer/util.d.ts +8 -0
- package/shared/analyzer/util.js +130 -76
- package/shared/analyzer/util.js.map +1 -0
- package/shared/analyzer/workflows.d.ts +8 -0
- package/shared/analyzer/workflows.js +139 -134
- package/shared/analyzer/workflows.js.map +1 -0
- package/shared/contracts.d.ts +250 -0
- package/shared/contracts.js +2 -0
- package/shared/contracts.js.map +1 -0
- package/shared/diagram/aiFacts.d.ts +35 -0
- package/shared/diagram/aiFacts.js +102 -105
- package/shared/diagram/aiFacts.js.map +1 -0
- package/shared/diagram/aiMermaid.d.ts +5 -0
- package/shared/diagram/aiMermaid.js +46 -45
- package/shared/diagram/aiMermaid.js.map +1 -0
- package/shared/diagram/atlas.d.ts +16 -0
- package/shared/diagram/atlas.js +51 -60
- package/shared/diagram/atlas.js.map +1 -0
- package/shared/diagram/mermaid.d.ts +43 -0
- package/shared/diagram/mermaid.js +348 -370
- package/shared/diagram/mermaid.js.map +1 -0
- package/shared/review/contracts.d.ts +95 -0
- package/shared/review/contracts.js +2 -0
- package/shared/review/contracts.js.map +1 -0
- package/shared/review/review.d.ts +10 -0
- package/shared/review/review.js +184 -0
- package/shared/review/review.js.map +1 -0
- package/shared/search/query.d.ts +32 -0
- package/shared/search/query.js +395 -388
- package/shared/search/query.js.map +1 -0
package/cli/commands.js
CHANGED
|
@@ -4,16 +4,11 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Every command returns an exit code instead of calling process.exit — the
|
|
6
6
|
// bin wrapper is the only place that touches the process.
|
|
7
|
-
|
|
8
7
|
import fs from 'node:fs';
|
|
9
8
|
import fsp from 'node:fs/promises';
|
|
10
9
|
import net from 'node:net';
|
|
11
10
|
import { spawn } from 'node:child_process';
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
DEFAULT_SETTINGS, configPath, configExists, readSettings, writeSettings,
|
|
15
|
-
publicSettings, serverUrls, maskAccessKey, isLoopbackHost,
|
|
16
|
-
} from '../server/config.js';
|
|
11
|
+
import { DEFAULT_SETTINGS, configPath, configExists, readSettings, writeSettings, publicSettings, serverUrls, maskAccessKey, isLoopbackHost, } from '../server/config.js';
|
|
17
12
|
import { startServer } from '../server/index.js';
|
|
18
13
|
import { pidIsAlive, readPidFile, removePidFile, readRunInfo, runInfoPath } from '../server/pidfile.js';
|
|
19
14
|
import { logPath, rotateLogIfNeeded, tailLog, followLog, logExists, spawnDetached, waitForPidFile, waitForReady } from '../server/daemon.js';
|
|
@@ -23,165 +18,164 @@ import { caddyRun, caddyValidate, httpsReadiness, httpsStatus, writeCaddyfile }
|
|
|
23
18
|
import { bold, cyan, dim, ok, warn, bad, paint, kv, tick, cross, dash, welcomeBanner, panel, row, hint } from './ui.js';
|
|
24
19
|
import { buildSteps, unansweredSteps, defaultOf, applyFlags, answersToSettings, summaryLines } from './wizard.js';
|
|
25
20
|
import { runSteps, WizardCancelled } from './prompt.js';
|
|
26
|
-
|
|
27
21
|
// ---------------------------------------------------------------- setup ---
|
|
28
|
-
|
|
29
22
|
export async function runSetup({ flags = {}, out = console.log, err = console.error, version = '' } = {}) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
const file = flags.config || configPath();
|
|
24
|
+
const existed = await configExists(file);
|
|
25
|
+
const current = existed ? await readSettings(file) : { ...DEFAULT_SETTINGS };
|
|
26
|
+
// Non-interactive (or a piped stdout): flags are the answers, defaults fill
|
|
27
|
+
// the rest, and the summary is printed instead of asked about.
|
|
28
|
+
if (flags.nonInteractive || !process.stdout.isTTY) {
|
|
29
|
+
const { settings } = applyFlags(current, flags);
|
|
30
|
+
await writeSettings(settings, file);
|
|
31
|
+
if (flags.json) {
|
|
32
|
+
out(JSON.stringify({ configFile: file, created: !existed, settings: publicSettings(settings) }, null, 2));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
out(tick + `Wrote ${file}`);
|
|
36
|
+
printSummary(out, settings, { revealKey: Boolean(patchHasNewKey(flags)) });
|
|
37
|
+
}
|
|
38
|
+
await printTunnelFollowup(out, settings);
|
|
39
|
+
// `start` may be the flag `--start` (true) or the mode a subcommand asked
|
|
40
|
+
// for ('background') — both mean "boot it now", differently.
|
|
41
|
+
if (flags.start === 'background')
|
|
42
|
+
return runStartBackground({ flags, out, err });
|
|
43
|
+
if (flags.start)
|
|
44
|
+
return runStart({ flags, out, err });
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
out(welcomeBanner(version, file));
|
|
48
|
+
if (existed)
|
|
49
|
+
out(dim(' A config already exists — answers start from what it says.\n'));
|
|
50
|
+
// Flag answers go in first; the wizard only asks what is left.
|
|
51
|
+
const seed = {};
|
|
52
|
+
for (const id of ['name', 'email', 'mode', 'host', 'port', 'domain', 'https', 'autoOpen']) {
|
|
53
|
+
if (flags[id] !== undefined)
|
|
54
|
+
seed[id] = flags[id];
|
|
55
|
+
}
|
|
56
|
+
if (flags.provider !== undefined)
|
|
57
|
+
seed.provider = flags.provider;
|
|
58
|
+
if (flags.baseUrl !== undefined)
|
|
59
|
+
seed.baseUrl = flags.baseUrl;
|
|
60
|
+
if (flags.model !== undefined)
|
|
61
|
+
seed.model = flags.model;
|
|
62
|
+
let answers;
|
|
63
|
+
try {
|
|
64
|
+
answers = await runSteps(unansweredSteps(buildSteps(current), seed), seed);
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
if (e instanceof WizardCancelled) {
|
|
68
|
+
err('\n Setup cancelled — nothing was written.');
|
|
69
|
+
return 130;
|
|
70
|
+
}
|
|
71
|
+
throw e;
|
|
72
|
+
}
|
|
73
|
+
let settings;
|
|
74
|
+
try {
|
|
75
|
+
settings = answersToSettings(current, answers);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
err(bad(' Those answers do not make a valid config: ' + e.message));
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
out('');
|
|
82
|
+
printSummary(out, settings, { revealKey: false });
|
|
83
|
+
out('');
|
|
84
|
+
const confirmed = await confirm(out, 'Write this config?');
|
|
85
|
+
if (!confirmed) {
|
|
86
|
+
err(' Not written.');
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
38
89
|
await writeSettings(settings, file);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
90
|
+
out(tick + 'Wrote ' + file);
|
|
91
|
+
// The generated key exists exactly once in a readable form: right now.
|
|
92
|
+
if (answers.keyChoice === 'generate' && settings.accessKey) {
|
|
93
|
+
out('');
|
|
94
|
+
out(warn(' Your access key — shown this once, store it somewhere safe:'));
|
|
95
|
+
out(' ' + bold(settings.accessKey));
|
|
44
96
|
}
|
|
45
97
|
await printTunnelFollowup(out, settings);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
98
|
+
if (flags.start === 'background' || (!flags.start && await confirm(out, 'Start Onboarder now?', true))) {
|
|
99
|
+
out('');
|
|
100
|
+
return flags.start === 'background' ? runStartBackground({ flags, out, err }) : runStart({ flags, out, err });
|
|
101
|
+
}
|
|
102
|
+
out(dim(settings.https
|
|
103
|
+
? ' Later: `onboarder start` (starts Caddy automatically), or `onboarder https status`'
|
|
104
|
+
: ' Later: `onboarder start`'));
|
|
50
105
|
return 0;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
out(welcomeBanner(version, file));
|
|
54
|
-
if (existed) out(dim(' A config already exists — answers start from what it says.\n'));
|
|
55
|
-
|
|
56
|
-
// Flag answers go in first; the wizard only asks what is left.
|
|
57
|
-
const seed = {};
|
|
58
|
-
for (const id of ['name', 'email', 'mode', 'host', 'port', 'domain', 'https', 'autoOpen']) {
|
|
59
|
-
if (flags[id] !== undefined) seed[id] = flags[id];
|
|
60
|
-
}
|
|
61
|
-
if (flags.provider !== undefined) seed.provider = flags.provider;
|
|
62
|
-
if (flags.baseUrl !== undefined) seed.baseUrl = flags.baseUrl;
|
|
63
|
-
if (flags.model !== undefined) seed.model = flags.model;
|
|
64
|
-
|
|
65
|
-
let answers;
|
|
66
|
-
try {
|
|
67
|
-
answers = await runSteps(unansweredSteps(buildSteps(current), seed), seed);
|
|
68
|
-
} catch (e) {
|
|
69
|
-
if (e instanceof WizardCancelled) {
|
|
70
|
-
err('\n Setup cancelled — nothing was written.');
|
|
71
|
-
return 130;
|
|
72
|
-
}
|
|
73
|
-
throw e;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
let settings;
|
|
77
|
-
try {
|
|
78
|
-
settings = answersToSettings(current, answers);
|
|
79
|
-
} catch (e) {
|
|
80
|
-
err(bad(' Those answers do not make a valid config: ' + e.message));
|
|
81
|
-
return 1;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
out('');
|
|
85
|
-
printSummary(out, settings, { revealKey: false });
|
|
86
|
-
out('');
|
|
87
|
-
const confirmed = await confirm(out, 'Write this config?');
|
|
88
|
-
if (!confirmed) {
|
|
89
|
-
err(' Not written.');
|
|
90
|
-
return 1;
|
|
91
|
-
}
|
|
92
|
-
await writeSettings(settings, file);
|
|
93
|
-
out(tick + 'Wrote ' + file);
|
|
94
|
-
|
|
95
|
-
// The generated key exists exactly once in a readable form: right now.
|
|
96
|
-
if (answers.keyChoice === 'generate' && settings.accessKey) {
|
|
97
|
-
out('');
|
|
98
|
-
out(warn(' Your access key — shown this once, store it somewhere safe:'));
|
|
99
|
-
out(' ' + bold(settings.accessKey));
|
|
100
|
-
}
|
|
101
|
-
await printTunnelFollowup(out, settings);
|
|
102
|
-
|
|
103
|
-
if (flags.start === 'background' || (!flags.start && await confirm(out, 'Start Onboarder now?', true))) {
|
|
104
|
-
out('');
|
|
105
|
-
return flags.start === 'background' ? runStartBackground({ flags, out, err }) : runStart({ flags, out, err });
|
|
106
|
-
}
|
|
107
|
-
out(dim(settings.https
|
|
108
|
-
? ' Later: `onboarder start` (starts Caddy automatically), or `onboarder https status`'
|
|
109
|
-
: ' Later: `onboarder start`'));
|
|
110
|
-
return 0;
|
|
111
106
|
}
|
|
112
|
-
|
|
113
107
|
function patchHasNewKey(flags) {
|
|
114
|
-
|
|
108
|
+
return flags.accessKey === 'generate' || (flags.accessKey === undefined && flags.mode === 'self-hosted');
|
|
115
109
|
}
|
|
116
|
-
|
|
117
110
|
function printSummary(out, settings, { revealKey = false } = {}) {
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
out(bold(' About to write:'));
|
|
112
|
+
for (const [label, value] of summaryLines(settings, { revealKey }))
|
|
113
|
+
out(kv(label, String(value)));
|
|
120
114
|
}
|
|
121
|
-
|
|
122
115
|
async function confirm(out, question, fallback = false) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
116
|
+
const { default: readline } = await import('node:readline/promises');
|
|
117
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
118
|
+
try {
|
|
119
|
+
const typed = await rl.question(` ${bold(question)} ${dim(fallback ? '(Y/n)' : '(y/N)')} `);
|
|
120
|
+
const v = typed.trim().toLowerCase();
|
|
121
|
+
return v ? v === 'y' || v === 'yes' : fallback;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
rl.close();
|
|
125
|
+
}
|
|
132
126
|
}
|
|
133
|
-
|
|
134
127
|
async function printTunnelFollowup(out, settings) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
128
|
+
const status = tunnelStatus(settings);
|
|
129
|
+
for (const name of ['cloudflare', 'tailscale']) {
|
|
130
|
+
const t = status[name];
|
|
131
|
+
if (!t.enabled)
|
|
132
|
+
continue;
|
|
133
|
+
out('');
|
|
134
|
+
if (t.installed) {
|
|
135
|
+
out(` ${name} — start the tunnel with:`);
|
|
136
|
+
out(' ' + cyan(t.command));
|
|
137
|
+
out(dim(` or just: onboarder tunnel ${name}`));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
out(warn(` ${name} is enabled but its CLI is not installed.`));
|
|
141
|
+
out(dim(' Install: ' + t.install));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
149
144
|
}
|
|
150
|
-
|
|
151
145
|
// ---------------------------------------------------------------- start ---
|
|
152
|
-
|
|
153
146
|
// What the person is about to be looking at, as a titled panel instead of a
|
|
154
147
|
// loose run of lines. One function serves the foreground banner and the
|
|
155
148
|
// background confirmation, so "what does `onboarder start` tell me" has exactly
|
|
156
149
|
// one answer no matter which mode you used.
|
|
157
150
|
export function serverDetails(started, { configFile, pid = null, mode = 'foreground', logFile = '' } = {}) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
151
|
+
const { settings, host, port } = started;
|
|
152
|
+
const urls = serverUrls(settings);
|
|
153
|
+
const rows = [
|
|
154
|
+
row('URL', bold(cyan(urls.local))),
|
|
155
|
+
row('Bind', `${host}:${port}`),
|
|
156
|
+
row('Mode', settings.mode === 'self-hosted'
|
|
157
|
+
? (isLoopbackHost(host) ? 'self-hosted (loopback — use a tunnel for remote access)' : 'self-hosted (network reachable, access key required)')
|
|
158
|
+
: 'local (this machine only)'),
|
|
159
|
+
];
|
|
160
|
+
if (urls.network)
|
|
161
|
+
rows.push(row('Network', urls.network));
|
|
162
|
+
if (urls.domain)
|
|
163
|
+
rows.push(row('Public', urls.domain));
|
|
164
|
+
if (settings.mode === 'self-hosted') {
|
|
165
|
+
rows.push(row('Access key', settings.accessKey ? maskAccessKey(settings.accessKey) : bad('NOT SET — every API call is refused')));
|
|
166
|
+
}
|
|
167
|
+
rows.push(row('PID', String(pid ?? process.pid)));
|
|
168
|
+
rows.push(row('Config', dim(configFile || '')));
|
|
169
|
+
if (logFile)
|
|
170
|
+
rows.push(row('Logs', dim(logFile)));
|
|
171
|
+
return { rows, urls, mode };
|
|
176
172
|
}
|
|
177
|
-
|
|
178
173
|
function printServerDetails(out, started, options) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
174
|
+
const { rows } = serverDetails(started, options);
|
|
175
|
+
out('');
|
|
176
|
+
out(panel('Onboarder is running', rows));
|
|
177
|
+
out('');
|
|
183
178
|
}
|
|
184
|
-
|
|
185
179
|
// Keep a foreground panel at the terminal's current width.
|
|
186
180
|
//
|
|
187
181
|
// Resizing a window after the server started used to leave a panel whose right
|
|
@@ -194,765 +188,796 @@ function printServerDetails(out, started, options) {
|
|
|
194
188
|
// A non-TTY (a pipe, a log file) never redraws: there is no cursor to move and
|
|
195
189
|
// re-printing would just duplicate the block.
|
|
196
190
|
export function watchResize(render, stream = process.stdout) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
191
|
+
if (!stream.isTTY || typeof process.stdout.on !== 'function')
|
|
192
|
+
return (..._args) => { };
|
|
193
|
+
let previous = '';
|
|
194
|
+
const onResize = () => {
|
|
195
|
+
const next = render();
|
|
196
|
+
if (next === previous)
|
|
197
|
+
return;
|
|
198
|
+
const lines = previous ? previous.split('\n').length : 0;
|
|
199
|
+
// Up over the old block, clear it, print the new one. `\x1b[J` clears from
|
|
200
|
+
// the cursor to the end of the screen, which is exactly the old block.
|
|
201
|
+
stream.write(lines ? `\x1b[${lines}A\x1b[J` : '');
|
|
202
|
+
stream.write(next);
|
|
203
|
+
previous = next;
|
|
204
|
+
};
|
|
205
|
+
process.stdout.on('SIGWINCH', onResize);
|
|
206
|
+
return () => process.stdout.removeListener('SIGWINCH', onResize);
|
|
211
207
|
}
|
|
212
|
-
|
|
213
208
|
export async function runStart({ flags = {}, out = console.log, err = console.error } = {}) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
209
|
+
const file = flags.config || configPath();
|
|
210
|
+
if (!await configExists(file)) {
|
|
211
|
+
// No config yet: on a terminal the kind thing is to run setup first; in a
|
|
212
|
+
// script the predictable thing is to boot the defaults (which are the
|
|
213
|
+
// pre-settings behavior) and say so.
|
|
214
|
+
if (process.stdout.isTTY && !flags.nonInteractive) {
|
|
215
|
+
out(dim(' No settings yet — running setup first.'));
|
|
216
|
+
return runSetup({ flags: { ...flags, start: true }, out, err });
|
|
217
|
+
}
|
|
218
|
+
out(dim(` No config at ${file} — starting with defaults (local mode).`));
|
|
219
|
+
}
|
|
220
|
+
const recorded = readPidFile(file);
|
|
221
|
+
if (recorded && pidIsAlive(recorded)) {
|
|
222
|
+
err(` Onboarder is already running (PID ${recorded}).`);
|
|
223
|
+
err(dim(' Use `onboarder status`, `onboarder stop`, or `onboarder restart`.'));
|
|
224
|
+
return 1;
|
|
225
|
+
}
|
|
226
|
+
if (recorded)
|
|
227
|
+
removePidFile(file, recorded);
|
|
228
|
+
// `quiet`: the CLI prints its own details panel below, so the loose banner
|
|
229
|
+
// would be the same facts twice. `node server/index.js` still gets the banner.
|
|
230
|
+
const started = await startServer({ configFile: file, log: (..._args) => { }, openBrowser: false });
|
|
231
|
+
if (started.settings.https) {
|
|
232
|
+
out('');
|
|
233
|
+
const httpsCode = await runHttps('setup', { flags, out: flags.json ? (..._args) => { } : out, err });
|
|
234
|
+
if (httpsCode !== 0) {
|
|
235
|
+
await new Promise((resolve) => started.server.close(resolve));
|
|
236
|
+
return httpsCode;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (flags.json)
|
|
240
|
+
out(JSON.stringify({ host: started.host, port: started.port, url: serverUrls(started.settings).local }));
|
|
241
|
+
// `start` foreground prints the details itself rather than letting
|
|
242
|
+
// `startServer` print the old loose banner — the panel replaces it. The
|
|
243
|
+
// background child lands here too (that is the point of reusing this path),
|
|
244
|
+
// and ONBOARDER_BACKGROUND is what tells the two apart: one is attached to a
|
|
245
|
+
// terminal you are about to close, the other is already detached from it.
|
|
246
|
+
printServerDetails(out, started, { configFile: file, mode: 'foreground' });
|
|
247
|
+
// Redraw the panel when the terminal is resized, so the border stays on screen
|
|
248
|
+
// and long paths re-elide to the new width instead of hanging off the edge.
|
|
249
|
+
watchResize(() => '\n' + panel('Onboarder is running', serverDetails(started, { configFile: file }).rows) + '\n');
|
|
250
|
+
if (process.env.ONBOARDER_LAUNCH) {
|
|
251
|
+
// Started by launchd/systemd/the Startup folder: these lines are going into
|
|
252
|
+
// a log file nobody is watching, so they say what the supervisor is doing
|
|
253
|
+
// rather than telling a person to press Ctrl-C.
|
|
254
|
+
out(dim(` Launched at login by ${process.env.ONBOARDER_LAUNCH}. This process is supervised — stop it with \`onboarder stop\`.`));
|
|
255
|
+
}
|
|
256
|
+
else if (process.env.ONBOARDER_BACKGROUND) {
|
|
257
|
+
out(dim(' Started in the background — this process is now independent of any terminal.'));
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
out(dim(' Running in the foreground. Ctrl-C stops it; closing this terminal stops it too.'));
|
|
261
|
+
out(dim(' To keep it alive after you close the terminal: ') + cyan('onboarder start background'));
|
|
262
|
+
}
|
|
237
263
|
out('');
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return httpsCode;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
if (flags.json) out(JSON.stringify({ host: started.host, port: started.port, url: serverUrls(started.settings).local }));
|
|
245
|
-
// `start` foreground prints the details itself rather than letting
|
|
246
|
-
// `startServer` print the old loose banner — the panel replaces it. The
|
|
247
|
-
// background child lands here too (that is the point of reusing this path),
|
|
248
|
-
// and ONBOARDER_BACKGROUND is what tells the two apart: one is attached to a
|
|
249
|
-
// terminal you are about to close, the other is already detached from it.
|
|
250
|
-
printServerDetails(out, started, { configFile: file, mode: 'foreground' });
|
|
251
|
-
// Redraw the panel when the terminal is resized, so the border stays on screen
|
|
252
|
-
// and long paths re-elide to the new width instead of hanging off the edge.
|
|
253
|
-
watchResize(() => '\n' + panel('Onboarder is running', serverDetails(started, { configFile: file }).rows) + '\n');
|
|
254
|
-
if (process.env.ONBOARDER_LAUNCH) {
|
|
255
|
-
// Started by launchd/systemd/the Startup folder: these lines are going into
|
|
256
|
-
// a log file nobody is watching, so they say what the supervisor is doing
|
|
257
|
-
// rather than telling a person to press Ctrl-C.
|
|
258
|
-
out(dim(` Launched at login by ${process.env.ONBOARDER_LAUNCH}. This process is supervised — stop it with \`onboarder stop\`.`));
|
|
259
|
-
} else if (process.env.ONBOARDER_BACKGROUND) {
|
|
260
|
-
out(dim(' Started in the background — this process is now independent of any terminal.'));
|
|
261
|
-
} else {
|
|
262
|
-
out(dim(' Running in the foreground. Ctrl-C stops it; closing this terminal stops it too.'));
|
|
263
|
-
out(dim(' To keep it alive after you close the terminal: ') + cyan('onboarder start background'));
|
|
264
|
-
}
|
|
265
|
-
out('');
|
|
266
|
-
// The listening server holds the event loop; resolve so callers/tests know
|
|
267
|
-
// we are up, but leave the process running.
|
|
268
|
-
return { ...started, code: 0 };
|
|
264
|
+
// The listening server holds the event loop; resolve so callers/tests know
|
|
265
|
+
// we are up, but leave the process running.
|
|
266
|
+
return { ...started, code: 0 };
|
|
269
267
|
}
|
|
270
|
-
|
|
271
268
|
// `onboarder start background` — same server, no terminal attached. The child is
|
|
272
269
|
// an ordinary foreground `start`; all this does is detach it and then *wait for
|
|
273
270
|
// it to answer* before reporting success, so a bind failure surfaces here as a
|
|
274
271
|
// failure with the log tail attached rather than a cheerful lie.
|
|
275
272
|
export async function runStartBackground({ flags = {}, out = console.log, err = console.error } = {}) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
273
|
+
const file = flags.config || configPath();
|
|
274
|
+
if (!await configExists(file) && process.stdout.isTTY && !flags.nonInteractive) {
|
|
275
|
+
out(dim(' No settings yet — running setup first.'));
|
|
276
|
+
return runSetup({ flags: { ...flags, start: 'background' }, out, err });
|
|
277
|
+
}
|
|
278
|
+
const recorded = readPidFile(file);
|
|
279
|
+
if (recorded && pidIsAlive(recorded)) {
|
|
280
|
+
err(` Onboarder is already running (PID ${recorded}).`);
|
|
281
|
+
err(dim(' Use `onboarder status`, `onboarder stop`, or `onboarder restart`.'));
|
|
282
|
+
return 1;
|
|
283
|
+
}
|
|
284
|
+
if (recorded)
|
|
285
|
+
removePidFile(file, recorded);
|
|
286
|
+
const settings = await readSettings(file);
|
|
287
|
+
const log = logPath(file);
|
|
288
|
+
await rotateLogIfNeeded(log);
|
|
289
|
+
spawnDetached({ configFile: file, log });
|
|
290
|
+
const pid = await waitForPidFile(file, { timeoutMs: flags.timeout ? Number(flags.timeout) * 1000 : 20000 });
|
|
291
|
+
const urls = serverUrls(settings);
|
|
292
|
+
const ready = pid ? await waitForReady(new URL('/api/health', urls.local).toString(), { timeoutMs: 8000 }) : false;
|
|
293
|
+
if (!ready) {
|
|
294
|
+
// Two different failures with two different fixes, so they are reported
|
|
295
|
+
// differently. No pid record means the child never finished binding — the
|
|
296
|
+
// log holds the reason (a busy port, an invalid config). A live pid that will
|
|
297
|
+
// not answer means it bound and then something in front of it is in the way
|
|
298
|
+
// (a proxy, a firewall), and no amount of log-reading will show that.
|
|
299
|
+
if (!pid) {
|
|
300
|
+
err(bad(' Onboarder did not start in the background.'));
|
|
301
|
+
const tail = await tailLog(log, 15);
|
|
302
|
+
if (tail.length) {
|
|
303
|
+
err(dim(` Last lines of ${log}:`));
|
|
304
|
+
for (const line of tail)
|
|
305
|
+
err(' ' + line);
|
|
306
|
+
}
|
|
307
|
+
err(dim(' Fix the cause above, then run `onboarder start background` again.'));
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
err(bad(` Onboarder is running (PID ${pid}) but ${urls.local} is not answering.`));
|
|
311
|
+
err(dim(` Its log is ${log}. If a proxy or firewall fronts this port, check that first.`));
|
|
312
|
+
err(dim(' Otherwise: `onboarder stop`, then `onboarder start background` again.'));
|
|
313
|
+
}
|
|
314
|
+
return 1;
|
|
315
|
+
}
|
|
316
|
+
if (flags.json) {
|
|
317
|
+
out(JSON.stringify({ background: true, pid, url: urls.local, log, configFile: file }, null, 2));
|
|
318
|
+
return 0;
|
|
319
|
+
}
|
|
320
|
+
out('');
|
|
321
|
+
out(panel('Onboarder is running in the background', [
|
|
322
|
+
row('URL', bold(cyan(urls.local))),
|
|
323
|
+
row('PID', String(pid)),
|
|
324
|
+
row('Mode', 'background — survives closing this terminal'),
|
|
325
|
+
row('Logs', dim(log)),
|
|
326
|
+
row('Config', dim(file)),
|
|
327
|
+
]));
|
|
328
|
+
out('');
|
|
329
|
+
out(dim(' Follow the log: ') + cyan('onboarder logs -f'));
|
|
330
|
+
out(dim(' Stop it: ') + cyan('onboarder stop'));
|
|
331
|
+
out(dim(' Check on it: ') + cyan('onboarder status'));
|
|
332
|
+
out('');
|
|
322
333
|
return 0;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
out('');
|
|
326
|
-
out(panel('Onboarder is running in the background', [
|
|
327
|
-
row('URL', bold(cyan(urls.local))),
|
|
328
|
-
row('PID', String(pid)),
|
|
329
|
-
row('Mode', 'background — survives closing this terminal'),
|
|
330
|
-
row('Logs', dim(log)),
|
|
331
|
-
row('Config', dim(file)),
|
|
332
|
-
]));
|
|
333
|
-
out('');
|
|
334
|
-
out(dim(' Follow the log: ') + cyan('onboarder logs -f'));
|
|
335
|
-
out(dim(' Stop it: ') + cyan('onboarder stop'));
|
|
336
|
-
out(dim(' Check on it: ') + cyan('onboarder status'));
|
|
337
|
-
out('');
|
|
338
|
-
return 0;
|
|
339
334
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
335
|
// ------------------------------------------------------------- lifecycle ---
|
|
343
|
-
|
|
344
336
|
// Panel rows carry their own inline marker rather than the `tick`/`cross`/`dash`
|
|
345
337
|
// constants: those bake in a two-column indent for standalone lines, which would
|
|
346
338
|
// push the first row out of the panel's label column.
|
|
347
339
|
const MARK = { yes: (s) => paint('✓ ', 'green') + s, no: (s) => paint('– ', 'gray') + s, warn: (s) => paint('! ', 'yellow') + s };
|
|
348
|
-
|
|
349
340
|
// How a live instance presents itself, in one sentence per mode. The point of
|
|
350
341
|
// the sentence is the thing someone actually needs to know: can I close my
|
|
351
342
|
// terminal, or will that kill it?
|
|
352
343
|
const MODE_NOTES = {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
344
|
+
background: 'background — survives closing the terminal',
|
|
345
|
+
startup: 'started at login — the OS restarts it if it stops',
|
|
346
|
+
foreground: 'foreground — stops when you press Ctrl-C or close the terminal',
|
|
356
347
|
};
|
|
357
|
-
|
|
358
348
|
export async function runStatus({ flags = {}, out = console.log } = {}) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
349
|
+
const file = flags.config || configPath();
|
|
350
|
+
const pid = readPidFile(file);
|
|
351
|
+
let running = pid && pidIsAlive(pid);
|
|
352
|
+
let settings = null;
|
|
353
|
+
try {
|
|
354
|
+
settings = await readSettings(file);
|
|
355
|
+
}
|
|
356
|
+
catch { /* defaults are still meaningful */ }
|
|
357
|
+
const portBusy = settings ? !(await portIsFree(settings.host, settings.port)) : false;
|
|
358
|
+
if (pid && !running)
|
|
359
|
+
removePidFile(file, pid);
|
|
360
|
+
// The pid says whether it is up; the run record says *how* it came up, which
|
|
361
|
+
// is what tells someone whether closing their terminal will kill it.
|
|
362
|
+
const info = running ? readRunInfo(file) : null;
|
|
363
|
+
const result = {
|
|
364
|
+
running: Boolean(running || portBusy),
|
|
365
|
+
pid: running ? pid : null,
|
|
366
|
+
port: settings ? `${settings.host}:${settings.port}` : null,
|
|
367
|
+
portBusy,
|
|
368
|
+
managed: Boolean(running),
|
|
369
|
+
configFile: file,
|
|
370
|
+
mode: running ? (info?.mode || 'foreground') : null,
|
|
371
|
+
url: running ? (info?.url || (settings ? serverUrls(settings).local : null)) : null,
|
|
372
|
+
uptimeMs: running && info?.startedAt ? Date.now() - Date.parse(info.startedAt) : null,
|
|
373
|
+
log: running && info?.log ? info.log : null,
|
|
374
|
+
startup: (await startupStatus()).installed,
|
|
375
|
+
};
|
|
376
|
+
if (flags.json) {
|
|
377
|
+
out(JSON.stringify(result, null, 2));
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
out('');
|
|
381
|
+
out(panel('Onboarder status', [
|
|
382
|
+
row('State', running
|
|
383
|
+
? MARK.yes(`Running (PID ${pid})`)
|
|
384
|
+
: portBusy
|
|
385
|
+
? MARK.warn(`Port busy — ${result.port}, but no Onboarder process record`)
|
|
386
|
+
: MARK.no('Stopped')),
|
|
387
|
+
...(running ? [
|
|
388
|
+
row('URL', cyan(result.url || '')),
|
|
389
|
+
row('Mode', MODE_NOTES[result.mode] || MODE_NOTES.foreground),
|
|
390
|
+
...(result.uptimeMs ? [row('Uptime', humanDuration(result.uptimeMs))] : []),
|
|
391
|
+
...(result.log ? [row('Logs', dim(result.log))] : []),
|
|
392
|
+
] : []),
|
|
393
|
+
row('Config', dim(file)),
|
|
394
|
+
row('At login', result.startup ? MARK.yes('Enabled') : MARK.no('Not enabled — `onboarder start startup install`')),
|
|
395
|
+
...(result.portBusy && !running
|
|
396
|
+
? [hint('Inspect the listener with `ss -ltnp` or `lsof -i :' + settings.port + '` before stopping another process.')]
|
|
397
|
+
: []),
|
|
398
|
+
]));
|
|
399
|
+
out('');
|
|
400
|
+
}
|
|
401
|
+
return 0;
|
|
407
402
|
}
|
|
408
|
-
|
|
409
403
|
// "3d 4h", "12m 5s" — coarse on purpose. Precision past the second is noise on
|
|
410
404
|
// something a person glances at.
|
|
411
405
|
export function humanDuration(ms) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
406
|
+
const total = Math.max(0, Math.floor(ms / 1000));
|
|
407
|
+
const days = Math.floor(total / 86400);
|
|
408
|
+
const hours = Math.floor((total % 86400) / 3600);
|
|
409
|
+
const minutes = Math.floor((total % 3600) / 60);
|
|
410
|
+
const seconds = total % 60;
|
|
411
|
+
if (days)
|
|
412
|
+
return `${days}d ${hours}h`;
|
|
413
|
+
if (hours)
|
|
414
|
+
return `${hours}h ${minutes}m`;
|
|
415
|
+
if (minutes)
|
|
416
|
+
return `${minutes}m ${seconds}s`;
|
|
417
|
+
return `${seconds}s`;
|
|
421
418
|
}
|
|
422
|
-
|
|
423
419
|
function waitForExit(pid, timeoutMs = 5000) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
420
|
+
return new Promise((resolve) => {
|
|
421
|
+
const started = Date.now();
|
|
422
|
+
const poll = () => {
|
|
423
|
+
if (!pidIsAlive(pid))
|
|
424
|
+
return resolve(true);
|
|
425
|
+
if (Date.now() - started >= timeoutMs)
|
|
426
|
+
return resolve(false);
|
|
427
|
+
setTimeout(poll, 100);
|
|
428
|
+
};
|
|
429
|
+
poll();
|
|
430
|
+
});
|
|
433
431
|
}
|
|
434
|
-
|
|
435
432
|
export async function runStop({ flags = {}, out = console.log, err = console.error } = {}) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
433
|
+
const file = flags.config || configPath();
|
|
434
|
+
const pid = readPidFile(file);
|
|
435
|
+
if (!pid) {
|
|
436
|
+
const settings = await readSettings(file);
|
|
437
|
+
const portBusy = !(await portIsFree(settings.host, settings.port));
|
|
438
|
+
if (flags.json)
|
|
439
|
+
out(JSON.stringify({ stopped: false, reason: portBusy ? 'unmanaged busy port' : 'not running', port: `${settings.host}:${settings.port}` }, null, 2));
|
|
440
|
+
else if (portBusy) {
|
|
441
|
+
out(`${warn('!')}Port busy ${settings.host}:${settings.port} — no Onboarder process record.`);
|
|
442
|
+
out(dim(' Inspect it with `ss -ltnp` or `lsof -i :' + settings.port + '`; `onboarder stop` will not kill an unmanaged process.'));
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
out(`${dash}Not running.`);
|
|
446
|
+
}
|
|
447
|
+
return portBusy ? 1 : 0;
|
|
448
|
+
}
|
|
449
|
+
if (!pidIsAlive(pid)) {
|
|
450
|
+
removePidFile(file, pid);
|
|
451
|
+
if (flags.json)
|
|
452
|
+
out(JSON.stringify({ stopped: true, pid, stale: true }, null, 2));
|
|
453
|
+
else
|
|
454
|
+
out(tick + `Removed stale process record for PID ${pid}.`);
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
process.kill(pid, 'SIGTERM');
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
if (error?.code !== 'ESRCH')
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
const stopped = await waitForExit(pid);
|
|
465
|
+
if (!stopped && pidIsAlive(pid)) {
|
|
466
|
+
err(` PID ${pid} did not stop after SIGTERM.`);
|
|
467
|
+
err(dim(' Check it with `ps -p ' + pid + ' -f`, then stop it only if it really is Onboarder.'));
|
|
468
|
+
return 1;
|
|
469
|
+
}
|
|
451
470
|
removePidFile(file, pid);
|
|
452
|
-
if (flags.json)
|
|
453
|
-
|
|
471
|
+
if (flags.json)
|
|
472
|
+
out(JSON.stringify({ stopped: true, pid }, null, 2));
|
|
473
|
+
else
|
|
474
|
+
out(tick + `Stopped Onboarder (PID ${pid}).`);
|
|
454
475
|
return 0;
|
|
455
|
-
}
|
|
456
|
-
try {
|
|
457
|
-
process.kill(pid, 'SIGTERM');
|
|
458
|
-
} catch (error) {
|
|
459
|
-
if (error?.code !== 'ESRCH') throw error;
|
|
460
|
-
}
|
|
461
|
-
const stopped = await waitForExit(pid);
|
|
462
|
-
if (!stopped && pidIsAlive(pid)) {
|
|
463
|
-
err(` PID ${pid} did not stop after SIGTERM.`);
|
|
464
|
-
err(dim(' Check it with `ps -p ' + pid + ' -f`, then stop it only if it really is Onboarder.'));
|
|
465
|
-
return 1;
|
|
466
|
-
}
|
|
467
|
-
removePidFile(file, pid);
|
|
468
|
-
if (flags.json) out(JSON.stringify({ stopped: true, pid }, null, 2));
|
|
469
|
-
else out(tick + `Stopped Onboarder (PID ${pid}).`);
|
|
470
|
-
return 0;
|
|
471
476
|
}
|
|
472
|
-
|
|
473
477
|
export async function runRestart(options = {}) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
478
|
+
const file = options.flags?.config || configPath();
|
|
479
|
+
const pid = readPidFile(file);
|
|
480
|
+
if (pid && pidIsAlive(pid)) {
|
|
481
|
+
const stopped = await runStop(options);
|
|
482
|
+
if (stopped !== 0)
|
|
483
|
+
return stopped;
|
|
484
|
+
}
|
|
485
|
+
else if (pid) {
|
|
486
|
+
removePidFile(file, pid);
|
|
487
|
+
}
|
|
488
|
+
return runStart(options);
|
|
483
489
|
}
|
|
484
|
-
|
|
485
490
|
// ----------------------------------------------------------------- logs ---
|
|
486
|
-
|
|
487
491
|
// `onboarder logs` — the answer to "it is running in the background, what is it
|
|
488
492
|
// doing?". Prints the tail of the same file the background child writes, and
|
|
489
493
|
// `-f` follows it. Line-oriented and level-colored, so a wall of request logs
|
|
490
494
|
// stays scannable instead of being one undifferentiated block.
|
|
491
495
|
export async function runLogs({ flags = {}, out = console.log, err = console.error } = {}) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
496
|
+
const file = flags.config || configPath();
|
|
497
|
+
const log = logPath(file);
|
|
498
|
+
const lines = flags.lines === undefined ? 40 : Math.max(1, Number(flags.lines) || 40);
|
|
499
|
+
if (!await logExists(log)) {
|
|
500
|
+
if (flags.json)
|
|
501
|
+
out(JSON.stringify({ log, exists: false, lines: [] }, null, 2));
|
|
502
|
+
else {
|
|
503
|
+
err(` No log file yet at ${log}.`);
|
|
504
|
+
err(dim(' A foreground `onboarder start` prints to the terminal; only `onboarder start background` writes this file.'));
|
|
505
|
+
}
|
|
506
|
+
return flags.json ? 0 : 1;
|
|
507
|
+
}
|
|
508
|
+
if (!flags.follow) {
|
|
509
|
+
const tail = await tailLog(log, lines);
|
|
510
|
+
if (flags.json) {
|
|
511
|
+
out(JSON.stringify({ log, exists: true, lines: tail }, null, 2));
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
out('');
|
|
515
|
+
out(bold(` ${log}`) + dim(` (last ${tail.length} of ${lines})`));
|
|
516
|
+
out('');
|
|
517
|
+
for (const line of tail)
|
|
518
|
+
out(' ' + line);
|
|
519
|
+
out('');
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
if (flags.json) {
|
|
523
|
+
err(' --follow and --json do not combine; pick one.');
|
|
524
|
+
return 2;
|
|
525
|
+
}
|
|
508
526
|
out('');
|
|
509
|
-
|
|
527
|
+
out(bold(` Following ${log}`) + dim(' (Ctrl-C to stop)'));
|
|
510
528
|
out('');
|
|
529
|
+
const stop = followLog(log, (line) => out(' ' + line), { from: 'start' });
|
|
530
|
+
const finish = () => { stop(); process.exit(0); };
|
|
531
|
+
process.once('SIGINT', finish);
|
|
532
|
+
await new Promise((resolve) => process.once('exit', resolve));
|
|
511
533
|
return 0;
|
|
512
|
-
}
|
|
513
|
-
if (flags.json) { err(' --follow and --json do not combine; pick one.'); return 2; }
|
|
514
|
-
out('');
|
|
515
|
-
out(bold(` Following ${log}`) + dim(' (Ctrl-C to stop)'));
|
|
516
|
-
out('');
|
|
517
|
-
const stop = followLog(log, (line) => out(' ' + line), { from: 'start' });
|
|
518
|
-
const finish = () => { stop(); process.exit(0); };
|
|
519
|
-
process.once('SIGINT', finish);
|
|
520
|
-
await new Promise((resolve) => process.once('exit', resolve));
|
|
521
|
-
return 0;
|
|
522
534
|
}
|
|
523
|
-
|
|
524
535
|
// -------------------------------------------------------------- startup ---
|
|
525
|
-
|
|
526
536
|
// `onboarder start startup` — run at login, not just right now. Three verbs
|
|
527
537
|
// because the three actions are genuinely different: `install` writes the
|
|
528
538
|
// artifact and hands it to the OS, `remove` takes it back out, `status` only
|
|
529
539
|
// looks. `install` also starts it once so the person is not left waiting for the
|
|
530
540
|
// next reboot to find out whether it worked.
|
|
531
541
|
export async function runStartup(action = 'status', { flags = {}, out = console.log, err = console.error } = {}) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
542
|
+
const file = flags.config || configPath();
|
|
543
|
+
const target = startupTarget();
|
|
544
|
+
const log = logPath(file);
|
|
545
|
+
if (action === 'install' || action === 'enable' || action === 'on') {
|
|
546
|
+
const result = await installStartup({ configFile: file, log, target });
|
|
547
|
+
if (!result.ok) {
|
|
548
|
+
err(bad(' Could not install the startup entry.'));
|
|
549
|
+
err(' ' + (result.reason || result.output || result.error || 'unknown error'));
|
|
550
|
+
return 1;
|
|
551
|
+
}
|
|
552
|
+
out(tick + `Onboarder will start at login (${target.hint}).`);
|
|
553
|
+
out(kv('Startup file', result.path));
|
|
554
|
+
if (result.output)
|
|
555
|
+
out(dim(' ' + result.output.split('\n').slice(-2).join('\n ')));
|
|
556
|
+
out('');
|
|
557
|
+
out(dim(' Start it right now too: ') + cyan('onboarder start background'));
|
|
558
|
+
out(dim(' Turn it off again: ') + cyan('onboarder start startup remove'));
|
|
559
|
+
out('');
|
|
560
|
+
return 0;
|
|
561
|
+
}
|
|
562
|
+
if (action === 'remove' || action === 'disable' || action === 'off' || action === 'uninstall') {
|
|
563
|
+
const status = await startupStatus(target);
|
|
564
|
+
if (!status.installed) {
|
|
565
|
+
out(dash + 'No startup entry is installed — nothing to remove.');
|
|
566
|
+
return 0;
|
|
567
|
+
}
|
|
568
|
+
const result = await removeStartup({ target });
|
|
569
|
+
if (!result.ok) {
|
|
570
|
+
err(bad(' Could not remove the startup entry: ' + (result.reason || 'unknown error')));
|
|
571
|
+
return 1;
|
|
572
|
+
}
|
|
573
|
+
out(tick + 'Startup entry removed. A running server is untouched — use `onboarder stop` for that.');
|
|
574
|
+
out(kv('Removed', result.path));
|
|
575
|
+
out('');
|
|
576
|
+
return 0;
|
|
577
|
+
}
|
|
578
|
+
if (action === 'status' || action === undefined) {
|
|
579
|
+
const status = await startupStatus(target);
|
|
580
|
+
if (flags.json) {
|
|
581
|
+
out(JSON.stringify(status, null, 2));
|
|
582
|
+
return status.installed ? 0 : 1;
|
|
583
|
+
}
|
|
584
|
+
out('');
|
|
585
|
+
out(panel('Onboarder at login', [
|
|
586
|
+
row('State', status.installed
|
|
587
|
+
? (status.active ? MARK.yes('Enabled and running') : MARK.warn('Enabled, not running'))
|
|
588
|
+
: MARK.no('Not enabled')),
|
|
589
|
+
row('How', status.detail),
|
|
590
|
+
...(status.path ? [row('File', dim(status.path))] : []),
|
|
591
|
+
...(status.supported ? [hint(status.installed
|
|
592
|
+
? 'Remove it with `onboarder start startup remove`.'
|
|
593
|
+
: 'Enable it with `onboarder start startup install`.')] : []),
|
|
594
|
+
]));
|
|
595
|
+
out('');
|
|
596
|
+
return status.installed ? 0 : 1;
|
|
597
|
+
}
|
|
598
|
+
throw new Error('Usage: onboarder start startup [install|remove|status]');
|
|
586
599
|
}
|
|
587
|
-
|
|
588
600
|
// --------------------------------------------------------------- config ---
|
|
589
|
-
|
|
590
601
|
// Keys `config set` may touch — the same allow-list the HTTP API enforces, so
|
|
591
602
|
// neither surface can smuggle in a key the schema does not know.
|
|
592
603
|
const SETTABLE = {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
604
|
+
mode: (v) => { if (!['local', 'self-hosted'].includes(v))
|
|
605
|
+
throw new Error('mode must be "local" or "self-hosted".'); return v; },
|
|
606
|
+
host: (v) => v,
|
|
607
|
+
port: (v) => { const n = Number(v); if (!Number.isInteger(n) || n < 1 || n > 65535)
|
|
608
|
+
throw new Error('port must be 1-65535.'); return n; },
|
|
609
|
+
domain: (v) => v,
|
|
610
|
+
https: (v) => ['true', 'yes', '1', 'on'].includes(String(v).toLowerCase()),
|
|
611
|
+
autoOpen: (v) => ['true', 'yes', '1', 'on'].includes(String(v).toLowerCase()),
|
|
612
|
+
'account.name': (v) => v,
|
|
613
|
+
'account.email': (v) => v,
|
|
614
|
+
'account.provider': (v) => v,
|
|
615
|
+
'account.baseUrl': (v) => v,
|
|
616
|
+
'account.model': (v) => v,
|
|
617
|
+
'tunnel.cloudflare': (v) => ['true', 'yes', '1', 'on'].includes(String(v).toLowerCase()),
|
|
618
|
+
'tunnel.tailscale': (v) => ['true', 'yes', '1', 'on'].includes(String(v).toLowerCase()),
|
|
606
619
|
};
|
|
607
|
-
|
|
608
620
|
function dig(obj, dotted) {
|
|
609
|
-
|
|
621
|
+
return dotted.split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);
|
|
610
622
|
}
|
|
611
|
-
|
|
612
623
|
function bury(obj, dotted, value) {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
624
|
+
const keys = dotted.split('.');
|
|
625
|
+
let o = obj;
|
|
626
|
+
for (const k of keys.slice(0, -1))
|
|
627
|
+
o = o[k] = o[k] && typeof o[k] === 'object' ? o[k] : {};
|
|
628
|
+
o[keys.at(-1)] = value;
|
|
617
629
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
630
|
export async function runConfig(sub, args, { flags = {}, out = console.log } = {}) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
631
|
+
const file = flags.config || configPath();
|
|
632
|
+
switch (sub) {
|
|
633
|
+
case 'path':
|
|
634
|
+
out(file);
|
|
635
|
+
return 0;
|
|
636
|
+
case 'show': {
|
|
637
|
+
const settings = await readSettings(file);
|
|
638
|
+
if (flags.json) {
|
|
639
|
+
const pub = publicSettings(settings);
|
|
640
|
+
if (flags.reveal)
|
|
641
|
+
pub.accessKey = settings.accessKey || null;
|
|
642
|
+
out(JSON.stringify({ configFile: file, ...pub }, null, 2));
|
|
643
|
+
return 0;
|
|
644
|
+
}
|
|
645
|
+
const urls = serverUrls(settings);
|
|
646
|
+
out(bold(' ' + file));
|
|
647
|
+
out(kv('Mode', settings.mode));
|
|
648
|
+
out(kv('Bind', `${settings.host}:${settings.port}`));
|
|
649
|
+
if (settings.domain)
|
|
650
|
+
out(kv('Domain', settings.domain));
|
|
651
|
+
out(kv('HTTPS', settings.https ? `enabled — ${urls.domain}` : settings.domain ? 'disabled (plain HTTP)' : 'no domain configured'));
|
|
652
|
+
out(kv('Local', urls.local));
|
|
653
|
+
if (urls.network)
|
|
654
|
+
out(kv('Network', urls.network));
|
|
655
|
+
if (urls.domain)
|
|
656
|
+
out(kv('Public', urls.domain));
|
|
657
|
+
if (settings.mode === 'self-hosted') {
|
|
658
|
+
out(kv('Access key', flags.reveal ? (settings.accessKey || '(none)') : maskAccessKey(settings.accessKey)));
|
|
659
|
+
if (!flags.reveal)
|
|
660
|
+
out(dim(' `onboarder config show --reveal` prints it — you are on this machine, after all.'));
|
|
661
|
+
}
|
|
662
|
+
const who = [settings.account.name, settings.account.email].filter(Boolean).join(' · ');
|
|
663
|
+
if (who)
|
|
664
|
+
out(kv('Profile', who));
|
|
665
|
+
if (settings.account.baseUrl) {
|
|
666
|
+
out(kv('AI', `${settings.account.provider} — ${settings.account.baseUrl}${settings.account.model ? ' — ' + settings.account.model : ''}`));
|
|
667
|
+
}
|
|
668
|
+
const status = tunnelStatus(settings);
|
|
669
|
+
for (const name of ['cloudflare', 'tailscale']) {
|
|
670
|
+
if (status[name].enabled) {
|
|
671
|
+
out(kv(name, status[name].installed ? status[name].command : 'enabled, CLI missing — ' + status[name].install));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
out(kv('Auto-open', settings.autoOpen ? 'yes' : 'no'));
|
|
675
|
+
return 0;
|
|
676
|
+
}
|
|
677
|
+
case 'get': {
|
|
678
|
+
const [key] = args;
|
|
679
|
+
if (!key || (!(key in SETTABLE) && key !== 'accessKey'))
|
|
680
|
+
throw new Error('Unknown key. Known: ' + [...Object.keys(SETTABLE), 'accessKey'].join(', '));
|
|
681
|
+
const settings = await readSettings(file);
|
|
682
|
+
const value = key === 'accessKey' ? maskAccessKey(settings.accessKey) : dig(settings, key);
|
|
683
|
+
out(String(value ?? ''));
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
case 'set': {
|
|
687
|
+
const [key, ...rest] = args;
|
|
688
|
+
const raw = rest.join(' ');
|
|
689
|
+
if (!key || !(key in SETTABLE))
|
|
690
|
+
throw new Error('Unknown or unsettable key "' + key + '". Known: ' + Object.keys(SETTABLE).join(', ') + '\nThe access key is managed by `onboarder config key …`.');
|
|
691
|
+
if (!raw)
|
|
692
|
+
throw new Error('Missing value: onboarder config set ' + key + ' <value>');
|
|
693
|
+
const value = SETTABLE[key](raw);
|
|
694
|
+
const current = await readSettings(file);
|
|
695
|
+
const next = { ...current, account: { ...current.account }, tunnel: { ...current.tunnel } };
|
|
696
|
+
bury(next, key, value);
|
|
697
|
+
await writeSettings(next, file); // normalizeSettings validates the result
|
|
698
|
+
out(tick + key + ' = ' + (typeof value === 'boolean' ? String(value) : value));
|
|
699
|
+
if (['host', 'port'].includes(key))
|
|
700
|
+
out(dim(' Restart the server for this to take effect.'));
|
|
701
|
+
return 0;
|
|
656
702
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
661
|
-
case 'get': {
|
|
662
|
-
const [key] = args;
|
|
663
|
-
if (!key || (!(key in SETTABLE) && key !== 'accessKey')) throw new Error('Unknown key. Known: ' + [...Object.keys(SETTABLE), 'accessKey'].join(', '));
|
|
664
|
-
const settings = await readSettings(file);
|
|
665
|
-
const value = key === 'accessKey' ? maskAccessKey(settings.accessKey) : dig(settings, key);
|
|
666
|
-
out(String(value ?? ''));
|
|
667
|
-
return 0;
|
|
668
|
-
}
|
|
669
|
-
case 'set': {
|
|
670
|
-
const [key, ...rest] = args;
|
|
671
|
-
const raw = rest.join(' ');
|
|
672
|
-
if (!key || !(key in SETTABLE)) throw new Error('Unknown or unsettable key "' + key + '". Known: ' + Object.keys(SETTABLE).join(', ') + '\nThe access key is managed by `onboarder config key …`.');
|
|
673
|
-
if (!raw) throw new Error('Missing value: onboarder config set ' + key + ' <value>');
|
|
674
|
-
const value = SETTABLE[key](raw);
|
|
675
|
-
const current = await readSettings(file);
|
|
676
|
-
const next = { ...current, account: { ...current.account }, tunnel: { ...current.tunnel } };
|
|
677
|
-
bury(next, key, value);
|
|
678
|
-
await writeSettings(next, file); // normalizeSettings validates the result
|
|
679
|
-
out(tick + key + ' = ' + (typeof value === 'boolean' ? String(value) : value));
|
|
680
|
-
if (['host', 'port'].includes(key)) out(dim(' Restart the server for this to take effect.'));
|
|
681
|
-
return 0;
|
|
682
|
-
}
|
|
683
|
-
default:
|
|
684
|
-
throw new Error('Usage: onboarder config <show|get|set|path|reset|key>');
|
|
685
|
-
}
|
|
703
|
+
default:
|
|
704
|
+
throw new Error('Usage: onboarder config <show|get|set|path|reset|key>');
|
|
705
|
+
}
|
|
686
706
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
707
|
// Config surgery that did not fit in `set`: full reset, and the access key,
|
|
690
708
|
// which gets its own verb because it is the one value that is shown once and
|
|
691
709
|
// never round-trips through an API.
|
|
692
710
|
export async function runConfigKey(action, args, { flags = {}, out = console.log, err = console.error } = {}) {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
711
|
+
const file = flags.config || configPath();
|
|
712
|
+
const current = await readSettings(file);
|
|
713
|
+
if (action === 'rotate') {
|
|
714
|
+
const { generateAccessKey } = await import('../server/config.js');
|
|
715
|
+
const key = generateAccessKey();
|
|
716
|
+
await writeSettings({ ...current, accessKey: key }, file);
|
|
717
|
+
out(tick + 'Rotated. The old key is dead this second — settings are re-read per request.');
|
|
718
|
+
out(' ' + bold(key));
|
|
719
|
+
out(dim(' Shown once. Distribute it to your devices.'));
|
|
720
|
+
return 0;
|
|
721
|
+
}
|
|
722
|
+
if (action === 'show') {
|
|
723
|
+
out(current.accessKey || '(no key set)');
|
|
724
|
+
return 0;
|
|
725
|
+
}
|
|
726
|
+
if (action === 'set') {
|
|
727
|
+
const value = args.join(' ').trim();
|
|
728
|
+
if (value.length < 16)
|
|
729
|
+
throw new Error('Access keys are at least 16 characters.');
|
|
730
|
+
await writeSettings({ ...current, accessKey: value }, file);
|
|
731
|
+
out(tick + 'Access key updated.');
|
|
732
|
+
return 0;
|
|
733
|
+
}
|
|
734
|
+
throw new Error('Usage: onboarder config key <rotate|show|set <value>>');
|
|
716
735
|
}
|
|
717
|
-
|
|
718
736
|
export async function runConfigReset({ flags = {}, out = console.log, err = console.error } = {}) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
737
|
+
const file = flags.config || configPath();
|
|
738
|
+
if (!flags.yes) {
|
|
739
|
+
err(' This rewrites ' + file + ' with factory defaults (local mode, port 4310, no key).');
|
|
740
|
+
err(' Re-run with --yes to confirm.');
|
|
741
|
+
return 1;
|
|
742
|
+
}
|
|
743
|
+
await writeSettings({ ...DEFAULT_SETTINGS }, file);
|
|
744
|
+
out(tick + 'Reset to defaults: ' + file);
|
|
745
|
+
return 0;
|
|
728
746
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
747
|
// --------------------------------------------------------------- doctor ---
|
|
732
|
-
|
|
733
748
|
// Diagnose without mutating. Every check reports { id, ok, required, detail }
|
|
734
749
|
// and the command exits non-zero if any *required* check failed — the same
|
|
735
750
|
// shape `doctor --json` hands to scripts.
|
|
736
751
|
export async function runDoctor({ flags = {}, out = console.log } = {}) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
checks.push({
|
|
741
|
-
id: 'node', ok: Number(process.versions.node.split('.')[0]) >= 20, required: true,
|
|
742
|
-
detail: `v${process.versions.node} (needs ≥ 20)`,
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
let settings = null;
|
|
746
|
-
try {
|
|
747
|
-
settings = await readSettings(file);
|
|
748
|
-
checks.push({ id: 'config', ok: true, required: true, detail: file });
|
|
749
|
-
} catch (e) {
|
|
750
|
-
checks.push({ id: 'config', ok: false, required: true, detail: `${file} — ${e.message}` });
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
const dir = file.slice(0, file.lastIndexOf('/'));
|
|
754
|
-
let dirOk = true;
|
|
755
|
-
try { await fsp.access(dir, fs.constants.W_OK); } catch { dirOk = false; }
|
|
756
|
-
checks.push({ id: 'config-dir', ok: dirOk, required: true, detail: dirOk ? dir + ' is writable' : dir + ' is NOT writable' });
|
|
757
|
-
|
|
758
|
-
if (settings) {
|
|
759
|
-
const free = await portIsFree(settings.host, settings.port);
|
|
752
|
+
const file = flags.config || configPath();
|
|
753
|
+
const checks = [];
|
|
760
754
|
checks.push({
|
|
761
|
-
|
|
762
|
-
|
|
755
|
+
id: 'node', ok: Number(process.versions.node.split('.')[0]) >= 20, required: true,
|
|
756
|
+
detail: `v${process.versions.node} (needs ≥ 20)`,
|
|
763
757
|
});
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
758
|
+
let settings = null;
|
|
759
|
+
try {
|
|
760
|
+
settings = await readSettings(file);
|
|
761
|
+
checks.push({ id: 'config', ok: true, required: true, detail: file });
|
|
762
|
+
}
|
|
763
|
+
catch (e) {
|
|
764
|
+
checks.push({ id: 'config', ok: false, required: true, detail: `${file} — ${e.message}` });
|
|
765
|
+
}
|
|
766
|
+
const dir = file.slice(0, file.lastIndexOf('/'));
|
|
767
|
+
let dirOk = true;
|
|
768
|
+
try {
|
|
769
|
+
await fsp.access(dir, fs.constants.W_OK);
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
dirOk = false;
|
|
773
|
+
}
|
|
774
|
+
checks.push({ id: 'config-dir', ok: dirOk, required: true, detail: dirOk ? dir + ' is writable' : dir + ' is NOT writable' });
|
|
775
|
+
if (settings) {
|
|
776
|
+
const free = await portIsFree(settings.host, settings.port);
|
|
777
|
+
checks.push({
|
|
778
|
+
id: 'port', ok: free, required: false,
|
|
779
|
+
detail: free ? `${settings.host}:${settings.port} is free` : `${settings.host}:${settings.port} is already in use (running already?)`,
|
|
780
|
+
});
|
|
781
|
+
if (settings.mode === 'self-hosted') {
|
|
782
|
+
checks.push({
|
|
783
|
+
id: 'access-key', ok: Boolean(settings.accessKey), required: true,
|
|
784
|
+
detail: settings.accessKey ? 'set — ' + maskAccessKey(settings.accessKey) : 'MISSING — the API refuses every call until one is set',
|
|
785
|
+
});
|
|
786
|
+
const loopback = ['127.0.0.1', 'localhost', '::1', '[::1]'].includes(settings.host);
|
|
787
|
+
checks.push({
|
|
788
|
+
id: 'domain', ok: true, required: false,
|
|
789
|
+
detail: settings.domain || (loopback ? '(none — a tunnel or local reverse proxy provides the name)' : '(none — visitors can connect by server IP)'),
|
|
790
|
+
});
|
|
791
|
+
if (settings.https) {
|
|
792
|
+
const readiness = await httpsReadiness(settings, { configFile: file });
|
|
793
|
+
for (const check of readiness.checks.slice(1).filter((item) => item.id !== 'caddy')) {
|
|
794
|
+
checks.push({ ...check, required: check.id !== 'dns-target' });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
778
797
|
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
for (const name of ['git', 'cloudflared', 'tailscale', 'caddy']) {
|
|
784
|
-
const wanted = name === 'git'
|
|
785
|
-
|| (name === 'caddy' && settings?.https)
|
|
786
|
-
|| (settings && settings.tunnel?.[name === 'cloudflared' ? 'cloudflare' : 'tailscale']);
|
|
787
|
-
const found = Boolean(findOnPath(name));
|
|
788
|
-
checks.push({
|
|
789
|
-
id: name,
|
|
790
|
-
ok: found || !wanted,
|
|
791
|
-
required: name === 'git' || (name === 'caddy' && settings?.https),
|
|
792
|
-
detail: found ? 'installed' : wanted ? 'not installed — ' + installHint(name) : 'not installed (not needed for your settings)',
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
if (flags.json) {
|
|
797
|
-
out(JSON.stringify({ ok: checks.every((c) => c.ok || !c.required), checks }, null, 2));
|
|
798
|
-
} else {
|
|
799
|
-
out('');
|
|
800
|
-
out(bold(' Onboarder doctor'));
|
|
801
|
-
for (const c of checks) {
|
|
802
|
-
const mark = c.ok ? tick : c.required ? cross : dash;
|
|
803
|
-
out(`${mark}${c.id} — ${c.detail}`);
|
|
804
798
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
799
|
+
for (const name of ['git', 'cloudflared', 'tailscale', 'caddy']) {
|
|
800
|
+
const wanted = name === 'git'
|
|
801
|
+
|| (name === 'caddy' && settings?.https)
|
|
802
|
+
|| (settings && settings.tunnel?.[name === 'cloudflared' ? 'cloudflare' : 'tailscale']);
|
|
803
|
+
const found = Boolean(findOnPath(name));
|
|
804
|
+
checks.push({
|
|
805
|
+
id: name,
|
|
806
|
+
ok: found || !wanted,
|
|
807
|
+
required: name === 'git' || (name === 'caddy' && settings?.https),
|
|
808
|
+
detail: found ? 'installed' : wanted ? 'not installed — ' + installHint(name) : 'not installed (not needed for your settings)',
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
if (flags.json) {
|
|
812
|
+
out(JSON.stringify({ ok: checks.every((c) => c.ok || !c.required), checks }, null, 2));
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
out('');
|
|
816
|
+
out(bold(' Onboarder doctor'));
|
|
817
|
+
for (const c of checks) {
|
|
818
|
+
const mark = c.ok ? tick : c.required ? cross : dash;
|
|
819
|
+
out(`${mark}${c.id} — ${c.detail}`);
|
|
820
|
+
}
|
|
821
|
+
out('');
|
|
822
|
+
}
|
|
823
|
+
return checks.every((c) => c.ok || !c.required) ? 0 : 1;
|
|
808
824
|
}
|
|
809
|
-
|
|
810
825
|
function portIsFree(host, port) {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
826
|
+
return new Promise((resolve) => {
|
|
827
|
+
const probe = net.createServer();
|
|
828
|
+
probe.once('error', () => resolve(false));
|
|
829
|
+
probe.listen(port, host, () => probe.close(() => resolve(true)));
|
|
830
|
+
});
|
|
816
831
|
}
|
|
817
|
-
|
|
818
832
|
// ------------------------------------------------------------------ https ---
|
|
819
|
-
|
|
820
833
|
export async function runHttps(action = 'status', { flags = {}, out = console.log, err = console.error } = {}) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
}
|
|
839
|
-
if (action === 'check') {
|
|
840
|
-
const readiness = await httpsReadiness(settings, { configFile: file });
|
|
841
|
-
if (flags.json) out(JSON.stringify(readiness, null, 2));
|
|
842
|
-
else {
|
|
843
|
-
out('');
|
|
844
|
-
out(bold(' HTTPS readiness'));
|
|
845
|
-
for (const check of readiness.checks) out(`${check.ok ? tick : check.required ? cross : dash}${check.id} — ${check.detail}`);
|
|
846
|
-
if (!readiness.ok) {
|
|
847
|
-
out('');
|
|
848
|
-
out(warn(' Fix the required items, then run `onboarder https setup` again.'));
|
|
849
|
-
out(dim(' DNS: point an A/AAAA record to this VPS. Firewall: allow inbound 80 and 443.'));
|
|
850
|
-
}
|
|
851
|
-
out('');
|
|
852
|
-
}
|
|
853
|
-
return readiness.ok ? 0 : 1;
|
|
854
|
-
}
|
|
855
|
-
if (action === 'setup' || action === 'start') {
|
|
856
|
-
const readiness = await httpsReadiness(settings, { configFile: file });
|
|
857
|
-
if (!readiness.ok) {
|
|
858
|
-
err(bad(' HTTPS setup is not ready:'));
|
|
859
|
-
for (const check of readiness.checks.filter((item) => item.required && !item.ok)) err(cross + check.id + ' — ' + check.detail);
|
|
860
|
-
err(' Point DNS at this VPS, allow inbound TCP 80/443, install Caddy, then retry.');
|
|
861
|
-
return 1;
|
|
862
|
-
}
|
|
863
|
-
const caddyfile = await writeCaddyfile(settings, file);
|
|
864
|
-
out(dim(' Caddyfile: ' + caddyfile));
|
|
865
|
-
await caddyValidate(settings, file);
|
|
866
|
-
|
|
867
|
-
// Ubuntu's package usually leaves Caddy running as a service. Reload first
|
|
868
|
-
// so repeat setup is idempotent and does not mistake Caddy for a foreign
|
|
869
|
-
// listener. Reload talks only to Caddy's local admin endpoint and never
|
|
870
|
-
// stops an unknown process.
|
|
871
|
-
let result;
|
|
872
|
-
let reloaded = false;
|
|
873
|
-
try {
|
|
874
|
-
result = caddyRun(settings, file, 'reload');
|
|
875
|
-
reloaded = true;
|
|
876
|
-
} catch {
|
|
877
|
-
try {
|
|
878
|
-
result = caddyRun(settings, file, 'start');
|
|
879
|
-
} catch (error) {
|
|
880
|
-
err(bad(' Caddy could not start: ' + (error.message || error)));
|
|
881
|
-
if (process.platform !== 'win32') {
|
|
882
|
-
err(' On Ubuntu, start the packaged service once:');
|
|
883
|
-
err(' sudo systemctl enable --now caddy');
|
|
884
|
-
err(' Then run `onboarder https setup` again.');
|
|
834
|
+
const file = flags.config || configPath();
|
|
835
|
+
const settings = await readSettings(file);
|
|
836
|
+
const status = httpsStatus(settings, file);
|
|
837
|
+
if (action === 'status') {
|
|
838
|
+
if (flags.json)
|
|
839
|
+
out(JSON.stringify(status, null, 2));
|
|
840
|
+
else {
|
|
841
|
+
out('');
|
|
842
|
+
out(bold(' Onboarder HTTPS'));
|
|
843
|
+
out(kv('Enabled', status.enabled ? 'yes' : 'no'));
|
|
844
|
+
out(kv('Domain', status.domain || '(not configured)'));
|
|
845
|
+
out(kv('URL', status.url || '(none)'));
|
|
846
|
+
out(kv('Caddy', status.caddyInstalled ? status.caddyVersion : 'not installed'));
|
|
847
|
+
out(kv('Caddyfile', status.caddyfile));
|
|
848
|
+
if (status.enabled)
|
|
849
|
+
out(dim(' `onboarder https check` verifies DNS and ports before issuance.'));
|
|
850
|
+
out('');
|
|
885
851
|
}
|
|
886
|
-
return 1;
|
|
887
|
-
}
|
|
852
|
+
return status.enabled ? 0 : 1;
|
|
888
853
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
854
|
+
if (action === 'check') {
|
|
855
|
+
const readiness = await httpsReadiness(settings, { configFile: file });
|
|
856
|
+
if (flags.json)
|
|
857
|
+
out(JSON.stringify(readiness, null, 2));
|
|
858
|
+
else {
|
|
859
|
+
out('');
|
|
860
|
+
out(bold(' HTTPS readiness'));
|
|
861
|
+
for (const check of readiness.checks)
|
|
862
|
+
out(`${check.ok ? tick : check.required ? cross : dash}${check.id} — ${check.detail}`);
|
|
863
|
+
if (!readiness.ok) {
|
|
864
|
+
out('');
|
|
865
|
+
out(warn(' Fix the required items, then run `onboarder https setup` again.'));
|
|
866
|
+
out(dim(' DNS: point an A/AAAA record to this VPS. Firewall: allow inbound 80 and 443.'));
|
|
867
|
+
}
|
|
868
|
+
out('');
|
|
869
|
+
}
|
|
870
|
+
return readiness.ok ? 0 : 1;
|
|
871
|
+
}
|
|
872
|
+
if (action === 'setup' || action === 'start') {
|
|
873
|
+
const readiness = await httpsReadiness(settings, { configFile: file });
|
|
874
|
+
if (!readiness.ok) {
|
|
875
|
+
err(bad(' HTTPS setup is not ready:'));
|
|
876
|
+
for (const check of readiness.checks.filter((item) => item.required && !item.ok))
|
|
877
|
+
err(cross + check.id + ' — ' + check.detail);
|
|
878
|
+
err(' Point DNS at this VPS, allow inbound TCP 80/443, install Caddy, then retry.');
|
|
879
|
+
return 1;
|
|
880
|
+
}
|
|
881
|
+
const caddyfile = await writeCaddyfile(settings, file);
|
|
882
|
+
out(dim(' Caddyfile: ' + caddyfile));
|
|
883
|
+
await caddyValidate(settings, file);
|
|
884
|
+
// Ubuntu's package usually leaves Caddy running as a service. Reload first
|
|
885
|
+
// so repeat setup is idempotent and does not mistake Caddy for a foreign
|
|
886
|
+
// listener. Reload talks only to Caddy's local admin endpoint and never
|
|
887
|
+
// stops an unknown process.
|
|
888
|
+
let result;
|
|
889
|
+
let reloaded = false;
|
|
890
|
+
try {
|
|
891
|
+
result = caddyRun(settings, file, 'reload');
|
|
892
|
+
reloaded = true;
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
try {
|
|
896
|
+
result = caddyRun(settings, file, 'start');
|
|
897
|
+
}
|
|
898
|
+
catch (error) {
|
|
899
|
+
err(bad(' Caddy could not start: ' + (error.message || error)));
|
|
900
|
+
if (process.platform !== 'win32') {
|
|
901
|
+
err(' On Ubuntu, start the packaged service once:');
|
|
902
|
+
err(' sudo systemctl enable --now caddy');
|
|
903
|
+
err(' Then run `onboarder https setup` again.');
|
|
904
|
+
}
|
|
905
|
+
return 1;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
out(tick + (reloaded ? 'Caddy reloaded. It is using' : 'Caddy started. It is using') + ' https://' + settings.domain + '.');
|
|
909
|
+
out(' Public URL https://' + settings.domain);
|
|
910
|
+
out(dim(' Upstream http://127.0.0.1:' + settings.port));
|
|
911
|
+
if (result.output)
|
|
912
|
+
out(dim(' ' + result.output.split('\n').slice(-3).join('\n ')));
|
|
913
|
+
return 0;
|
|
914
|
+
}
|
|
915
|
+
if (action === 'stop') {
|
|
916
|
+
if (!status.caddyInstalled) {
|
|
917
|
+
err(' Caddy is not installed.');
|
|
918
|
+
return 1;
|
|
919
|
+
}
|
|
920
|
+
caddyRun(settings, file, 'stop');
|
|
921
|
+
out(tick + 'Caddy stopped. The Onboarder HTTP server is unchanged.');
|
|
922
|
+
return 0;
|
|
923
|
+
}
|
|
924
|
+
throw new Error('Usage: onboarder https <check|setup|start|stop|status>');
|
|
902
925
|
}
|
|
903
|
-
|
|
904
926
|
// --------------------------------------------------------------- tunnel ---
|
|
905
|
-
|
|
906
927
|
export async function runTunnel(kind, { flags = {}, out = console.log, err = console.error } = {}) {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
928
|
+
const settings = await readSettings(flags.config || configPath());
|
|
929
|
+
const target = `http://127.0.0.1:${settings.port}`;
|
|
930
|
+
if (kind === 'tailscale') {
|
|
931
|
+
if (!findOnPath('tailscale')) {
|
|
932
|
+
err(' tailscale is not installed — ' + installHint('tailscale'));
|
|
933
|
+
return 1;
|
|
934
|
+
}
|
|
935
|
+
const cmd = tailscaleCommand(settings);
|
|
936
|
+
out(' ' + cmd);
|
|
937
|
+
const { spawnSync } = await import('node:child_process');
|
|
938
|
+
const r = spawnSync(cmd, { shell: true, stdio: 'inherit' });
|
|
939
|
+
if (r.status === 0) {
|
|
940
|
+
out(tick + 'Tailscale is serving ' + target + ' over your tailnet (https://<machine>.<tailnet>.ts.net).');
|
|
941
|
+
out(dim(' `tailscale serve status` to verify, `onboarder doctor` to re-check.'));
|
|
942
|
+
}
|
|
943
|
+
return r.status ?? 1;
|
|
944
|
+
}
|
|
945
|
+
if (kind === 'cloudflare') {
|
|
946
|
+
if (!findOnPath('cloudflared')) {
|
|
947
|
+
err(' cloudflared is not installed — ' + installHint('cloudflared'));
|
|
948
|
+
return 1;
|
|
949
|
+
}
|
|
950
|
+
const cmd = cloudflareCommand(settings);
|
|
951
|
+
out(dim(' $ ' + cmd));
|
|
952
|
+
out(dim(' Ctrl-C stops the tunnel; the server keeps running.\n'));
|
|
953
|
+
// Quick tunnels print their random name on stderr; surface just that line
|
|
954
|
+
// plus everything when --verbose, so the default output is one useful URL.
|
|
955
|
+
const child = spawn('cloudflared', ['tunnel', '--url', target], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
956
|
+
let announced = false;
|
|
957
|
+
child.stderr.on('data', (chunk) => {
|
|
958
|
+
const text = String(chunk);
|
|
959
|
+
const m = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
960
|
+
if (m && !announced) {
|
|
961
|
+
announced = true;
|
|
962
|
+
out(tick + 'Public URL: ' + bold(m[0]));
|
|
963
|
+
out(dim(' Anyone with the URL still needs the access key: ' + m[0]));
|
|
964
|
+
}
|
|
965
|
+
if (flags.verbose)
|
|
966
|
+
process.stderr.write(text);
|
|
967
|
+
});
|
|
968
|
+
child.stdout.on('data', (d) => { if (flags.verbose)
|
|
969
|
+
process.stdout.write(d); });
|
|
970
|
+
const stop = () => child.kill('SIGINT');
|
|
971
|
+
process.once('SIGINT', stop);
|
|
972
|
+
return new Promise((resolve) => {
|
|
973
|
+
child.on('close', (code) => {
|
|
974
|
+
process.removeListener('SIGINT', stop);
|
|
975
|
+
if (!announced && code !== 0)
|
|
976
|
+
err(' cloudflared exited without printing a URL — re-run with --verbose to see why.');
|
|
977
|
+
resolve(code ?? 0);
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
throw new Error('Usage: onboarder tunnel <cloudflare|tailscale>');
|
|
958
982
|
}
|
|
983
|
+
//# sourceMappingURL=commands.js.map
|