@jhizzard/termdeck-stack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/package.json +39 -0
- package/src/index.js +424 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @jhizzard/termdeck-stack
|
|
2
|
+
|
|
3
|
+
One-command installer for the TermDeck developer memory stack.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx @jhizzard/termdeck-stack
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What gets installed
|
|
10
|
+
|
|
11
|
+
| Layer | Package | What it does |
|
|
12
|
+
|-------|---------|--------------|
|
|
13
|
+
| 1 | `@jhizzard/termdeck` | Browser terminal multiplexer with metadata overlays and Flashback recall toasts |
|
|
14
|
+
| 2 | `@jhizzard/mnestra` | pgvector memory store + MCP server. Lights up Flashback. Provides `memory_*` tools to Claude Code, Cursor, Windsurf |
|
|
15
|
+
| 3 | `@jhizzard/rumen` | Async learning loop on a Supabase Edge Function cron. Synthesizes cross-project insights |
|
|
16
|
+
| 4 | `@supabase/mcp-server-supabase` | MCP that lets the TermDeck setup wizard provision your Supabase project automatically |
|
|
17
|
+
|
|
18
|
+
The wizard:
|
|
19
|
+
|
|
20
|
+
1. Prints the four-layer overview so you see what you're agreeing to.
|
|
21
|
+
2. Detects which pieces are already on your machine.
|
|
22
|
+
3. Asks which tier you want (default: 4 — full stack).
|
|
23
|
+
4. Runs `npm install -g` for the missing pieces.
|
|
24
|
+
5. Merges Mnestra and Supabase MCP entries into `~/.claude/mcp.json` — preserving any existing MCP servers.
|
|
25
|
+
6. Prints the next steps (Supabase PAT, credentials, `termdeck` to start).
|
|
26
|
+
|
|
27
|
+
## Modes
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
npx @jhizzard/termdeck-stack # interactive
|
|
31
|
+
npx @jhizzard/termdeck-stack --tier 4 # unattended
|
|
32
|
+
npx @jhizzard/termdeck-stack --dry-run # print plan, don't install
|
|
33
|
+
npx @jhizzard/termdeck-stack --yes # accept defaults (combine with --tier)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Why this exists
|
|
37
|
+
|
|
38
|
+
The TermDeck stack used to be a 15-step install: provision Supabase, run six SQL migrations, mint API keys, paste them into `secrets.env`, edit `config.yaml`, install Mnestra globally, deploy Rumen, install the Supabase MCP, wire `~/.claude/mcp.json`. Most testers bounced before step 5.
|
|
39
|
+
|
|
40
|
+
This installer collapses every step that's a `npm install -g` into one command, then drops the user at the doorstep of the in-browser setup wizard (which handles credentials).
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jhizzard/termdeck-stack",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One-command installer for the TermDeck developer memory stack: TermDeck + Mnestra + Rumen + Supabase MCP",
|
|
5
|
+
"bin": {
|
|
6
|
+
"termdeck-stack": "./src/index.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "./src/index.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"src/**",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"termdeck",
|
|
16
|
+
"mnestra",
|
|
17
|
+
"rumen",
|
|
18
|
+
"memory",
|
|
19
|
+
"rag",
|
|
20
|
+
"mcp",
|
|
21
|
+
"claude",
|
|
22
|
+
"installer",
|
|
23
|
+
"supabase"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/jhizzard/termdeck.git",
|
|
28
|
+
"directory": "packages/stack-installer"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/jhizzard/termdeck/tree/main/packages/stack-installer#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/jhizzard/termdeck/issues"
|
|
33
|
+
},
|
|
34
|
+
"author": "Joshua Izzard",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// @jhizzard/termdeck-stack — one-command installer for the TermDeck
|
|
4
|
+
// developer memory stack.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// npx @jhizzard/termdeck-stack interactive wizard
|
|
8
|
+
// npx @jhizzard/termdeck-stack --tier 4 unattended (1|2|3|4)
|
|
9
|
+
// npx @jhizzard/termdeck-stack --dry-run print plan, don't install
|
|
10
|
+
//
|
|
11
|
+
// The wizard:
|
|
12
|
+
// 1. Prints the four-layer overview so the user understands what
|
|
13
|
+
// they're agreeing to.
|
|
14
|
+
// 2. Detects which pieces are already installed.
|
|
15
|
+
// 3. Asks (or accepts via --tier) which layers to install.
|
|
16
|
+
// 4. Runs `npm install -g` for missing pieces.
|
|
17
|
+
// 5. Merges entries into ~/.claude/mcp.json for Mnestra and
|
|
18
|
+
// Supabase MCP — preserving any existing entries.
|
|
19
|
+
// 6. Prints next steps.
|
|
20
|
+
//
|
|
21
|
+
// Zero runtime deps beyond Node built-ins; readline/promises handles
|
|
22
|
+
// the prompt without bringing in inquirer or prompts as a dep.
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const os = require('node:os');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
const readline = require('node:readline/promises');
|
|
30
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
31
|
+
|
|
32
|
+
const ANSI = {
|
|
33
|
+
green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m',
|
|
34
|
+
cyan: '\x1b[36m', magenta: '\x1b[35m', dim: '\x1b[2m', bold: '\x1b[1m',
|
|
35
|
+
reset: '\x1b[0m',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const HOME = os.homedir();
|
|
39
|
+
const MCP_CONFIG = path.join(HOME, '.claude', 'mcp.json');
|
|
40
|
+
|
|
41
|
+
const LAYERS = [
|
|
42
|
+
{
|
|
43
|
+
tier: 1,
|
|
44
|
+
pkg: '@jhizzard/termdeck',
|
|
45
|
+
bin: 'termdeck',
|
|
46
|
+
label: 'TermDeck',
|
|
47
|
+
role: 'Browser terminal multiplexer with metadata overlays, panel theming, and Flashback recall toasts. Tier-1 ready out of the box.',
|
|
48
|
+
required: true,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
tier: 2,
|
|
52
|
+
pkg: '@jhizzard/mnestra',
|
|
53
|
+
bin: 'mnestra',
|
|
54
|
+
label: 'Mnestra',
|
|
55
|
+
role: 'pgvector memory store + MCP server. Lights up Flashback in TermDeck and provides memory_recall / memory_remember tools to Claude Code, Cursor, and Windsurf.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
tier: 3,
|
|
59
|
+
pkg: '@jhizzard/rumen',
|
|
60
|
+
bin: null, // no global bin — used as library + tsx scripts
|
|
61
|
+
label: 'Rumen',
|
|
62
|
+
role: 'Async learning loop. Synthesizes insights across projects on a Supabase Edge Function cron. Surfaces patterns Flashback alone wouldn\'t catch.',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
tier: 4,
|
|
66
|
+
pkg: '@supabase/mcp-server-supabase',
|
|
67
|
+
bin: 'mcp-server-supabase',
|
|
68
|
+
label: 'Supabase MCP',
|
|
69
|
+
role: 'MCP server that lets the TermDeck setup wizard provision your Supabase project automatically — replaces the 4-credential paste step with a project picker.',
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// ── Args ─────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function parseArgs(argv) {
|
|
76
|
+
const out = { tier: null, dryRun: false, help: false, yes: false };
|
|
77
|
+
for (let i = 0; i < argv.length; i++) {
|
|
78
|
+
const a = argv[i];
|
|
79
|
+
if (a === '--tier' && argv[i + 1]) { out.tier = parseInt(argv[++i], 10); continue; }
|
|
80
|
+
if (a.startsWith('--tier=')) { out.tier = parseInt(a.split('=')[1], 10); continue; }
|
|
81
|
+
if (a === '--dry-run') { out.dryRun = true; continue; }
|
|
82
|
+
if (a === '--yes' || a === '-y') { out.yes = true; continue; }
|
|
83
|
+
if (a === '--help' || a === '-h') { out.help = true; continue; }
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function printHelp() {
|
|
89
|
+
process.stdout.write(`
|
|
90
|
+
termdeck-stack — install the TermDeck developer memory stack
|
|
91
|
+
|
|
92
|
+
Usage:
|
|
93
|
+
npx @jhizzard/termdeck-stack Interactive wizard
|
|
94
|
+
npx @jhizzard/termdeck-stack --tier 4 Unattended install (1|2|3|4)
|
|
95
|
+
npx @jhizzard/termdeck-stack --dry-run Print plan, don't install
|
|
96
|
+
npx @jhizzard/termdeck-stack --yes Accept all prompts (combine with --tier)
|
|
97
|
+
|
|
98
|
+
Tiers:
|
|
99
|
+
1 TermDeck only
|
|
100
|
+
2 TermDeck + Mnestra (Flashback works)
|
|
101
|
+
3 + Rumen (async learning)
|
|
102
|
+
4 + Supabase MCP (one-click setup wizard)
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Pretty output helpers ───────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
function box(title) {
|
|
109
|
+
const inner = 65;
|
|
110
|
+
const padded = ` ${title} `.padEnd(inner);
|
|
111
|
+
process.stdout.write(`${ANSI.bold}╔${'═'.repeat(inner)}╗${ANSI.reset}\n`);
|
|
112
|
+
process.stdout.write(`${ANSI.bold}║${padded}║${ANSI.reset}\n`);
|
|
113
|
+
process.stdout.write(`${ANSI.bold}╚${'═'.repeat(inner)}╝${ANSI.reset}\n\n`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function rule() {
|
|
117
|
+
process.stdout.write(`${ANSI.dim}${'─'.repeat(67)}${ANSI.reset}\n`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function statusLine(emoji, label, detail) {
|
|
121
|
+
const padded = label.padEnd(38);
|
|
122
|
+
process.stdout.write(` ${emoji} ${padded}${ANSI.dim}${detail || ''}${ANSI.reset}\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Detection ───────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
function nodeVersion() {
|
|
128
|
+
return process.version.slice(1); // strip leading 'v'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function npmVersion() {
|
|
132
|
+
const r = spawnSync('npm', ['--version'], { encoding: 'utf8' });
|
|
133
|
+
if (r.status !== 0) return null;
|
|
134
|
+
return (r.stdout || '').trim() || null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function detectGlobalPackage(pkg) {
|
|
138
|
+
// `npm ls -g <pkg> --depth=0 --json` — robust across npm versions.
|
|
139
|
+
const r = spawnSync('npm', ['ls', '-g', pkg, '--depth=0', '--json'], { encoding: 'utf8' });
|
|
140
|
+
if (!r.stdout) return null;
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(r.stdout);
|
|
143
|
+
const found = parsed.dependencies && parsed.dependencies[pkg];
|
|
144
|
+
if (found && found.version) return found.version;
|
|
145
|
+
} catch (_e) { /* fall through */ }
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function detectAll() {
|
|
150
|
+
const node = nodeVersion();
|
|
151
|
+
const npm = npmVersion();
|
|
152
|
+
const layers = LAYERS.map((l) => ({
|
|
153
|
+
...l,
|
|
154
|
+
installedVersion: detectGlobalPackage(l.pkg),
|
|
155
|
+
}));
|
|
156
|
+
return { node, npm, layers };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Layered overview ────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
function printOverview() {
|
|
162
|
+
process.stdout.write(`${ANSI.cyan}The TermDeck stack is four packages that compose into a "stateless${ANSI.reset}\n`);
|
|
163
|
+
process.stdout.write(`${ANSI.cyan}LLM, persistent everything else" memory layer for terminal work:${ANSI.reset}\n\n`);
|
|
164
|
+
|
|
165
|
+
for (const l of LAYERS) {
|
|
166
|
+
const tag = l.required ? `${ANSI.bold}required${ANSI.reset}` : 'optional';
|
|
167
|
+
process.stdout.write(` ${ANSI.bold}Layer ${l.tier} (${tag})${ANSI.reset}\n`);
|
|
168
|
+
process.stdout.write(` ${ANSI.green}${l.pkg}${ANSI.reset}\n`);
|
|
169
|
+
|
|
170
|
+
// Word-wrap the role to ~62 cols, indented.
|
|
171
|
+
const words = l.role.split(/\s+/);
|
|
172
|
+
let line = ' ';
|
|
173
|
+
for (const w of words) {
|
|
174
|
+
if (line.length + w.length + 1 > 64) {
|
|
175
|
+
process.stdout.write(`${ANSI.dim}${line}${ANSI.reset}\n`);
|
|
176
|
+
line = ' ' + w;
|
|
177
|
+
} else {
|
|
178
|
+
line += (line.endsWith(' ') ? '' : ' ') + w;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (line.trim().length > 0) process.stdout.write(`${ANSI.dim}${line}${ANSI.reset}\n`);
|
|
182
|
+
process.stdout.write('\n');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function printDetectionTable(detection) {
|
|
187
|
+
process.stdout.write(`${ANSI.bold}Detecting what's already on this machine...${ANSI.reset}\n\n`);
|
|
188
|
+
|
|
189
|
+
if (detection.node) statusLine(`${ANSI.green}✓${ANSI.reset}`, 'Node', `v${detection.node}`);
|
|
190
|
+
else statusLine(`${ANSI.red}✗${ANSI.reset}`, 'Node', 'not detected — install Node 18+ first');
|
|
191
|
+
|
|
192
|
+
if (detection.npm) statusLine(`${ANSI.green}✓${ANSI.reset}`, 'npm', detection.npm);
|
|
193
|
+
else statusLine(`${ANSI.red}✗${ANSI.reset}`, 'npm', 'not detected');
|
|
194
|
+
|
|
195
|
+
process.stdout.write('\n');
|
|
196
|
+
|
|
197
|
+
for (const l of detection.layers) {
|
|
198
|
+
if (l.installedVersion) {
|
|
199
|
+
statusLine(`${ANSI.green}✓${ANSI.reset}`, l.pkg, `v${l.installedVersion} already installed`);
|
|
200
|
+
} else {
|
|
201
|
+
statusLine(`${ANSI.dim}─${ANSI.reset}`, l.pkg, 'not installed');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
process.stdout.write('\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Tier prompt ─────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
async function promptTier({ defaultTier }) {
|
|
210
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
211
|
+
process.stdout.write(`${ANSI.bold}Which tier would you like to install?${ANSI.reset}\n`);
|
|
212
|
+
process.stdout.write(` 1) TermDeck only\n`);
|
|
213
|
+
process.stdout.write(` 2) + Mnestra ${ANSI.dim}(Flashback works)${ANSI.reset}\n`);
|
|
214
|
+
process.stdout.write(` 3) + Rumen ${ANSI.dim}(async learning across projects)${ANSI.reset}\n`);
|
|
215
|
+
process.stdout.write(` 4) + Supabase MCP ${ANSI.dim}(one-click setup wizard)${ANSI.reset}\n\n`);
|
|
216
|
+
while (true) {
|
|
217
|
+
const ans = (await rl.question(` Choice [default ${defaultTier}]: `)).trim();
|
|
218
|
+
if (ans === '') { rl.close(); return defaultTier; }
|
|
219
|
+
const n = parseInt(ans, 10);
|
|
220
|
+
if (n >= 1 && n <= 4) { rl.close(); return n; }
|
|
221
|
+
process.stdout.write(` ${ANSI.red}Please enter 1, 2, 3, or 4.${ANSI.reset}\n`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Install ─────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
function npmInstallGlobal(pkg) {
|
|
228
|
+
return new Promise((resolve) => {
|
|
229
|
+
const child = spawn('npm', ['install', '-g', pkg], { stdio: 'inherit' });
|
|
230
|
+
child.on('exit', (code) => resolve(code === 0));
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function installLayers(plan, opts) {
|
|
235
|
+
process.stdout.write(`\n${ANSI.bold}Installing ${plan.length} package${plan.length === 1 ? '' : 's'}...${ANSI.reset}\n\n`);
|
|
236
|
+
let failures = 0;
|
|
237
|
+
for (let i = 0; i < plan.length; i++) {
|
|
238
|
+
const l = plan[i];
|
|
239
|
+
process.stdout.write(`${ANSI.bold}[${i + 1}/${plan.length}] ${l.pkg}${ANSI.reset}\n`);
|
|
240
|
+
if (opts.dryRun) {
|
|
241
|
+
statusLine(`${ANSI.yellow}↩${ANSI.reset}`, '(dry-run)', `would run: npm install -g ${l.pkg}`);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const ok = await npmInstallGlobal(l.pkg);
|
|
245
|
+
if (ok) statusLine(`${ANSI.green}✓${ANSI.reset}`, l.pkg, 'installed');
|
|
246
|
+
else { statusLine(`${ANSI.red}✗${ANSI.reset}`, l.pkg, 'install failed (continuing)'); failures++; }
|
|
247
|
+
process.stdout.write('\n');
|
|
248
|
+
}
|
|
249
|
+
return failures;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── ~/.claude/mcp.json wiring ───────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
function readMcpConfig() {
|
|
255
|
+
if (!fs.existsSync(MCP_CONFIG)) return { mcpServers: {} };
|
|
256
|
+
try {
|
|
257
|
+
const parsed = JSON.parse(fs.readFileSync(MCP_CONFIG, 'utf8'));
|
|
258
|
+
if (!parsed.mcpServers) parsed.mcpServers = {};
|
|
259
|
+
return parsed;
|
|
260
|
+
} catch (_e) {
|
|
261
|
+
return { mcpServers: {} };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function writeMcpConfig(cfg) {
|
|
266
|
+
fs.mkdirSync(path.dirname(MCP_CONFIG), { recursive: true });
|
|
267
|
+
fs.writeFileSync(MCP_CONFIG, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function wireMcpEntries(plan, opts) {
|
|
271
|
+
if (opts.dryRun) {
|
|
272
|
+
process.stdout.write(`${ANSI.bold}Would wire ~/.claude/mcp.json (dry-run skipped)${ANSI.reset}\n\n`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const cfg = readMcpConfig();
|
|
276
|
+
const installedTiers = new Set(plan.map((l) => l.tier));
|
|
277
|
+
const additions = [];
|
|
278
|
+
const keptExisting = [];
|
|
279
|
+
|
|
280
|
+
if (installedTiers.has(2) && !cfg.mcpServers.mnestra) {
|
|
281
|
+
cfg.mcpServers.mnestra = {
|
|
282
|
+
command: 'mnestra',
|
|
283
|
+
env: {
|
|
284
|
+
SUPABASE_URL: '${SUPABASE_URL}',
|
|
285
|
+
SUPABASE_SERVICE_ROLE_KEY: '${SUPABASE_SERVICE_ROLE_KEY}',
|
|
286
|
+
OPENAI_API_KEY: '${OPENAI_API_KEY}',
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
additions.push('mnestra');
|
|
290
|
+
} else if (cfg.mcpServers.mnestra) {
|
|
291
|
+
keptExisting.push('mnestra');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (installedTiers.has(4) && !cfg.mcpServers.supabase) {
|
|
295
|
+
cfg.mcpServers.supabase = {
|
|
296
|
+
command: 'npx',
|
|
297
|
+
args: ['-y', '@supabase/mcp-server-supabase@latest'],
|
|
298
|
+
env: {
|
|
299
|
+
SUPABASE_ACCESS_TOKEN: 'SUPABASE_PAT_HERE',
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
additions.push('supabase');
|
|
303
|
+
} else if (cfg.mcpServers.supabase) {
|
|
304
|
+
keptExisting.push('supabase');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (additions.length === 0 && keptExisting.length === 0) return;
|
|
308
|
+
|
|
309
|
+
process.stdout.write(`${ANSI.bold}Wiring ~/.claude/mcp.json...${ANSI.reset}\n`);
|
|
310
|
+
for (const name of additions) statusLine(`${ANSI.green}+${ANSI.reset}`, `${name} entry`, 'added');
|
|
311
|
+
for (const name of keptExisting) statusLine(`${ANSI.dim}=${ANSI.reset}`, `${name} entry`, 'already present, kept as-is');
|
|
312
|
+
if (additions.length > 0) writeMcpConfig(cfg);
|
|
313
|
+
process.stdout.write('\n');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── Next steps ──────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
function printNextSteps(plan, opts) {
|
|
319
|
+
rule();
|
|
320
|
+
process.stdout.write(`${ANSI.bold}${ANSI.green}Stack installed.${ANSI.reset}\n\n`);
|
|
321
|
+
|
|
322
|
+
const tiers = new Set(plan.map((l) => l.tier));
|
|
323
|
+
let stepNum = 1;
|
|
324
|
+
|
|
325
|
+
if (tiers.has(4)) {
|
|
326
|
+
process.stdout.write(` ${ANSI.bold}${stepNum++}.${ANSI.reset} Mint a Supabase Personal Access Token at:\n`);
|
|
327
|
+
process.stdout.write(` ${ANSI.cyan}https://supabase.com/dashboard/account/tokens${ANSI.reset}\n`);
|
|
328
|
+
process.stdout.write(` Then edit ${ANSI.dim}${MCP_CONFIG}${ANSI.reset} and replace ${ANSI.yellow}SUPABASE_PAT_HERE${ANSI.reset}.\n\n`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (tiers.has(2) && !tiers.has(4)) {
|
|
332
|
+
process.stdout.write(` ${ANSI.bold}${stepNum++}.${ANSI.reset} Configure Tier 2 (Mnestra) credentials. Two options:\n`);
|
|
333
|
+
process.stdout.write(` • In-browser: run ${ANSI.green}termdeck${ANSI.reset}, click ${ANSI.bold}config${ANSI.reset}, paste credentials in the wizard\n`);
|
|
334
|
+
process.stdout.write(` • CLI: ${ANSI.green}termdeck init --mnestra${ANSI.reset}\n\n`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (tiers.has(3)) {
|
|
338
|
+
process.stdout.write(` ${ANSI.bold}${stepNum++}.${ANSI.reset} Deploy Rumen to your Supabase project:\n`);
|
|
339
|
+
process.stdout.write(` ${ANSI.green}termdeck init --rumen${ANSI.reset}\n\n`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
process.stdout.write(` ${ANSI.bold}${stepNum++}.${ANSI.reset} Start the stack:\n`);
|
|
343
|
+
process.stdout.write(` ${ANSI.green}termdeck${ANSI.reset}\n`);
|
|
344
|
+
if (tiers.has(2)) {
|
|
345
|
+
process.stdout.write(` ${ANSI.dim}(auto-orchestrates Mnestra and surfaces Rumen status from v0.5.0)${ANSI.reset}\n`);
|
|
346
|
+
}
|
|
347
|
+
process.stdout.write('\n');
|
|
348
|
+
|
|
349
|
+
if (opts.dryRun) {
|
|
350
|
+
process.stdout.write(` ${ANSI.yellow}(--dry-run was set; nothing was actually installed.)${ANSI.reset}\n\n`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ── Main ────────────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
async function main(argv) {
|
|
357
|
+
const args = parseArgs(argv);
|
|
358
|
+
if (args.help) { printHelp(); return 0; }
|
|
359
|
+
|
|
360
|
+
process.stdout.write('\n');
|
|
361
|
+
box('TermDeck Stack Installer');
|
|
362
|
+
|
|
363
|
+
printOverview();
|
|
364
|
+
rule();
|
|
365
|
+
process.stdout.write('\n');
|
|
366
|
+
|
|
367
|
+
const detection = detectAll();
|
|
368
|
+
printDetectionTable(detection);
|
|
369
|
+
|
|
370
|
+
if (!detection.node) {
|
|
371
|
+
process.stdout.write(`${ANSI.red}Node 18+ is required. Install Node and re-run this script.${ANSI.reset}\n`);
|
|
372
|
+
return 1;
|
|
373
|
+
}
|
|
374
|
+
if (!detection.npm) {
|
|
375
|
+
process.stdout.write(`${ANSI.red}npm is required. Install npm and re-run this script.${ANSI.reset}\n`);
|
|
376
|
+
return 1;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let tier = args.tier;
|
|
380
|
+
if (!tier) {
|
|
381
|
+
if (args.yes) tier = 4;
|
|
382
|
+
else tier = await promptTier({ defaultTier: 4 });
|
|
383
|
+
}
|
|
384
|
+
if (tier < 1 || tier > 4) {
|
|
385
|
+
process.stdout.write(`${ANSI.red}Invalid tier ${tier}. Must be 1, 2, 3, or 4.${ANSI.reset}\n`);
|
|
386
|
+
return 1;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const wantedLayers = detection.layers.filter((l) => l.tier <= tier);
|
|
390
|
+
const missingLayers = wantedLayers.filter((l) => !l.installedVersion);
|
|
391
|
+
|
|
392
|
+
process.stdout.write(`${ANSI.bold}Plan:${ANSI.reset} install tier ${tier} `);
|
|
393
|
+
if (missingLayers.length === 0) {
|
|
394
|
+
process.stdout.write(`${ANSI.green}— all layers already present.${ANSI.reset}\n\n`);
|
|
395
|
+
} else {
|
|
396
|
+
process.stdout.write(`${ANSI.dim}(${missingLayers.length} of ${wantedLayers.length} layer${wantedLayers.length === 1 ? '' : 's'} missing)${ANSI.reset}\n\n`);
|
|
397
|
+
for (const l of missingLayers) statusLine(`${ANSI.cyan}+${ANSI.reset}`, l.pkg, l.role.split('. ')[0] + '.');
|
|
398
|
+
process.stdout.write('\n');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let failures = 0;
|
|
402
|
+
if (missingLayers.length > 0) failures = await installLayers(missingLayers, { dryRun: args.dryRun });
|
|
403
|
+
|
|
404
|
+
// Wire MCP entries even when nothing was installed — covers the
|
|
405
|
+
// "already had everything but never set up Claude Code MCP" case.
|
|
406
|
+
wireMcpEntries(wantedLayers, { dryRun: args.dryRun });
|
|
407
|
+
|
|
408
|
+
printNextSteps(wantedLayers, { dryRun: args.dryRun });
|
|
409
|
+
|
|
410
|
+
if (failures > 0) {
|
|
411
|
+
process.stdout.write(`${ANSI.yellow}${failures} package${failures === 1 ? '' : 's'} failed to install — re-run after fixing the underlying npm issue.${ANSI.reset}\n\n`);
|
|
412
|
+
return 1;
|
|
413
|
+
}
|
|
414
|
+
return 0;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (require.main === module) {
|
|
418
|
+
main(process.argv.slice(2)).then((code) => process.exit(code || 0)).catch((err) => {
|
|
419
|
+
process.stderr.write(`[termdeck-stack] failed: ${err && err.stack || err}\n`);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
module.exports = main;
|