@frumu/tandem-panel 0.3.28 → 0.4.3

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/.env.example CHANGED
@@ -1,7 +1,13 @@
1
1
  # Control panel bind port
2
2
  TANDEM_CONTROL_PANEL_PORT=39732
3
3
 
4
- # Tip: run `tandem-control-panel --init` to auto-generate this file and token.
4
+ # Tip: run `tandem-setup init` to auto-generate this file and token.
5
+
6
+ # Control panel bind host (loopback by default)
7
+ TANDEM_CONTROL_PANEL_HOST=127.0.0.1
8
+
9
+ # Optional externally reachable URL for future pairing / gateway flows
10
+ TANDEM_CONTROL_PANEL_PUBLIC_URL=
5
11
 
6
12
  # Full engine URL (preferred)
7
13
  TANDEM_ENGINE_URL=http://127.0.0.1:39731
@@ -13,6 +19,10 @@ TANDEM_ENGINE_PORT=39731
13
19
  # Auto-start local engine if not running (1=yes, 0=no)
14
20
  TANDEM_CONTROL_PANEL_AUTO_START_ENGINE=1
15
21
 
22
+ # Canonical state roots for official bootstrap installs
23
+ TANDEM_STATE_DIR=
24
+ TANDEM_CONTROL_PANEL_STATE_DIR=
25
+
16
26
  # Engine API token used when control panel auto-starts a local engine.
17
27
  # If unset, the panel generates one at startup and prints it in logs.
18
28
  TANDEM_CONTROL_PANEL_ENGINE_TOKEN=tk_change_me
@@ -26,6 +36,15 @@ TANDEM_PROVIDER_STREAM_IDLE_TIMEOUT_MS=90000
26
36
  TANDEM_PERMISSION_WAIT_TIMEOUT_MS=15000
27
37
  TANDEM_TOOL_EXEC_TIMEOUT_MS=45000
28
38
  TANDEM_BASH_TIMEOUT_MS=30000
39
+ # Bug Monitor (disabled by default)
40
+ # TANDEM_BUG_MONITOR_ENABLED=0
41
+ # TANDEM_BUG_MONITOR_REPO=owner/repo
42
+ # TANDEM_BUG_MONITOR_MCP_SERVER=github
43
+ # TANDEM_BUG_MONITOR_PROVIDER_PREFERENCE=auto
44
+ # TANDEM_BUG_MONITOR_PROVIDER_ID=openrouter
45
+ # TANDEM_BUG_MONITOR_MODEL_ID=openai/gpt-4.1-mini
46
+ # Optional global duplicate-signature retry limit for tool calls.
47
+ # TANDEM_TOOL_LOOP_DUPLICATE_SIGNATURE_LIMIT=200
29
48
 
30
49
  # Optional caps when guard budgets are enabled
31
50
  # TANDEM_TOOL_BUDGET_DEFAULT=10
package/README.md CHANGED
@@ -8,40 +8,47 @@ Full web control center for Tandem Engine (non-desktop entry point).
8
8
  npm i -g @frumu/tandem-panel
9
9
  ```
10
10
 
11
- ## Run
11
+ ## Official Bootstrap
12
12
 
13
13
  ```bash
14
- tandem-control-panel
14
+ tandem-setup init
15
15
  ```
16
16
 
17
- Alias also supported:
17
+ This creates a canonical env file, bootstraps engine state, and installs services on Linux/macOS when run with the privileges needed for service registration.
18
+
19
+ Useful follow-up commands:
18
20
 
19
21
  ```bash
20
- tandem-setup
22
+ tandem-setup doctor
23
+ tandem-setup service status
24
+ tandem-setup service restart
25
+ tandem-setup pair mobile
21
26
  ```
22
27
 
23
- Bootstrap env/token first (recommended):
28
+ ## Run Foreground
24
29
 
25
30
  ```bash
26
- tandem-control-panel --init
31
+ tandem-control-panel
27
32
  ```
28
33
 
29
34
  Or:
30
35
 
31
36
  ```bash
32
- tandem-control-panel-init
37
+ tandem-setup run
33
38
  ```
34
39
 
35
- Install Linux systemd services (engine + panel):
40
+ ## Service Management
36
41
 
37
42
  ```bash
