happy-stacks 0.1.2 → 0.3.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 +164 -89
- package/bin/happys.mjs +70 -10
- package/docs/edison.md +381 -0
- package/docs/happy-development.md +733 -0
- package/docs/menubar.md +54 -0
- package/docs/paths-and-env.md +141 -0
- package/docs/stacks.md +39 -0
- package/extras/swiftbar/auth-login.sh +5 -2
- package/extras/swiftbar/git-cache-refresh.sh +130 -0
- package/extras/swiftbar/happy-stacks.5s.sh +131 -81
- package/extras/swiftbar/happys-term.sh +15 -38
- package/extras/swiftbar/happys.sh +15 -32
- package/extras/swiftbar/install.sh +99 -13
- package/extras/swiftbar/lib/git.sh +309 -1
- package/extras/swiftbar/lib/icons.sh +2 -2
- package/extras/swiftbar/lib/render.sh +209 -80
- package/extras/swiftbar/lib/system.sh +27 -4
- package/extras/swiftbar/lib/utils.sh +311 -28
- package/extras/swiftbar/pnpm.sh +2 -1
- package/extras/swiftbar/set-interval.sh +10 -5
- package/extras/swiftbar/set-server-flavor.sh +11 -2
- package/extras/swiftbar/wt-pr.sh +9 -2
- package/package.json +2 -1
- package/scripts/auth.mjs +521 -226
- package/scripts/build.mjs +29 -10
- package/scripts/cli-link.mjs +6 -6
- package/scripts/completion.mjs +18 -11
- package/scripts/daemon.mjs +133 -31
- package/scripts/dev.mjs +196 -137
- package/scripts/doctor.mjs +44 -55
- package/scripts/edison.mjs +1853 -0
- package/scripts/happy.mjs +10 -25
- package/scripts/init.mjs +46 -31
- package/scripts/install.mjs +21 -15
- package/scripts/lint.mjs +124 -0
- package/scripts/menubar.mjs +76 -10
- package/scripts/migrate.mjs +35 -35
- package/scripts/mobile.mjs +24 -17
- package/scripts/run.mjs +122 -35
- package/scripts/self.mjs +13 -35
- package/scripts/server_flavor.mjs +7 -7
- package/scripts/service.mjs +31 -28
- package/scripts/setup.mjs +694 -0
- package/scripts/setup_pr.mjs +165 -0
- package/scripts/stack.mjs +1851 -363
- package/scripts/stop.mjs +9 -6
- package/scripts/tailscale.mjs +23 -11
- package/scripts/test.mjs +123 -0
- package/scripts/tui.mjs +526 -0
- package/scripts/typecheck.mjs +10 -31
- package/scripts/ui_gateway.mjs +3 -3
- package/scripts/uninstall.mjs +21 -13
- package/scripts/utils/auth/dev_key.mjs +163 -0
- package/scripts/utils/auth/files.mjs +56 -0
- package/scripts/utils/auth/handy_master_secret.mjs +68 -0
- package/scripts/utils/auth/login_ux.mjs +76 -0
- package/scripts/utils/auth/sources.mjs +12 -0
- package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +48 -0
- package/scripts/utils/cli/flags.mjs +17 -0
- package/scripts/utils/cli/normalize.mjs +16 -0
- package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +2 -2
- package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
- package/scripts/utils/crypto/tokens.mjs +14 -0
- package/scripts/utils/dev/daemon.mjs +104 -0
- package/scripts/utils/dev/expo_web.mjs +112 -0
- package/scripts/utils/dev/server.mjs +183 -0
- package/scripts/utils/{config.mjs → env/config.mjs} +8 -3
- package/scripts/utils/{dotenv.mjs → env/dotenv.mjs} +3 -0
- package/scripts/utils/{env.mjs → env/env.mjs} +64 -13
- package/scripts/utils/{env_file.mjs → env/env_file.mjs} +38 -1
- package/scripts/utils/{env_local.mjs → env/env_local.mjs} +1 -0
- package/scripts/utils/env/read.mjs +30 -0
- package/scripts/utils/env/sandbox.mjs +14 -0
- package/scripts/utils/env/values.mjs +13 -0
- package/scripts/utils/{expo.mjs → expo/expo.mjs} +7 -11
- package/scripts/utils/fs/json.mjs +25 -0
- package/scripts/utils/fs/ops.mjs +29 -0
- package/scripts/utils/fs/package_json.mjs +8 -0
- package/scripts/utils/fs/tail.mjs +12 -0
- package/scripts/utils/git/refs.mjs +26 -0
- package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +60 -4
- package/scripts/utils/net/dns.mjs +10 -0
- package/scripts/utils/{ports.mjs → net/ports.mjs} +3 -5
- package/scripts/utils/paths/canonical_home.mjs +20 -0
- package/scripts/utils/paths/localhost_host.mjs +9 -0
- package/scripts/utils/{paths.mjs → paths/paths.mjs} +14 -8
- package/scripts/utils/{runtime.mjs → paths/runtime.mjs} +4 -4
- package/scripts/utils/proc/commands.mjs +34 -0
- package/scripts/utils/proc/ownership.mjs +135 -0
- package/scripts/utils/proc/package_scripts.mjs +31 -0
- package/scripts/utils/proc/pids.mjs +11 -0
- package/scripts/utils/proc/pm.mjs +317 -0
- package/scripts/utils/{proc.mjs → proc/proc.mjs} +30 -2
- package/scripts/utils/proc/watch.mjs +63 -0
- package/scripts/utils/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +109 -94
- package/scripts/utils/server/port.mjs +68 -0
- package/scripts/utils/{server.mjs → server/server.mjs} +36 -0
- package/scripts/utils/server/urls.mjs +91 -0
- package/scripts/utils/{validate.mjs → server/validate.mjs} +1 -1
- package/scripts/utils/service/autostart_darwin.mjs +142 -0
- package/scripts/utils/stack/context.mjs +23 -0
- package/scripts/utils/stack/dirs.mjs +27 -0
- package/scripts/utils/stack/editor_workspace.mjs +152 -0
- package/scripts/utils/stack/names.mjs +12 -0
- package/scripts/utils/stack/runtime_state.mjs +87 -0
- package/scripts/utils/stack/stacks.mjs +45 -0
- package/scripts/utils/stack/startup.mjs +208 -0
- package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +85 -42
- package/scripts/utils/ui/browser.mjs +22 -0
- package/scripts/utils/ui/text.mjs +16 -0
- package/scripts/where.mjs +17 -10
- package/scripts/worktrees.mjs +110 -64
- package/scripts/utils/pm.mjs +0 -303
- /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
- /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
- /package/scripts/utils/{fs.mjs → fs/fs.mjs} +0 -0
package/scripts/tui.mjs
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { join, resolve, sep } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { printResult } from './utils/cli/cli.mjs';
|
|
6
|
+
import { readEnvObjectFromFile } from './utils/env/read.mjs';
|
|
7
|
+
import { getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
8
|
+
import { getStackRuntimeStatePath, readStackRuntimeStateFile } from './utils/stack/runtime_state.mjs';
|
|
9
|
+
import { getEnvValueAny } from './utils/env/values.mjs';
|
|
10
|
+
import { padRight, parsePrefixedLabel, stripAnsi } from './utils/ui/text.mjs';
|
|
11
|
+
|
|
12
|
+
function nowTs() {
|
|
13
|
+
const d = new Date();
|
|
14
|
+
return d.toISOString().slice(11, 19);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function clamp(n, lo, hi) {
|
|
18
|
+
return Math.max(lo, Math.min(hi, n));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function mkPane(id, title, { visible = true, kind = 'log' } = {}) {
|
|
22
|
+
return { id, title, kind, visible, lines: [], scroll: 0 };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function pushLine(pane, line, { maxLines = 4000 } = {}) {
|
|
26
|
+
pane.lines.push(line);
|
|
27
|
+
if (pane.lines.length > maxLines) {
|
|
28
|
+
pane.lines.splice(0, pane.lines.length - maxLines);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function drawBox({ x, y, w, h, title, lines, scroll }) {
|
|
33
|
+
const top = y;
|
|
34
|
+
const bottom = y + h - 1;
|
|
35
|
+
const left = x;
|
|
36
|
+
const horiz = '─'.repeat(Math.max(0, w - 2));
|
|
37
|
+
const t = title ? ` ${title} ` : '';
|
|
38
|
+
const titleStart = Math.max(1, Math.min(w - 2 - t.length, 2));
|
|
39
|
+
const topLine =
|
|
40
|
+
'┌' +
|
|
41
|
+
horiz
|
|
42
|
+
.split('')
|
|
43
|
+
.map((ch, i) => {
|
|
44
|
+
const pos = i + 1;
|
|
45
|
+
if (t && pos >= titleStart && pos < titleStart + t.length) {
|
|
46
|
+
return t[pos - titleStart];
|
|
47
|
+
}
|
|
48
|
+
return ch;
|
|
49
|
+
})
|
|
50
|
+
.join('') +
|
|
51
|
+
'┐';
|
|
52
|
+
|
|
53
|
+
const midLine = '│' + ' '.repeat(Math.max(0, w - 2)) + '│';
|
|
54
|
+
const botLine = '└' + horiz + '┘';
|
|
55
|
+
|
|
56
|
+
const out = [];
|
|
57
|
+
out.push({ row: top, col: left, text: topLine });
|
|
58
|
+
for (let r = top + 1; r < bottom; r++) {
|
|
59
|
+
out.push({ row: r, col: left, text: midLine });
|
|
60
|
+
}
|
|
61
|
+
out.push({ row: bottom, col: left, text: botLine });
|
|
62
|
+
|
|
63
|
+
const innerW = Math.max(0, w - 2);
|
|
64
|
+
const innerH = Math.max(0, h - 2);
|
|
65
|
+
const maxScroll = Math.max(0, lines.length - innerH);
|
|
66
|
+
const s = clamp(scroll, 0, maxScroll);
|
|
67
|
+
const start = Math.max(0, lines.length - innerH - s);
|
|
68
|
+
const slice = lines.slice(start, start + innerH);
|
|
69
|
+
for (let i = 0; i < innerH; i++) {
|
|
70
|
+
const line = stripAnsi(slice[i] ?? '');
|
|
71
|
+
out.push({ row: top + 1 + i, col: left + 1, text: padRight(line, innerW) });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { out, maxScroll };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isTuiHelp(argv) {
|
|
78
|
+
if (!argv.length) return true;
|
|
79
|
+
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === 'help')) return true;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function inferStackNameFromForwardedArgs(args) {
|
|
84
|
+
// Primary: stack-scoped usage: `happys tui stack <subcmd> <name> ...`
|
|
85
|
+
const i = args.indexOf('stack');
|
|
86
|
+
if (i >= 0) {
|
|
87
|
+
const name = args[i + 2];
|
|
88
|
+
if (name && !name.startsWith('-')) return name;
|
|
89
|
+
}
|
|
90
|
+
// Fallback: use current environment stack (or main).
|
|
91
|
+
return (process.env.HAPPY_STACKS_STACK ?? process.env.HAPPY_LOCAL_STACK ?? '').trim() || 'main';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const readEnvObject = readEnvObjectFromFile;
|
|
95
|
+
|
|
96
|
+
function formatComponentRef({ rootDir, component, dir }) {
|
|
97
|
+
const raw = String(dir ?? '').trim();
|
|
98
|
+
if (!raw) return '(unset)';
|
|
99
|
+
|
|
100
|
+
const abs = resolve(raw);
|
|
101
|
+
const defaultDir = resolve(join(rootDir, 'components', component));
|
|
102
|
+
const worktreesPrefix = resolve(join(rootDir, 'components', '.worktrees', component)) + sep;
|
|
103
|
+
|
|
104
|
+
if (abs === defaultDir) return 'default';
|
|
105
|
+
if (abs.startsWith(worktreesPrefix)) {
|
|
106
|
+
return abs.slice(worktreesPrefix.length);
|
|
107
|
+
}
|
|
108
|
+
return abs;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function buildStackSummaryLines({ rootDir, stackName }) {
|
|
112
|
+
const { envPath, baseDir } = resolveStackEnvPath(stackName);
|
|
113
|
+
const env = await readEnvObject(envPath);
|
|
114
|
+
const runtimePath = getStackRuntimeStatePath(stackName);
|
|
115
|
+
const runtime = await readStackRuntimeStateFile(runtimePath);
|
|
116
|
+
|
|
117
|
+
const serverComponent =
|
|
118
|
+
getEnvValueAny(env, ['HAPPY_STACKS_SERVER_COMPONENT', 'HAPPY_LOCAL_SERVER_COMPONENT']) || 'happy-server-light';
|
|
119
|
+
|
|
120
|
+
const ports = runtime?.ports && typeof runtime.ports === 'object' ? runtime.ports : {};
|
|
121
|
+
const expoWebPort = runtime?.expo && typeof runtime.expo === 'object' ? runtime.expo.webPort : null;
|
|
122
|
+
const processes = runtime?.processes && typeof runtime.processes === 'object' ? runtime.processes : {};
|
|
123
|
+
|
|
124
|
+
const components = [
|
|
125
|
+
{ key: 'happy', envKey: 'HAPPY_STACKS_COMPONENT_DIR_HAPPY', legacyKey: 'HAPPY_LOCAL_COMPONENT_DIR_HAPPY' },
|
|
126
|
+
{ key: 'happy-cli', envKey: 'HAPPY_STACKS_COMPONENT_DIR_HAPPY_CLI', legacyKey: 'HAPPY_LOCAL_COMPONENT_DIR_HAPPY_CLI' },
|
|
127
|
+
{
|
|
128
|
+
key: serverComponent === 'happy-server' ? 'happy-server' : 'happy-server-light',
|
|
129
|
+
envKey:
|
|
130
|
+
serverComponent === 'happy-server'
|
|
131
|
+
? 'HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER'
|
|
132
|
+
: 'HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER_LIGHT',
|
|
133
|
+
legacyKey:
|
|
134
|
+
serverComponent === 'happy-server'
|
|
135
|
+
? 'HAPPY_LOCAL_COMPONENT_DIR_HAPPY_SERVER'
|
|
136
|
+
: 'HAPPY_LOCAL_COMPONENT_DIR_HAPPY_SERVER_LIGHT',
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const lines = [];
|
|
141
|
+
lines.push(`stack: ${stackName}`);
|
|
142
|
+
lines.push(`server: ${serverComponent}`);
|
|
143
|
+
lines.push(`baseDir: ${baseDir}`);
|
|
144
|
+
lines.push(`env: ${envPath}`);
|
|
145
|
+
lines.push(`runtime: ${runtimePath}${runtime ? '' : ' (missing)'}`);
|
|
146
|
+
if (runtime?.startedAt) lines.push(`startedAt: ${runtime.startedAt}`);
|
|
147
|
+
if (runtime?.updatedAt) lines.push(`updatedAt: ${runtime.updatedAt}`);
|
|
148
|
+
if (runtime?.ownerPid) lines.push(`ownerPid: ${runtime.ownerPid}`);
|
|
149
|
+
|
|
150
|
+
lines.push('');
|
|
151
|
+
lines.push('ports:');
|
|
152
|
+
lines.push(` server: ${ports?.server ?? '(unknown)'}`);
|
|
153
|
+
if (expoWebPort) lines.push(` ui: ${expoWebPort}`);
|
|
154
|
+
if (ports?.backend) lines.push(` backend: ${ports.backend}`);
|
|
155
|
+
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push('pids:');
|
|
158
|
+
if (processes?.serverPid) lines.push(` serverPid: ${processes.serverPid}`);
|
|
159
|
+
if (processes?.expoWebPid) lines.push(` expoWebPid: ${processes.expoWebPid}`);
|
|
160
|
+
if (processes?.daemonPid) lines.push(` daemonPid: ${processes.daemonPid}`);
|
|
161
|
+
if (processes?.uiGatewayPid) lines.push(` uiGatewayPid: ${processes.uiGatewayPid}`);
|
|
162
|
+
|
|
163
|
+
lines.push('');
|
|
164
|
+
lines.push('components:');
|
|
165
|
+
for (const c of components) {
|
|
166
|
+
const dir = getEnvVal(env, c.envKey, c.legacyKey);
|
|
167
|
+
lines.push(` ${padRight(c.key, 16)} ${formatComponentRef({ rootDir, component: c.key, dir })}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return lines;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function main() {
|
|
174
|
+
const argv = process.argv.slice(2);
|
|
175
|
+
|
|
176
|
+
if (isTuiHelp(argv)) {
|
|
177
|
+
printResult({
|
|
178
|
+
json: false,
|
|
179
|
+
data: { usage: 'happys tui <happys args...>', json: false },
|
|
180
|
+
text: [
|
|
181
|
+
'[tui] usage:',
|
|
182
|
+
' happys tui <happys args...>',
|
|
183
|
+
'',
|
|
184
|
+
'examples:',
|
|
185
|
+
' happys tui stack dev resume-upstream',
|
|
186
|
+
' happys tui stack start resume-upstream',
|
|
187
|
+
' happys tui stack auth dev-auth login',
|
|
188
|
+
'',
|
|
189
|
+
'layouts:',
|
|
190
|
+
' single : one pane (focused)',
|
|
191
|
+
' split : two panes (left=orchestration, right=focused)',
|
|
192
|
+
' columns : multiple panes stacked in two columns (toggle visibility per pane)',
|
|
193
|
+
'',
|
|
194
|
+
'keys:',
|
|
195
|
+
' tab / shift+tab : focus next/prev (visible panes only)',
|
|
196
|
+
' 1..9 : jump to pane index',
|
|
197
|
+
' v : cycle layout (single → split → columns)',
|
|
198
|
+
' m : toggle focused pane visibility (columns layout)',
|
|
199
|
+
' c : clear focused pane',
|
|
200
|
+
' p : pause/resume rendering',
|
|
201
|
+
' ↑/↓, PgUp/PgDn : scroll focused pane',
|
|
202
|
+
' Home/End : jump bottom/top (focused pane)',
|
|
203
|
+
' q / Ctrl+C : quit (sends SIGINT to child)',
|
|
204
|
+
'',
|
|
205
|
+
'panes (default):',
|
|
206
|
+
' orchestration | summary | local | server | ui | daemon | stack logs',
|
|
207
|
+
].join('\n'),
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
213
|
+
throw new Error('[tui] requires a TTY (interactive terminal)');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const rootDir = getRootDir(import.meta.url);
|
|
217
|
+
const happysBin = join(rootDir, 'bin', 'happys.mjs');
|
|
218
|
+
const forwarded = argv;
|
|
219
|
+
|
|
220
|
+
const stackName = inferStackNameFromForwardedArgs(forwarded);
|
|
221
|
+
|
|
222
|
+
const panes = [
|
|
223
|
+
mkPane('orch', 'orchestration', { visible: true, kind: 'log' }),
|
|
224
|
+
mkPane('summary', `stack summary (${stackName})`, { visible: true, kind: 'summary' }),
|
|
225
|
+
mkPane('local', 'local', { visible: true, kind: 'log' }),
|
|
226
|
+
mkPane('server', 'server', { visible: true, kind: 'log' }),
|
|
227
|
+
mkPane('ui', 'ui', { visible: true, kind: 'log' }),
|
|
228
|
+
mkPane('daemon', 'daemon', { visible: true, kind: 'log' }),
|
|
229
|
+
mkPane('stacklog', 'stack logs', { visible: true, kind: 'log' }),
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
const paneIndexById = new Map(panes.map((p, i) => [p.id, i]));
|
|
233
|
+
|
|
234
|
+
const routeLine = (line) => {
|
|
235
|
+
const label = parsePrefixedLabel(line);
|
|
236
|
+
const normalized = label ? label.toLowerCase() : '';
|
|
237
|
+
|
|
238
|
+
let paneId = 'local';
|
|
239
|
+
if (normalized.includes('server')) paneId = 'server';
|
|
240
|
+
else if (normalized === 'ui') paneId = 'ui';
|
|
241
|
+
else if (normalized.includes('daemon')) paneId = 'daemon';
|
|
242
|
+
else if (normalized === 'stack') paneId = 'stacklog';
|
|
243
|
+
else if (normalized === 'local') paneId = 'local';
|
|
244
|
+
|
|
245
|
+
const idx = paneIndexById.get(paneId) ?? paneIndexById.get('local');
|
|
246
|
+
pushLine(panes[idx], line);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const logOrch = (msg) => {
|
|
250
|
+
pushLine(panes[paneIndexById.get('orch')], `[${nowTs()}] ${msg}`);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
let layout = 'columns'; // single | split | columns
|
|
254
|
+
let focused = 2; // local
|
|
255
|
+
let paused = false;
|
|
256
|
+
let renderScheduled = false;
|
|
257
|
+
|
|
258
|
+
const child = spawn(process.execPath, [happysBin, ...forwarded], {
|
|
259
|
+
cwd: rootDir,
|
|
260
|
+
env: { ...process.env },
|
|
261
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
262
|
+
detached: process.platform !== 'win32',
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
logOrch(`spawned: node ${happysBin} ${forwarded.join(' ')} (pid=${child.pid})`);
|
|
266
|
+
|
|
267
|
+
const buf = { out: '', err: '' };
|
|
268
|
+
const flush = (kind) => {
|
|
269
|
+
const key = kind === 'stderr' ? 'err' : 'out';
|
|
270
|
+
let b = buf[key];
|
|
271
|
+
while (true) {
|
|
272
|
+
const idx = b.indexOf('\n');
|
|
273
|
+
if (idx < 0) break;
|
|
274
|
+
const line = b.slice(0, idx);
|
|
275
|
+
b = b.slice(idx + 1);
|
|
276
|
+
routeLine(line);
|
|
277
|
+
}
|
|
278
|
+
buf[key] = b;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
child.stdout?.on('data', (d) => {
|
|
282
|
+
buf.out += d.toString();
|
|
283
|
+
flush('stdout');
|
|
284
|
+
scheduleRender();
|
|
285
|
+
});
|
|
286
|
+
child.stderr?.on('data', (d) => {
|
|
287
|
+
buf.err += d.toString();
|
|
288
|
+
flush('stderr');
|
|
289
|
+
scheduleRender();
|
|
290
|
+
});
|
|
291
|
+
child.on('exit', (code, sig) => {
|
|
292
|
+
logOrch(`child exited (code=${code}, sig=${sig ?? 'null'})`);
|
|
293
|
+
scheduleRender();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
async function refreshSummary() {
|
|
297
|
+
const idx = paneIndexById.get('summary');
|
|
298
|
+
try {
|
|
299
|
+
const lines = await buildStackSummaryLines({ rootDir, stackName });
|
|
300
|
+
panes[idx].lines = lines;
|
|
301
|
+
} catch (e) {
|
|
302
|
+
panes[idx].lines = [`summary error: ${e instanceof Error ? e.message : String(e)}`];
|
|
303
|
+
}
|
|
304
|
+
scheduleRender();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const summaryTimer = setInterval(() => {
|
|
308
|
+
if (!paused) {
|
|
309
|
+
void refreshSummary();
|
|
310
|
+
}
|
|
311
|
+
}, 1000);
|
|
312
|
+
|
|
313
|
+
function scheduleRender() {
|
|
314
|
+
if (paused) return;
|
|
315
|
+
if (renderScheduled) return;
|
|
316
|
+
renderScheduled = true;
|
|
317
|
+
setTimeout(() => {
|
|
318
|
+
renderScheduled = false;
|
|
319
|
+
render();
|
|
320
|
+
}, 16);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function visiblePaneIndexes() {
|
|
324
|
+
return panes
|
|
325
|
+
.map((p, idx) => ({ p, idx }))
|
|
326
|
+
.filter(({ p }) => p.visible)
|
|
327
|
+
.map(({ idx }) => idx);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function focusNext(delta) {
|
|
331
|
+
const visible = visiblePaneIndexes();
|
|
332
|
+
if (!visible.length) return;
|
|
333
|
+
const pos = Math.max(0, visible.indexOf(focused));
|
|
334
|
+
const next = (pos + delta + visible.length) % visible.length;
|
|
335
|
+
focused = visible[next];
|
|
336
|
+
scheduleRender();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function scrollFocused(delta) {
|
|
340
|
+
const pane = panes[focused];
|
|
341
|
+
pane.scroll = Math.max(0, pane.scroll + delta);
|
|
342
|
+
scheduleRender();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function clearFocused() {
|
|
346
|
+
const pane = panes[focused];
|
|
347
|
+
if (pane.kind === 'summary') return;
|
|
348
|
+
pane.lines = [];
|
|
349
|
+
pane.scroll = 0;
|
|
350
|
+
scheduleRender();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function cycleLayout() {
|
|
354
|
+
layout = layout === 'single' ? 'split' : layout === 'split' ? 'columns' : 'single';
|
|
355
|
+
scheduleRender();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function toggleFocusedVisibility() {
|
|
359
|
+
if (layout !== 'columns') return;
|
|
360
|
+
const pane = panes[focused];
|
|
361
|
+
if (pane.id === 'orch') return; // always visible
|
|
362
|
+
pane.visible = !pane.visible;
|
|
363
|
+
if (!pane.visible) {
|
|
364
|
+
// Move focus to next visible pane.
|
|
365
|
+
focusNext(+1);
|
|
366
|
+
}
|
|
367
|
+
scheduleRender();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function render() {
|
|
371
|
+
if (paused) return;
|
|
372
|
+
const cols = process.stdout.columns ?? 120;
|
|
373
|
+
const rows = process.stdout.rows ?? 40;
|
|
374
|
+
process.stdout.write('\x1b[?25l');
|
|
375
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
376
|
+
|
|
377
|
+
const header = `happys tui | ${forwarded.join(' ')} | layout=${layout} | focus=${panes[focused]?.title ?? focused}`;
|
|
378
|
+
process.stdout.write(padRight(header, cols) + '\n');
|
|
379
|
+
|
|
380
|
+
const bodyY = 1;
|
|
381
|
+
const bodyH = rows - 2;
|
|
382
|
+
const footerY = rows - 1;
|
|
383
|
+
|
|
384
|
+
const drawWrites = [];
|
|
385
|
+
if (layout === 'single') {
|
|
386
|
+
const pane = panes[focused];
|
|
387
|
+
const box = drawBox({ x: 0, y: bodyY, w: cols, h: bodyH, title: pane.title, lines: pane.lines, scroll: pane.scroll });
|
|
388
|
+
pane.scroll = clamp(pane.scroll, 0, box.maxScroll);
|
|
389
|
+
drawWrites.push(...box.out);
|
|
390
|
+
} else if (layout === 'split') {
|
|
391
|
+
const leftW = Math.floor(cols / 2);
|
|
392
|
+
const rightW = cols - leftW;
|
|
393
|
+
|
|
394
|
+
const leftPane = panes[paneIndexById.get('orch')];
|
|
395
|
+
const rightPane = panes[focused === paneIndexById.get('orch') ? paneIndexById.get('local') : focused];
|
|
396
|
+
|
|
397
|
+
const leftBox = drawBox({ x: 0, y: bodyY, w: leftW, h: bodyH, title: leftPane.title, lines: leftPane.lines, scroll: leftPane.scroll });
|
|
398
|
+
leftPane.scroll = clamp(leftPane.scroll, 0, leftBox.maxScroll);
|
|
399
|
+
drawWrites.push(...leftBox.out);
|
|
400
|
+
|
|
401
|
+
const rightBox = drawBox({ x: leftW, y: bodyY, w: rightW, h: bodyH, title: rightPane.title, lines: rightPane.lines, scroll: rightPane.scroll });
|
|
402
|
+
rightPane.scroll = clamp(rightPane.scroll, 0, rightBox.maxScroll);
|
|
403
|
+
drawWrites.push(...rightBox.out);
|
|
404
|
+
} else {
|
|
405
|
+
// columns: render all visible panes in two columns, stacked.
|
|
406
|
+
const visible = visiblePaneIndexes().map((idx) => panes[idx]);
|
|
407
|
+
const leftW = Math.floor(cols / 2);
|
|
408
|
+
const rightW = cols - leftW;
|
|
409
|
+
|
|
410
|
+
const leftPanes = [];
|
|
411
|
+
const rightPanes = [];
|
|
412
|
+
for (let i = 0; i < visible.length; i++) {
|
|
413
|
+
(i % 2 === 0 ? leftPanes : rightPanes).push(visible[i]);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const layoutColumn = (colX, colW, colPanes) => {
|
|
417
|
+
if (!colPanes.length) return;
|
|
418
|
+
const n = colPanes.length;
|
|
419
|
+
const base = Math.max(3, Math.floor(bodyH / n));
|
|
420
|
+
let y = bodyY;
|
|
421
|
+
for (let i = 0; i < n; i++) {
|
|
422
|
+
const pane = colPanes[i];
|
|
423
|
+
const remaining = bodyY + bodyH - y;
|
|
424
|
+
const h = i === n - 1 ? remaining : Math.min(base, remaining);
|
|
425
|
+
if (h < 3) break;
|
|
426
|
+
const box = drawBox({ x: colX, y, w: colW, h, title: pane.title, lines: pane.lines, scroll: pane.scroll });
|
|
427
|
+
pane.scroll = clamp(pane.scroll, 0, box.maxScroll);
|
|
428
|
+
drawWrites.push(...box.out);
|
|
429
|
+
y += h;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
layoutColumn(0, leftW, leftPanes);
|
|
434
|
+
layoutColumn(leftW, rightW, rightPanes);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
for (const w of drawWrites) {
|
|
438
|
+
process.stdout.write(`\x1b[${w.row + 1};${w.col + 1}H${w.text}`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const footer =
|
|
442
|
+
'tab:next shift+tab:prev 1..9:jump v:layout m:toggle-pane c:clear p:pause arrows:scroll q/Ctrl+C:quit';
|
|
443
|
+
process.stdout.write(`\x1b[${footerY + 1};1H` + padRight(footer, cols));
|
|
444
|
+
process.stdout.write('\x1b[?25h');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function shutdown() {
|
|
448
|
+
clearInterval(summaryTimer);
|
|
449
|
+
try {
|
|
450
|
+
process.stdin.setRawMode(false);
|
|
451
|
+
} catch {
|
|
452
|
+
// ignore
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
process.stdin.pause();
|
|
456
|
+
} catch {
|
|
457
|
+
// ignore
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
if (child.exitCode == null && child.pid) {
|
|
461
|
+
if (process.platform !== 'win32') process.kill(-child.pid, 'SIGINT');
|
|
462
|
+
else child.kill('SIGINT');
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
// ignore
|
|
466
|
+
}
|
|
467
|
+
process.stdout.write('\x1b[2J\x1b[H\x1b[?25h');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
process.stdin.setRawMode(true);
|
|
471
|
+
process.stdin.resume();
|
|
472
|
+
process.stdin.on('data', (d) => {
|
|
473
|
+
const s = d.toString('utf-8');
|
|
474
|
+
if (s === '\u0003' || s === 'q') {
|
|
475
|
+
shutdown();
|
|
476
|
+
process.exit(0);
|
|
477
|
+
}
|
|
478
|
+
if (s === '\t') return focusNext(+1);
|
|
479
|
+
if (s === '\x1b[Z') return focusNext(-1);
|
|
480
|
+
if (s >= '1' && s <= '9') {
|
|
481
|
+
const idx = Number(s) - 1;
|
|
482
|
+
if (idx >= 0 && idx < panes.length) {
|
|
483
|
+
if (panes[idx].visible) {
|
|
484
|
+
focused = idx;
|
|
485
|
+
scheduleRender();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (s === 'v') return cycleLayout();
|
|
491
|
+
if (s === 'm') return toggleFocusedVisibility();
|
|
492
|
+
if (s === 'c') return clearFocused();
|
|
493
|
+
if (s === 'p') {
|
|
494
|
+
paused = !paused;
|
|
495
|
+
if (!paused) {
|
|
496
|
+
void refreshSummary();
|
|
497
|
+
scheduleRender();
|
|
498
|
+
}
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (s === '\x1b[A') return scrollFocused(+1);
|
|
503
|
+
if (s === '\x1b[B') return scrollFocused(-1);
|
|
504
|
+
if (s === '\x1b[5~') return scrollFocused(+10);
|
|
505
|
+
if (s === '\x1b[6~') return scrollFocused(-10);
|
|
506
|
+
if (s === '\x1b[H') {
|
|
507
|
+
panes[focused].scroll = 1000000;
|
|
508
|
+
scheduleRender();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (s === '\x1b[F') {
|
|
512
|
+
panes[focused].scroll = 0;
|
|
513
|
+
scheduleRender();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
await refreshSummary();
|
|
519
|
+
render();
|
|
520
|
+
await new Promise(() => {});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
main().catch((err) => {
|
|
524
|
+
console.error('[tui] failed:', err);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
});
|
package/scripts/typecheck.mjs
CHANGED
|
@@ -1,36 +1,15 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
2
|
-
import { parseArgs } from './utils/args.mjs';
|
|
3
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
4
|
-
import { getComponentDir, getRootDir } from './utils/paths.mjs';
|
|
5
|
-
import { ensureDepsInstalled
|
|
6
|
-
import { pathExists } from './utils/fs.mjs';
|
|
7
|
-
import { run } from './utils/proc.mjs';
|
|
8
|
-
import {
|
|
9
|
-
import { readFile } from 'node:fs/promises';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
4
|
+
import { getComponentDir, getRootDir } from './utils/paths/paths.mjs';
|
|
5
|
+
import { ensureDepsInstalled } from './utils/proc/pm.mjs';
|
|
6
|
+
import { pathExists } from './utils/fs/fs.mjs';
|
|
7
|
+
import { run } from './utils/proc/proc.mjs';
|
|
8
|
+
import { detectPackageManagerCmd, pickFirstScript, readPackageJsonScripts } from './utils/proc/package_scripts.mjs';
|
|
10
9
|
|
|
11
10
|
const DEFAULT_COMPONENTS = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
|
|
12
11
|
|
|
13
|
-
async function detectPackageManagerCmd(dir) {
|
|
14
|
-
if (await pathExists(join(dir, 'yarn.lock'))) {
|
|
15
|
-
return { name: 'yarn', cmd: 'yarn', argsForScript: (script) => ['-s', script] };
|
|
16
|
-
}
|
|
17
|
-
await requirePnpm();
|
|
18
|
-
return { name: 'pnpm', cmd: 'pnpm', argsForScript: (script) => ['--silent', script] };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function readScripts(dir) {
|
|
22
|
-
try {
|
|
23
|
-
const raw = await readFile(join(dir, 'package.json'), 'utf-8');
|
|
24
|
-
const pkg = JSON.parse(raw);
|
|
25
|
-
const scripts = pkg?.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
|
|
26
|
-
return scripts;
|
|
27
|
-
} catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
12
|
function pickTypecheckScript(scripts) {
|
|
33
|
-
if (!scripts) return null;
|
|
34
13
|
const candidates = [
|
|
35
14
|
'typecheck',
|
|
36
15
|
'type-check',
|
|
@@ -39,7 +18,7 @@ function pickTypecheckScript(scripts) {
|
|
|
39
18
|
'tsc',
|
|
40
19
|
'typescript',
|
|
41
20
|
];
|
|
42
|
-
return
|
|
21
|
+
return pickFirstScript(scripts, candidates);
|
|
43
22
|
}
|
|
44
23
|
|
|
45
24
|
async function main() {
|
|
@@ -86,7 +65,7 @@ async function main() {
|
|
|
86
65
|
continue;
|
|
87
66
|
}
|
|
88
67
|
|
|
89
|
-
const scripts = await
|
|
68
|
+
const scripts = await readPackageJsonScripts(dir);
|
|
90
69
|
if (!scripts) {
|
|
91
70
|
results.push({ component, ok: true, skipped: true, dir, reason: 'no package.json' });
|
|
92
71
|
continue;
|
package/scripts/ui_gateway.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import http from 'node:http';
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
import { extname, resolve, sep } from 'node:path';
|
|
5
5
|
import { readFile, stat } from 'node:fs/promises';
|
|
6
6
|
|
|
7
|
-
import { parseArgs } from './utils/args.mjs';
|
|
8
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
7
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
8
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
9
9
|
|
|
10
10
|
function usage() {
|
|
11
11
|
return [
|
package/scripts/uninstall.mjs
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
|
|
3
3
|
import { rm } from 'node:fs/promises';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
|
-
import { homedir } from 'node:os';
|
|
6
5
|
import { join } from 'node:path';
|
|
7
6
|
import { spawnSync } from 'node:child_process';
|
|
8
7
|
|
|
9
|
-
import { parseArgs } from './utils/args.mjs';
|
|
10
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
8
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
9
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
10
|
+
import { expandHome } from './utils/paths/canonical_home.mjs';
|
|
11
|
+
import { getHappyStacksHomeDir, getRootDir, getStacksStorageRoot } from './utils/paths/paths.mjs';
|
|
12
|
+
import { getRuntimeDir } from './utils/paths/runtime.mjs';
|
|
13
|
+
import { getCanonicalHomeEnvPath } from './utils/env/config.mjs';
|
|
14
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
17
15
|
|
|
18
16
|
function resolveWorkspaceDir({ rootDir, homeDir }) {
|
|
19
17
|
// Uninstall should never default to deleting the repo root (getWorkspaceDir() can fall back to cliRootDir).
|
|
@@ -69,13 +67,14 @@ async function main() {
|
|
|
69
67
|
if (wantsHelp(argv, { flags }) || argv.includes('help')) {
|
|
70
68
|
printResult({
|
|
71
69
|
json,
|
|
72
|
-
data: { flags: ['--remove-workspace', '--remove-stacks', '--yes'], json: true },
|
|
70
|
+
data: { flags: ['--remove-workspace', '--remove-stacks', '--yes', '--global'], json: true },
|
|
73
71
|
text: [
|
|
74
72
|
'[uninstall] usage:',
|
|
75
73
|
' happys uninstall [--json] # dry-run',
|
|
76
74
|
' happys uninstall --yes [--json]',
|
|
77
75
|
' happys uninstall --remove-workspace --yes',
|
|
78
76
|
' happys uninstall --remove-stacks --yes',
|
|
77
|
+
' happys uninstall --global --yes # also remove global OS integrations (services/SwiftBar) even in sandbox mode',
|
|
79
78
|
'',
|
|
80
79
|
'notes:',
|
|
81
80
|
' - default removes: runtime, shims, cache, SwiftBar assets + plugin files, and LaunchAgent services',
|
|
@@ -93,11 +92,12 @@ async function main() {
|
|
|
93
92
|
const yes = flags.has('--yes');
|
|
94
93
|
const removeWorkspace = flags.has('--remove-workspace');
|
|
95
94
|
const removeStacks = flags.has('--remove-stacks');
|
|
95
|
+
const allowGlobal = flags.has('--global') || sandboxAllowsGlobalSideEffects();
|
|
96
96
|
|
|
97
97
|
const dryRun = !yes;
|
|
98
98
|
|
|
99
99
|
// 1) Stop/uninstall services best-effort.
|
|
100
|
-
if (!dryRun) {
|
|
100
|
+
if (!dryRun && (!isSandboxed() || allowGlobal)) {
|
|
101
101
|
try {
|
|
102
102
|
spawnSync(process.execPath, [join(rootDir, 'scripts', 'service.mjs'), 'uninstall'], {
|
|
103
103
|
stdio: json ? 'ignore' : 'inherit',
|
|
@@ -110,9 +110,15 @@ async function main() {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// 2) Remove SwiftBar plugin files best-effort.
|
|
113
|
-
const menubar =
|
|
113
|
+
const menubar =
|
|
114
|
+
isSandboxed() && !allowGlobal
|
|
115
|
+
? { ok: true, removed: 0, pluginsDir: null, skipped: 'sandbox' }
|
|
116
|
+
: dryRun
|
|
117
|
+
? { ok: true, removed: 0, pluginsDir: resolveSwiftbarPluginsDir() }
|
|
118
|
+
: await removeSwiftbarPluginFiles().catch(() => ({ ok: false, removed: 0, pluginsDir: null }));
|
|
114
119
|
|
|
115
120
|
// 3) Remove home-managed runtime + shims + extras + cache + env pointers.
|
|
121
|
+
const canonicalEnv = getCanonicalHomeEnvPath();
|
|
116
122
|
const toRemove = [
|
|
117
123
|
join(homeDir, 'bin'),
|
|
118
124
|
join(homeDir, 'runtime'),
|
|
@@ -120,6 +126,8 @@ async function main() {
|
|
|
120
126
|
join(homeDir, 'cache'),
|
|
121
127
|
join(homeDir, '.env'),
|
|
122
128
|
join(homeDir, 'env.local'),
|
|
129
|
+
// Stable pointer file (can differ from homeDir for custom installs).
|
|
130
|
+
canonicalEnv,
|
|
123
131
|
];
|
|
124
132
|
const removedPaths = [];
|
|
125
133
|
for (const p of toRemove) {
|