38
- sudo tandem-control-panel --install-services
43
+ tandem-setup service install
44
+ tandem-setup service status
45
+ tandem-setup service restart
46
+ tandem-setup service logs
39
47
  ```
40
48
 
41
- Options:
49
+ Legacy flag mode is still supported for compatibility:
42
50
 
43
- - `--service-mode=both|engine|panel` (default `both`)
44
- - `--service-user=<linux-user>` (default: `SUDO_USER`/current user)
51
+ `tandem-control-panel --init`, `--install-services`, and `--service-op=...`
45
52
 
46
53
  ## Features
47
54
 
@@ -68,8 +75,12 @@ cp .env.example .env
68
75
  Variables:
69
76
 
70
77
  - `TANDEM_CONTROL_PANEL_PORT` (default `39732`)
78
+ - `TANDEM_CONTROL_PANEL_HOST` (default `127.0.0.1`)
79
+ - `TANDEM_CONTROL_PANEL_PUBLIC_URL` (optional future pairing / gateway URL)
71
80
  - `TANDEM_ENGINE_URL` (default `http://127.0.0.1:39731`)
72
81
  - `TANDEM_ENGINE_HOST` + `TANDEM_ENGINE_PORT` fallback
82
+ - `TANDEM_STATE_DIR` (canonical engine state dir for official installs)
83
+ - `TANDEM_CONTROL_PANEL_STATE_DIR` (control-panel state dir for official installs)
73
84
  - `TANDEM_CONTROL_PANEL_AUTO_START_ENGINE` (`1`/`0`)
74
85
  - `TANDEM_CONTROL_PANEL_ENGINE_TOKEN` (token injected when panel auto-starts engine)
75
86
  - `TANDEM_API_TOKEN` (backward-compatible alias for engine token)
@@ -117,9 +128,10 @@ Notes:
117
128
 
118
129
  ## Setup Flow
119
130
 
120
- 1. Run `tandem-control-panel --init` to create/update `.env` and generate a token if missing.
121
- 2. Run `tandem-control-panel`.
122
- 3. Sign in with the printed `TANDEM_CONTROL_PANEL_ENGINE_TOKEN`.
131
+ 1. Run `tandem-setup init`.
132
+ 2. Verify with `tandem-setup doctor`.
133
+ 3. If running foreground, start `tandem-control-panel`.
134
+ 4. Sign in with the printed `TANDEM_CONTROL_PANEL_ENGINE_TOKEN`.
123
135
 
124
136
  ## Development
125
137
 
@@ -129,3 +141,20 @@ npm install
129
141
  npm run dev
130
142
  npm run build
131
143
  ```
144
+
145
+ ### Repo Source Workflow (No Global npm Install)
146
+
147
+ If you run from this repo directly, use:
148
+
149
+ ```bash
150
+ node packages/tandem-control-panel/bin/cli.js init --no-service
151
+ node packages/tandem-control-panel/bin/cli.js run
152
+ ```
153
+
154
+ Service install/ops from source:
155
+
156
+ ```bash
157
+ sudo node packages/tandem-control-panel/bin/cli.js service install
158
+ node packages/tandem-control-panel/bin/cli.js service status
159
+ sudo node packages/tandem-control-panel/bin/cli.js service restart
160
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { basename } from "path";
4
+ import { spawn } from "child_process";
5
+ import { fileURLToPath } from "url";
6
+
7
+ import { printInitSummary, initializeInstall } from "../lib/setup/bootstrap.js";
8
+ import { printDoctor, runDoctor } from "../lib/setup/doctor.js";
9
+ import { parseCliArgs, err } from "../lib/setup/common.js";
10
+ import { resolveSetupPaths } from "../lib/setup/paths.js";
11
+ import { installSystemdServices, operateSystemdServices } from "../lib/setup/services/systemd.js";
12
+ import { installLaunchdServices, operateLaunchdServices } from "../lib/setup/services/launchd.js";
13
+ import { resolveUserHome } from "../lib/setup/services/common.js";
14
+
15
+ const argv = process.argv.slice(2);
16
+ const cli = parseCliArgs(argv);
17
+ const entry = basename(process.argv[1] || "tandem-setup");
18
+ const setupLegacyPath = fileURLToPath(new URL("./setup.js", import.meta.url));
19
+
20
+ function runLegacy(args = []) {
21
+ return new Promise((resolve, reject) => {
22
+ const child = spawn(process.execPath, [setupLegacyPath, ...args], {
23
+ stdio: "inherit",
24
+ env: process.env,
25
+ });
26
+ child.on("error", reject);
27
+ child.on("close", (code) => resolve(code || 0));
28
+ });
29
+ }
30
+
31
+ async function installServicesFromEnv(envFile) {
32
+ const paths = resolveSetupPaths();
33
+ const serviceUser = String(cli.value("service-user") || process.env.SUDO_USER || process.env.USER || "").trim();
34
+ const homeDir = (await resolveUserHome(serviceUser || process.env.USER, process.platform)) || paths.home;
35
+ const options = {
36
+ envFile,
37
+ homeDir,
38
+ logsDir: paths.logsDir,
39
+ serviceUser: serviceUser || process.env.USER,
40
+ nodePath: process.execPath,
41
+ };
42
+ if (process.platform === "linux") return installSystemdServices(options);
43
+ if (process.platform === "darwin") return installLaunchdServices(options);
44
+ throw new Error("Service install is only supported on Linux and macOS.");
45
+ }
46
+
47
+ async function operateServices(operation) {
48
+ if (process.platform === "linux") return operateSystemdServices(operation);
49
+ if (process.platform === "darwin") return operateLaunchdServices(operation);
50
+ throw new Error("Service operations are only supported on Linux and macOS.");
51
+ }
52
+
53
+ async function main() {
54
+ const first = String(argv[0] || "").trim();
55
+ if (!first) {
56
+ process.exit(await runLegacy([]));
57
+ }
58
+
59
+ if (first.startsWith("--")) {
60
+ console.warn("[Tandem Setup] Legacy flag mode is deprecated. Use `tandem-setup init|service|doctor`.");
61
+ process.exit(await runLegacy(argv));
62
+ }
63
+
64
+ if (first === "run") {
65
+ process.exit(await runLegacy(argv.slice(1)));
66
+ }
67
+
68
+ if (first === "init") {
69
+ const result = await initializeInstall({
70
+ envPath: cli.value("env-file"),
71
+ overwrite: cli.has("rotate-token") || cli.has("reset-token"),
72
+ allowAmbientStateEnv: false,
73
+ allowCwdEnvMerge: false,
74
+ });
75
+ if (process.platform === "linux" || process.platform === "darwin") {
76
+ if (!cli.has("no-service") && !cli.has("foreground")) {
77
+ await installServicesFromEnv(result.envPath);
78
+ }
79
+ }
80
+ printInitSummary(result);
81
+ return;
82
+ }
83
+
84
+ if (first === "doctor") {
85
+ const result = await runDoctor({
86
+ envFile: cli.value("env-file"),
87
+ allowAmbientStateEnv: false,
88
+ allowCwdEnvMerge: false,
89
+ });
90
+ printDoctor(result, cli.has("json"));
91
+ process.exit(result.ok ? 0 : 1);
92
+ }
93
+
94
+ if (first === "service") {
95
+ const op = String(argv[1] || "").trim().toLowerCase();
96
+ if (!op || op === "install") {
97
+ const result = await initializeInstall({
98
+ envPath: cli.value("env-file"),
99
+ overwrite: false,
100
+ allowAmbientStateEnv: false,
101
+ allowCwdEnvMerge: false,
102
+ });
103
+ await installServicesFromEnv(result.envPath);
104
+ return;
105
+ }
106
+ await operateServices(op);
107
+ return;
108
+ }
109
+
110
+ if (first === "pair" && String(argv[1] || "").trim().toLowerCase() === "mobile") {
111
+ const doctor = await runDoctor({
112
+ envFile: cli.value("env-file"),
113
+ allowAmbientStateEnv: false,
114
+ allowCwdEnvMerge: false,
115
+ });
116
+ console.log("Mobile pairing is not implemented in this build.");
117
+ console.log(`Control panel: http://${doctor.panelHost}:${doctor.panelPort}`);
118
+ console.log(`Public URL: ${doctor.panelPublicUrl || "(not configured)"}`);
119
+ console.log(`Engine URL: ${doctor.engineUrl}`);
120
+ return;
121
+ }
122
+
123
+ if (entry === "tandem-control-panel") {
124
+ process.exit(await runLegacy(argv));
125
+ }
126
+
127
+ err(`Unknown command: ${first}`);
128
+ process.exit(1);
129
+ }
130
+
131
+ main().catch((error) => {
132
+ err(error instanceof Error ? error.message : String(error));
133
+ process.exit(1);
134
+ });
package/bin/init-env.js CHANGED
@@ -1,89 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { existsSync, readFileSync, writeFileSync } from "fs";
4
- import { resolve, join } from "path";
5
- import { randomBytes } from "crypto";
3
+ import { initializeInstall, printInitSummary } from "../lib/setup/bootstrap.js";
4
+ import { parseCliArgs } from "../lib/setup/common.js";
6
5
 
7
- function parseEnv(content) {
8
- const out = {};
9
- for (const rawLine of String(content || "").split(/\r?\n/)) {
10
- const line = rawLine.trim();
11
- if (!line || line.startsWith("#")) continue;
12
- const idx = line.indexOf("=");
13
- if (idx <= 0) continue;
14
- const key = line.slice(0, idx).trim();
15
- let value = line.slice(idx + 1).trim();
16
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
17
- value = value.slice(1, -1);
18
- }
19
- out[key] = value;
20
- }
21
- return out;
22
- }
6
+ const cli = parseCliArgs(process.argv.slice(2));
23
7
 
24
- function serializeEnv(entries) {
25
- return `${entries.map(([k, v]) => `${k}=${v}`).join("\n")}\n`;
26
- }
8
+ const result = await initializeInstall({
9
+ envPath: cli.value("env-file"),
10
+ overwrite: cli.has("reset-token") || cli.has("rotate-token") || cli.has("overwrite"),
11
+ allowAmbientStateEnv: false,
12
+ allowCwdEnvMerge: false,
13
+ });
27
14
 
28
- function ensureEnv({ cwd = process.cwd(), overwrite = false } = {}) {
29
- const envPath = resolve(cwd, ".env");
30
- const existed = existsSync(envPath);
31
- const examplePath = resolve(cwd, ".env.example");
32
- const localExamplePath = resolve(join(process.cwd(), "packages", "tandem-control-panel", ".env.example"));
33
-
34
- const sourcePath = existsSync(examplePath) ? examplePath : localExamplePath;
35
- const defaults = existsSync(sourcePath)
36
- ? parseEnv(readFileSync(sourcePath, "utf8"))
37
- : {
38
- TANDEM_CONTROL_PANEL_PORT: "39732",
39
- TANDEM_ENGINE_URL: "http://127.0.0.1:39731",
40
- TANDEM_CONTROL_PANEL_AUTO_START_ENGINE: "1",
41
- };
42
-
43
- const current = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
44
- const merged = { ...defaults, ...current };
45
-
46
- if (overwrite || !merged.TANDEM_CONTROL_PANEL_ENGINE_TOKEN || merged.TANDEM_CONTROL_PANEL_ENGINE_TOKEN === "tk_change_me") {
47
- merged.TANDEM_CONTROL_PANEL_ENGINE_TOKEN = `tk_${randomBytes(16).toString("hex")}`;
48
- }
49
-
50
- const preferredOrder = [
51
- "TANDEM_CONTROL_PANEL_PORT",
52
- "TANDEM_ENGINE_URL",
53
- "TANDEM_ENGINE_HOST",
54
- "TANDEM_ENGINE_PORT",
55
- "TANDEM_CONTROL_PANEL_AUTO_START_ENGINE",
56
- "TANDEM_CONTROL_PANEL_ENGINE_TOKEN",
57
- "TANDEM_CONTROL_PANEL_SESSION_TTL_MINUTES",
58
- ];
59
-
60
- const ordered = [];
61
- for (const key of preferredOrder) {
62
- if (merged[key] !== undefined) ordered.push([key, merged[key]]);
63
- }
64
- for (const [key, value] of Object.entries(merged)) {
65
- if (!preferredOrder.includes(key)) ordered.push([key, value]);
66
- }
67
-
68
- writeFileSync(envPath, serializeEnv(ordered), "utf8");
69
-
70
- return {
71
- envPath,
72
- token: merged.TANDEM_CONTROL_PANEL_ENGINE_TOKEN,
73
- created: !existed,
74
- engineUrl: merged.TANDEM_ENGINE_URL || `http://${merged.TANDEM_ENGINE_HOST || "127.0.0.1"}:${merged.TANDEM_ENGINE_PORT || "39731"}`,
75
- panelPort: merged.TANDEM_CONTROL_PANEL_PORT || "39732",
76
- };
77
- }
78
-
79
- if (import.meta.url === `file://${process.argv[1]}`) {
80
- const overwrite = process.argv.includes("--reset-token") || process.argv.includes("--overwrite");
81
- const result = ensureEnv({ overwrite });
82
- console.log("[Tandem Control Panel] Environment initialized.");
83
- console.log(`[Tandem Control Panel] .env: ${result.envPath}`);
84
- console.log(`[Tandem Control Panel] Engine URL: ${result.engineUrl}`);
85
- console.log(`[Tandem Control Panel] Panel URL: http://localhost:${result.panelPort}`);
86
- console.log(`[Tandem Control Panel] Token: ${result.token}`);
87
- }
88
-
89
- export { ensureEnv };
15
+ printInitSummary(result);
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ SETUP_JS="$SCRIPT_DIR/setup.js"
6
+ NODE_BIN="${NODE_BIN:-$(command -v node || true)}"
7
+
8
+ if [[ -z "${NODE_BIN}" ]]; then
9
+ echo "node not found in PATH" >&2
10
+ exit 1
11
+ fi
12
+
13
+ cmd="${1:-help}"
14
+ arg1="${2:-}"
15
+ arg2="${3:-}"
16
+
17
+ run_setup() {
18
+ sudo "$NODE_BIN" "$SETUP_JS" "$@"
19
+ }
20
+
21
+ case "$cmd" in
22
+ install-panel)
23
+ panel_port="${arg1:-3402}"
24
+ sudo TANDEM_CONTROL_PANEL_PORT="$panel_port" "$NODE_BIN" "$SETUP_JS" --install-services --service-mode=panel
25
+ ;;
26
+ install-both)
27
+ engine_port="${arg1:-39731}"
28
+ panel_port="${arg2:-3402}"
29
+ sudo TANDEM_ENGINE_PORT="$engine_port" TANDEM_CONTROL_PANEL_PORT="$panel_port" \
30
+ "$NODE_BIN" "$SETUP_JS" --install-services --service-mode=both
31
+ ;;
32
+ restart-panel)
33
+ run_setup --service-op=restart --service-mode=panel
34
+ ;;
35
+ restart-both)
36
+ run_setup --service-op=restart --service-mode=both
37
+ ;;
38
+ status-panel)
39
+ run_setup --service-op=status --service-mode=panel
40
+ ;;
41
+ status-both)
42
+ run_setup --service-op=status --service-mode=both
43
+ ;;
44
+ logs-panel)
45
+ run_setup --service-op=logs --service-mode=panel
46
+ ;;
47
+ logs-both)
48
+ run_setup --service-op=logs --service-mode=both
49
+ ;;
50
+ *)
51
+ cat <<'EOF'
52
+ Usage:
53
+ bash packages/tandem-control-panel/bin/service-local.sh install-panel [panel_port]
54
+ bash packages/tandem-control-panel/bin/service-local.sh install-both [engine_port] [panel_port]
55
+ bash packages/tandem-control-panel/bin/service-local.sh restart-panel
56
+ bash packages/tandem-control-panel/bin/service-local.sh restart-both
57
+ bash packages/tandem-control-panel/bin/service-local.sh status-panel
58
+ bash packages/tandem-control-panel/bin/service-local.sh status-both
59
+ bash packages/tandem-control-panel/bin/service-local.sh logs-panel
60
+ bash packages/tandem-control-panel/bin/service-local.sh logs-both
61
+ EOF
62
+ ;;
63
+ esac
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "child_process";
4
+ import { createRequire } from "module";
5
+ import { fileURLToPath } from "url";
6
+
7
+ import { loadDotEnvFile, resolveEnvLoadOrder } from "../lib/setup/env.js";
8
+ import { parseCliArgs } from "../lib/setup/common.js";
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const argv = process.argv.slice(2);
12
+ const mode = String(argv[0] || "").trim().toLowerCase();
13
+ const cli = parseCliArgs(argv.slice(1));
14
+ const explicitEnvFile = String(cli.value("env-file") || "").trim();
15
+
16
+ for (const envPath of resolveEnvLoadOrder({ explicitEnvFile })) {
17
+ loadDotEnvFile(envPath);
18
+ }
19
+
20
+ async function runPanel() {
21
+ const runtimePath = fileURLToPath(new URL("./setup.js", import.meta.url));
22
+ const child = spawn(process.execPath, [runtimePath, "--env-file", explicitEnvFile].filter(Boolean), {
23
+ stdio: "inherit",
24
+ env: process.env,
25
+ });
26
+ child.on("close", (code) => process.exit(code || 0));
27
+ }
28
+
29
+ async function runEngine() {
30
+ const engineEntrypoint = require.resolve("@frumu/tandem/bin/tandem-engine.js");
31
+ const host = String(process.env.TANDEM_ENGINE_HOST || "127.0.0.1").trim();
32
+ const port = String(process.env.TANDEM_ENGINE_PORT || "39731").trim();
33
+ const child = spawn(
34
+ process.execPath,
35
+ [engineEntrypoint, "serve", "--hostname", host, "--port", port],
36
+ { stdio: "inherit", env: process.env }
37
+ );
38
+ child.on("close", (code) => process.exit(code || 0));
39
+ }
40
+
41
+ if (mode === "engine") {
42
+ runEngine();
43
+ } else if (mode === "panel") {
44
+ runPanel();
45
+ } else {
46
+ console.error("Usage: service-runner.js <engine|panel> [--env-file PATH]");
47
+ process.exit(1);
48
+ }