happy-stacks 0.1.0 → 0.2.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 +130 -74
- package/bin/happys.mjs +140 -9
- 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/server-flavors.md +61 -2
- package/docs/stacks.md +55 -4
- package/extras/swiftbar/auth-login.sh +10 -7
- package/extras/swiftbar/git-cache-refresh.sh +130 -0
- package/extras/swiftbar/happy-stacks.5s.sh +175 -83
- package/extras/swiftbar/happys-term.sh +128 -0
- package/extras/swiftbar/happys.sh +35 -0
- 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 +279 -132
- package/extras/swiftbar/lib/system.sh +64 -10
- package/extras/swiftbar/lib/utils.sh +469 -10
- package/extras/swiftbar/pnpm-term.sh +2 -122
- package/extras/swiftbar/pnpm.sh +4 -14
- package/extras/swiftbar/set-interval.sh +10 -5
- package/extras/swiftbar/set-server-flavor.sh +19 -10
- package/extras/swiftbar/wt-pr.sh +10 -3
- package/package.json +2 -1
- package/scripts/auth.mjs +833 -14
- package/scripts/build.mjs +24 -4
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +15 -8
- package/scripts/daemon.mjs +200 -23
- package/scripts/dev.mjs +230 -57
- package/scripts/doctor.mjs +26 -21
- package/scripts/edison.mjs +1828 -0
- package/scripts/happy.mjs +3 -7
- package/scripts/init.mjs +275 -46
- package/scripts/install.mjs +14 -8
- package/scripts/lint.mjs +145 -0
- package/scripts/menubar.mjs +81 -8
- package/scripts/migrate.mjs +302 -0
- package/scripts/mobile.mjs +59 -21
- package/scripts/run.mjs +222 -43
- package/scripts/self.mjs +3 -7
- package/scripts/server_flavor.mjs +3 -3
- package/scripts/service.mjs +190 -38
- package/scripts/setup.mjs +790 -0
- package/scripts/setup_pr.mjs +182 -0
- package/scripts/stack.mjs +2273 -92
- package/scripts/stop.mjs +160 -0
- package/scripts/tailscale.mjs +164 -23
- package/scripts/test.mjs +144 -0
- package/scripts/tui.mjs +556 -0
- package/scripts/typecheck.mjs +145 -0
- package/scripts/ui_gateway.mjs +248 -0
- package/scripts/uninstall.mjs +21 -13
- package/scripts/utils/auth_files.mjs +58 -0
- package/scripts/utils/auth_login_ux.mjs +76 -0
- package/scripts/utils/auth_sources.mjs +12 -0
- package/scripts/utils/browser.mjs +22 -0
- package/scripts/utils/canonical_home.mjs +20 -0
- package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +71 -0
- package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
- package/scripts/utils/config.mjs +13 -1
- package/scripts/utils/dev_auth_key.mjs +169 -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/env.mjs +94 -23
- package/scripts/utils/env_file.mjs +36 -0
- package/scripts/utils/expo.mjs +96 -0
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +484 -0
- package/scripts/utils/localhost_host.mjs +17 -0
- package/scripts/utils/ownership.mjs +135 -0
- package/scripts/utils/paths.mjs +5 -2
- package/scripts/utils/pm.mjs +132 -22
- package/scripts/utils/ports.mjs +51 -13
- package/scripts/utils/proc.mjs +75 -7
- package/scripts/utils/runtime.mjs +1 -3
- package/scripts/utils/sandbox.mjs +14 -0
- package/scripts/utils/server.mjs +61 -0
- package/scripts/utils/server_port.mjs +9 -0
- package/scripts/utils/server_urls.mjs +54 -0
- package/scripts/utils/stack_context.mjs +23 -0
- package/scripts/utils/stack_runtime_state.mjs +104 -0
- package/scripts/utils/stack_startup.mjs +208 -0
- package/scripts/utils/stack_stop.mjs +255 -0
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/validate.mjs +42 -1
- package/scripts/utils/watch.mjs +63 -0
- package/scripts/utils/worktrees.mjs +57 -1
- package/scripts/where.mjs +14 -7
- package/scripts/worktrees.mjs +135 -15
- /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
- /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
- /package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +0 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
import './utils/env.mjs';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
6
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
7
|
+
import { getRootDir, resolveStackEnvPath } from './utils/paths.mjs';
|
|
8
|
+
import { isTty, promptSelect, withRl } from './utils/cli/wizard.mjs';
|
|
9
|
+
import { getCanonicalHomeDir } from './utils/config.mjs';
|
|
10
|
+
import { ensureEnvLocalUpdated } from './utils/env_local.mjs';
|
|
11
|
+
import { run, runCapture } from './utils/proc.mjs';
|
|
12
|
+
import { fetchHappyHealth } from './utils/server.mjs';
|
|
13
|
+
import { tailscaleServeEnable, tailscaleServeHttpsUrlForInternalServerUrl } from './tailscale.mjs';
|
|
14
|
+
import { getRuntimeDir } from './utils/runtime.mjs';
|
|
15
|
+
import { readFile } from 'node:fs/promises';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { parseDotenv } from './utils/dotenv.mjs';
|
|
18
|
+
import { installService } from './service.mjs';
|
|
19
|
+
import { getDevAuthKeyPath } from './utils/dev_auth_key.mjs';
|
|
20
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
21
|
+
|
|
22
|
+
function boolFromFlagsOrKv({ flags, kv, onFlag, offFlag, key, defaultValue }) {
|
|
23
|
+
if (flags.has(offFlag)) return false;
|
|
24
|
+
if (flags.has(onFlag)) return true;
|
|
25
|
+
if (key && kv.has(key)) {
|
|
26
|
+
const raw = String(kv.get(key) ?? '').trim().toLowerCase();
|
|
27
|
+
if (raw === '1' || raw === 'true' || raw === 'yes' || raw === 'y') return true;
|
|
28
|
+
if (raw === '0' || raw === 'false' || raw === 'no' || raw === 'n') return false;
|
|
29
|
+
}
|
|
30
|
+
return defaultValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeProfile(raw) {
|
|
34
|
+
const v = (raw ?? '').trim().toLowerCase();
|
|
35
|
+
if (!v) return '';
|
|
36
|
+
if (v === 'selfhost' || v === 'self-host' || v === 'self_host' || v === 'host') return 'selfhost';
|
|
37
|
+
if (v === 'dev' || v === 'developer' || v === 'develop') return 'dev';
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeServer(raw) {
|
|
42
|
+
const v = (raw ?? '').trim().toLowerCase();
|
|
43
|
+
if (!v) return '';
|
|
44
|
+
if (v === 'light' || v === 'server-light' || v === 'happy-server-light') return 'happy-server-light';
|
|
45
|
+
if (v === 'server' || v === 'full' || v === 'happy-server') return 'happy-server';
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function boolFromFlags({ flags, onFlag, offFlag, defaultValue }) {
|
|
50
|
+
if (flags.has(offFlag)) return false;
|
|
51
|
+
if (flags.has(onFlag)) return true;
|
|
52
|
+
return defaultValue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function commandExists(cmd) {
|
|
56
|
+
try {
|
|
57
|
+
const out = (await runCapture('sh', ['-lc', `command -v ${cmd} >/dev/null 2>&1 && echo yes || echo no`])).trim();
|
|
58
|
+
return out === 'yes';
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function openUrl(url) {
|
|
65
|
+
const u = String(url ?? '').trim();
|
|
66
|
+
if (!u) return false;
|
|
67
|
+
if (process.platform === 'darwin') {
|
|
68
|
+
await run('open', [u]).catch(() => {});
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (process.platform === 'linux') {
|
|
72
|
+
if (await commandExists('xdg-open')) {
|
|
73
|
+
await run('xdg-open', [u]).catch(() => {});
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function waitForHealthOk(internalServerUrl, { timeoutMs = 60_000 } = {}) {
|
|
82
|
+
const deadline = Date.now() + timeoutMs;
|
|
83
|
+
while (Date.now() < deadline) {
|
|
84
|
+
// eslint-disable-next-line no-await-in-loop
|
|
85
|
+
const health = await fetchHappyHealth(internalServerUrl);
|
|
86
|
+
if (health.ok) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
// eslint-disable-next-line no-await-in-loop
|
|
90
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseEnvFileText(text) {
|
|
96
|
+
try {
|
|
97
|
+
return parseDotenv(text ?? '');
|
|
98
|
+
} catch {
|
|
99
|
+
return new Map();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function readEnvValueFromFile(envPath, key) {
|
|
104
|
+
try {
|
|
105
|
+
if (!envPath || !existsSync(envPath)) return '';
|
|
106
|
+
const raw = await readFile(envPath, 'utf-8');
|
|
107
|
+
const parsed = parseEnvFileText(raw);
|
|
108
|
+
return (parsed.get(key) ?? '').trim();
|
|
109
|
+
} catch {
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function resolveMainServerPort() {
|
|
115
|
+
// Priority:
|
|
116
|
+
// - explicit env var
|
|
117
|
+
// - main stack env file (preferred)
|
|
118
|
+
// - default
|
|
119
|
+
const fromEnv =
|
|
120
|
+
(process.env.HAPPY_LOCAL_SERVER_PORT ?? process.env.HAPPY_STACKS_SERVER_PORT ?? '').toString().trim();
|
|
121
|
+
if (fromEnv) {
|
|
122
|
+
const n = Number(fromEnv);
|
|
123
|
+
return Number.isFinite(n) && n > 0 ? n : 3005;
|
|
124
|
+
}
|
|
125
|
+
const envPath = resolveStackEnvPath('main').envPath;
|
|
126
|
+
const v =
|
|
127
|
+
(await readEnvValueFromFile(envPath, 'HAPPY_LOCAL_SERVER_PORT')) ||
|
|
128
|
+
(await readEnvValueFromFile(envPath, 'HAPPY_STACKS_SERVER_PORT')) ||
|
|
129
|
+
'';
|
|
130
|
+
if (v) {
|
|
131
|
+
const n = Number(v);
|
|
132
|
+
return Number.isFinite(n) && n > 0 ? n : 3005;
|
|
133
|
+
}
|
|
134
|
+
return 3005;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function ensureSetupConfigPersisted({ rootDir, profile, serverComponent, tailscaleWanted, menubarMode }) {
|
|
138
|
+
const updates = [
|
|
139
|
+
{ key: 'HAPPY_STACKS_SERVER_COMPONENT', value: serverComponent },
|
|
140
|
+
{ key: 'HAPPY_LOCAL_SERVER_COMPONENT', value: serverComponent },
|
|
141
|
+
// Default for selfhost: upstream.
|
|
142
|
+
...(profile === 'selfhost'
|
|
143
|
+
? [
|
|
144
|
+
{ key: 'HAPPY_STACKS_REPO_SOURCE', value: 'upstream' },
|
|
145
|
+
{ key: 'HAPPY_LOCAL_REPO_SOURCE', value: 'upstream' },
|
|
146
|
+
]
|
|
147
|
+
: []),
|
|
148
|
+
{ key: 'HAPPY_STACKS_MENUBAR_MODE', value: menubarMode },
|
|
149
|
+
{ key: 'HAPPY_LOCAL_MENUBAR_MODE', value: menubarMode },
|
|
150
|
+
...(tailscaleWanted
|
|
151
|
+
? [
|
|
152
|
+
{ key: 'HAPPY_STACKS_TAILSCALE_SERVE', value: '1' },
|
|
153
|
+
{ key: 'HAPPY_LOCAL_TAILSCALE_SERVE', value: '1' },
|
|
154
|
+
]
|
|
155
|
+
: []),
|
|
156
|
+
];
|
|
157
|
+
await ensureEnvLocalUpdated({ rootDir, updates });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function ensureSystemdAvailable() {
|
|
161
|
+
if (process.platform !== 'linux') return true;
|
|
162
|
+
return (await commandExists('systemctl')) && (await commandExists('journalctl'));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function runNodeScript({ rootDir, rel, args = [], env = process.env }) {
|
|
166
|
+
await run(process.execPath, [join(rootDir, rel), ...args], { cwd: rootDir, env });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function spawnDetachedNodeScript({ rootDir, rel, args = [], env = process.env }) {
|
|
170
|
+
const child = spawn(process.execPath, [join(rootDir, rel), ...args], {
|
|
171
|
+
cwd: rootDir,
|
|
172
|
+
env,
|
|
173
|
+
stdio: 'ignore',
|
|
174
|
+
detached: process.platform !== 'win32',
|
|
175
|
+
});
|
|
176
|
+
child.unref();
|
|
177
|
+
return child.pid;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function mainCliHomeDirForEnvPath(envPath) {
|
|
181
|
+
const { baseDir } = resolveStackEnvPath('main');
|
|
182
|
+
// Prefer stack base dir; envPath is informational and can be legacy/new.
|
|
183
|
+
return join(baseDir, 'cli');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function getMainStacksAccessKeyPath() {
|
|
187
|
+
const cliHomeDir = mainCliHomeDirForEnvPath(resolveStackEnvPath('main').envPath);
|
|
188
|
+
return join(cliHomeDir, 'access.key');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getLegacyHappyAccessKeyPath() {
|
|
192
|
+
return join(homedir(), '.happy', 'cli', 'access.key');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getDevAuthStackAccessKeyPath(stackName = 'dev-auth') {
|
|
196
|
+
const { baseDir, envPath } = resolveStackEnvPath(stackName);
|
|
197
|
+
if (!existsSync(envPath)) return null;
|
|
198
|
+
return join(baseDir, 'cli', 'access.key');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function detectAuthSources() {
|
|
202
|
+
const devKeyPath = getDevAuthKeyPath();
|
|
203
|
+
const mainAccessKeyPath = getMainStacksAccessKeyPath();
|
|
204
|
+
const legacyAccessKeyPath = getLegacyHappyAccessKeyPath();
|
|
205
|
+
const devAuthAccessKeyPath = getDevAuthStackAccessKeyPath('dev-auth');
|
|
206
|
+
const allowLegacy = !isSandboxed() || sandboxAllowsGlobalSideEffects();
|
|
207
|
+
return {
|
|
208
|
+
devKeyPath,
|
|
209
|
+
hasDevKey: existsSync(devKeyPath),
|
|
210
|
+
mainAccessKeyPath,
|
|
211
|
+
hasMainAccessKey: existsSync(mainAccessKeyPath),
|
|
212
|
+
legacyAccessKeyPath,
|
|
213
|
+
hasLegacyAccessKey: allowLegacy && existsSync(legacyAccessKeyPath),
|
|
214
|
+
devAuthAccessKeyPath,
|
|
215
|
+
hasDevAuthAccessKey: Boolean(devAuthAccessKeyPath && existsSync(devAuthAccessKeyPath)),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function maybeConfigureAuthDefaults({ rootDir, profile, interactive }) {
|
|
220
|
+
if (!interactive) return;
|
|
221
|
+
|
|
222
|
+
const sources = detectAuthSources();
|
|
223
|
+
|
|
224
|
+
// 1) Dev key reuse (preferred: reuse if present).
|
|
225
|
+
if (sources.hasDevKey) {
|
|
226
|
+
// eslint-disable-next-line no-console
|
|
227
|
+
console.log(`[setup] dev-key: detected (${sources.devKeyPath})`);
|
|
228
|
+
const choice = await withRl(async (rl) => {
|
|
229
|
+
return await promptSelect(rl, {
|
|
230
|
+
title:
|
|
231
|
+
'A dev key is already configured on this machine. Reuse it? (recommended for restoring the UI account)',
|
|
232
|
+
options: [
|
|
233
|
+
{ label: 'yes (default) — keep using the existing dev key', value: 'reuse' },
|
|
234
|
+
{ label: 'print it now (will display a secret key)', value: 'print' },
|
|
235
|
+
{ label: 'skip', value: 'skip' },
|
|
236
|
+
],
|
|
237
|
+
defaultIndex: 0,
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
if (choice === 'print') {
|
|
241
|
+
// eslint-disable-next-line no-console
|
|
242
|
+
console.log('[setup] dev-key: printing (sensitive)');
|
|
243
|
+
await runNodeScript({ rootDir, rel: 'scripts/auth.mjs', args: ['dev-key', '--print'] });
|
|
244
|
+
}
|
|
245
|
+
} else if (profile === 'dev') {
|
|
246
|
+
// No dev key: offer to create a dedicated seed stack, which guides generating/saving one.
|
|
247
|
+
const create = await withRl(async (rl) => {
|
|
248
|
+
return await promptSelect(rl, {
|
|
249
|
+
title: 'No dev key found. Create one now via a dedicated dev-auth seed stack?',
|
|
250
|
+
options: [
|
|
251
|
+
{ label: 'yes (recommended)', value: true },
|
|
252
|
+
{ label: 'no', value: false },
|
|
253
|
+
],
|
|
254
|
+
defaultIndex: 0,
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
if (create) {
|
|
258
|
+
await runNodeScript({ rootDir, rel: 'scripts/stack.mjs', args: ['create-dev-auth-seed', 'dev-auth'] });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 2) Default auth seeding source for NEW stacks (ordering requested):
|
|
263
|
+
// - prefer dev-auth if we have a dev key (or dev-auth is already set up)
|
|
264
|
+
// - else prefer main happy-stacks (if authenticated)
|
|
265
|
+
// - else prefer legacy ~/.happy (if present)
|
|
266
|
+
const opts = [];
|
|
267
|
+
if (sources.hasDevKey && sources.hasDevAuthAccessKey) {
|
|
268
|
+
opts.push({ label: 'use dev-auth seed stack (recommended)', value: 'dev-auth' });
|
|
269
|
+
}
|
|
270
|
+
if (!sources.hasDevAuthAccessKey && sources.hasDevKey && existsSync(resolveStackEnvPath('dev-auth').envPath)) {
|
|
271
|
+
opts.push({ label: 'use dev-auth seed stack (exists but not authenticated yet)', value: 'dev-auth' });
|
|
272
|
+
}
|
|
273
|
+
if (sources.hasMainAccessKey) {
|
|
274
|
+
opts.push({ label: 'use Happy Stacks main (copy/symlink from main stack)', value: 'main' });
|
|
275
|
+
}
|
|
276
|
+
if (sources.hasLegacyAccessKey) {
|
|
277
|
+
opts.push({ label: 'use legacy ~/.happy (best-effort)', value: 'legacy' });
|
|
278
|
+
}
|
|
279
|
+
opts.push({ label: 'disable auto-seeding (I will login per stack)', value: 'off' });
|
|
280
|
+
|
|
281
|
+
const defaultSeed = opts[0]?.value ?? 'off';
|
|
282
|
+
const seedChoice = await withRl(async (rl) => {
|
|
283
|
+
return await promptSelect(rl, {
|
|
284
|
+
title: 'Default auth source for new stacks (so PR stacks can work without re-login)?',
|
|
285
|
+
options: opts,
|
|
286
|
+
defaultIndex: Math.max(0, opts.findIndex((o) => o.value === defaultSeed)),
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (seedChoice === 'off') {
|
|
291
|
+
await ensureEnvLocalUpdated({
|
|
292
|
+
rootDir,
|
|
293
|
+
updates: [
|
|
294
|
+
{ key: 'HAPPY_STACKS_AUTO_AUTH_SEED', value: '0' },
|
|
295
|
+
{ key: 'HAPPY_LOCAL_AUTO_AUTH_SEED', value: '0' },
|
|
296
|
+
],
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Symlink vs copy for seeded stacks (preferred: symlink so credentials stay up to date).
|
|
302
|
+
const linkChoice = await withRl(async (rl) => {
|
|
303
|
+
return await promptSelect(rl, {
|
|
304
|
+
title: 'When seeding auth into stacks, reuse credentials via symlink or copy?',
|
|
305
|
+
options: [
|
|
306
|
+
{ label: 'reuse (recommended) — symlink so it stays up to date', value: 'link' },
|
|
307
|
+
{ label: 'copy — more isolated per stack', value: 'copy' },
|
|
308
|
+
],
|
|
309
|
+
defaultIndex: 0,
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await ensureEnvLocalUpdated({
|
|
314
|
+
rootDir,
|
|
315
|
+
updates: [
|
|
316
|
+
{ key: 'HAPPY_STACKS_AUTO_AUTH_SEED', value: '1' },
|
|
317
|
+
{ key: 'HAPPY_LOCAL_AUTO_AUTH_SEED', value: '1' },
|
|
318
|
+
{ key: 'HAPPY_STACKS_AUTH_SEED_FROM', value: String(seedChoice) },
|
|
319
|
+
{ key: 'HAPPY_LOCAL_AUTH_SEED_FROM', value: String(seedChoice) },
|
|
320
|
+
{ key: 'HAPPY_STACKS_AUTH_LINK', value: linkChoice === 'link' ? '1' : '0' },
|
|
321
|
+
{ key: 'HAPPY_LOCAL_AUTH_LINK', value: linkChoice === 'link' ? '1' : '0' },
|
|
322
|
+
],
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function cmdSetup({ rootDir, argv }) {
|
|
327
|
+
// Alias: `happys setup pr ...` (maintainer-friendly, idempotent PR setup).
|
|
328
|
+
// This delegates to `setup-pr` so the logic stays centralized.
|
|
329
|
+
const firstPositional = argv.find((a) => !a.startsWith('--')) ?? '';
|
|
330
|
+
if (firstPositional === 'pr') {
|
|
331
|
+
const idx = argv.indexOf('pr');
|
|
332
|
+
const forwarded = idx >= 0 ? argv.slice(idx + 1) : [];
|
|
333
|
+
await run(process.execPath, [join(rootDir, 'scripts', 'setup_pr.mjs'), ...forwarded], { cwd: rootDir });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const { flags, kv } = parseArgs(argv);
|
|
338
|
+
const json = wantsJson(argv, { flags });
|
|
339
|
+
if (wantsHelp(argv, { flags })) {
|
|
340
|
+
printResult({
|
|
341
|
+
json,
|
|
342
|
+
data: {
|
|
343
|
+
profiles: ['selfhost', 'dev'],
|
|
344
|
+
flags: [
|
|
345
|
+
'--profile=selfhost|dev',
|
|
346
|
+
'--server=happy-server-light|happy-server',
|
|
347
|
+
'--install-path',
|
|
348
|
+
'--start-now',
|
|
349
|
+
'--auth|--no-auth',
|
|
350
|
+
'--tailscale|--no-tailscale',
|
|
351
|
+
'--autostart|--no-autostart',
|
|
352
|
+
'--menubar|--no-menubar',
|
|
353
|
+
'--json',
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
text: [
|
|
357
|
+
'[setup] usage:',
|
|
358
|
+
' happys setup',
|
|
359
|
+
' happys setup --profile=selfhost',
|
|
360
|
+
' happys setup --profile=dev',
|
|
361
|
+
' happys setup pr --happy=<pr-url|number> [--happy-cli=<pr-url|number>]',
|
|
362
|
+
' happys setup --auth',
|
|
363
|
+
' happys setup --no-auth',
|
|
364
|
+
'',
|
|
365
|
+
'notes:',
|
|
366
|
+
' - selfhost profile is a guided installer for running Happy locally (optionally with Tailscale + autostart).',
|
|
367
|
+
' - dev profile prepares a development workspace (bootstrap wizard + optional dev tooling).',
|
|
368
|
+
' - `setup pr` is a non-interactive, idempotent helper for maintainers to run PR stacks (delegates to `happys setup-pr`).',
|
|
369
|
+
].join('\n'),
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const interactive = isTty();
|
|
375
|
+
let profile = normalizeProfile(kv.get('--profile'));
|
|
376
|
+
if (!profile && interactive) {
|
|
377
|
+
profile = await withRl(async (rl) => {
|
|
378
|
+
return await promptSelect(rl, {
|
|
379
|
+
title: 'What is your goal?',
|
|
380
|
+
options: [
|
|
381
|
+
{ label: 'Use Happy on this machine (self-host)', value: 'selfhost' },
|
|
382
|
+
{ label: 'Develop Happy (worktrees/stacks)', value: 'dev' },
|
|
383
|
+
],
|
|
384
|
+
defaultIndex: 0,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (!profile) {
|
|
389
|
+
profile = 'selfhost';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const platform = process.platform;
|
|
393
|
+
const supportsAutostart = platform === 'darwin' || platform === 'linux';
|
|
394
|
+
const supportsMenubar = platform === 'darwin';
|
|
395
|
+
|
|
396
|
+
const serverFromArg = normalizeServer(kv.get('--server'));
|
|
397
|
+
let serverComponent = serverFromArg || normalizeServer(process.env.HAPPY_STACKS_SERVER_COMPONENT) || 'happy-server-light';
|
|
398
|
+
if (profile === 'selfhost' && interactive && !serverFromArg) {
|
|
399
|
+
serverComponent = await withRl(async (rl) => {
|
|
400
|
+
const picked = await promptSelect(rl, {
|
|
401
|
+
title: 'Select server flavor:',
|
|
402
|
+
options: [
|
|
403
|
+
{ label: 'happy-server-light (recommended; simplest local install)', value: 'happy-server-light' },
|
|
404
|
+
{ label: 'happy-server (full server; managed infra via Docker)', value: 'happy-server' },
|
|
405
|
+
],
|
|
406
|
+
defaultIndex: serverComponent === 'happy-server' ? 1 : 0,
|
|
407
|
+
});
|
|
408
|
+
return picked;
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const defaultTailscale = false;
|
|
413
|
+
const defaultAutostart = false;
|
|
414
|
+
const defaultMenubar = false;
|
|
415
|
+
const defaultStartNow = profile === 'selfhost';
|
|
416
|
+
const defaultInstallPath = false;
|
|
417
|
+
|
|
418
|
+
let tailscaleWanted = boolFromFlags({ flags, onFlag: '--tailscale', offFlag: '--no-tailscale', defaultValue: defaultTailscale });
|
|
419
|
+
let autostartWanted = boolFromFlags({ flags, onFlag: '--autostart', offFlag: '--no-autostart', defaultValue: defaultAutostart });
|
|
420
|
+
let menubarWanted = boolFromFlags({ flags, onFlag: '--menubar', offFlag: '--no-menubar', defaultValue: defaultMenubar });
|
|
421
|
+
let startNow = boolFromFlags({ flags, onFlag: '--start-now', offFlag: '--no-start-now', defaultValue: defaultStartNow });
|
|
422
|
+
let installPath = flags.has('--install-path') ? true : defaultInstallPath;
|
|
423
|
+
let authWanted = boolFromFlagsOrKv({
|
|
424
|
+
flags,
|
|
425
|
+
kv,
|
|
426
|
+
onFlag: '--auth',
|
|
427
|
+
offFlag: '--no-auth',
|
|
428
|
+
key: '--auth',
|
|
429
|
+
defaultValue: profile === 'selfhost',
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
if (interactive) {
|
|
433
|
+
if (profile === 'selfhost') {
|
|
434
|
+
tailscaleWanted = await withRl(async (rl) => {
|
|
435
|
+
const v = await promptSelect(rl, {
|
|
436
|
+
title: 'Enable remote access with Tailscale Serve (recommended for mobile)?',
|
|
437
|
+
options: [
|
|
438
|
+
{ label: 'no (default)', value: false },
|
|
439
|
+
{ label: 'yes', value: true },
|
|
440
|
+
],
|
|
441
|
+
defaultIndex: tailscaleWanted ? 1 : 0,
|
|
442
|
+
});
|
|
443
|
+
return v;
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
if (supportsAutostart) {
|
|
447
|
+
autostartWanted = await withRl(async (rl) => {
|
|
448
|
+
const v = await promptSelect(rl, {
|
|
449
|
+
title: 'Enable autostart at login?',
|
|
450
|
+
options: [
|
|
451
|
+
{ label: 'no (default)', value: false },
|
|
452
|
+
{ label: 'yes', value: true },
|
|
453
|
+
],
|
|
454
|
+
defaultIndex: autostartWanted ? 1 : 0,
|
|
455
|
+
});
|
|
456
|
+
return v;
|
|
457
|
+
});
|
|
458
|
+
} else {
|
|
459
|
+
autostartWanted = false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (supportsMenubar) {
|
|
463
|
+
menubarWanted = await withRl(async (rl) => {
|
|
464
|
+
const v = await promptSelect(rl, {
|
|
465
|
+
title: 'Install the macOS menubar (SwiftBar) control panel?',
|
|
466
|
+
options: [
|
|
467
|
+
{ label: 'no (default)', value: false },
|
|
468
|
+
{ label: 'yes', value: true },
|
|
469
|
+
],
|
|
470
|
+
defaultIndex: menubarWanted ? 1 : 0,
|
|
471
|
+
});
|
|
472
|
+
return v;
|
|
473
|
+
});
|
|
474
|
+
} else {
|
|
475
|
+
menubarWanted = false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
startNow = await withRl(async (rl) => {
|
|
479
|
+
const v = await promptSelect(rl, {
|
|
480
|
+
title: 'Start Happy now?',
|
|
481
|
+
options: [
|
|
482
|
+
{ label: 'yes (default)', value: true },
|
|
483
|
+
{ label: 'no', value: false },
|
|
484
|
+
],
|
|
485
|
+
defaultIndex: startNow ? 0 : 1,
|
|
486
|
+
});
|
|
487
|
+
return v;
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
authWanted = await withRl(async (rl) => {
|
|
491
|
+
const v = await promptSelect(rl, {
|
|
492
|
+
title: 'Authenticate now? (recommended)',
|
|
493
|
+
options: [
|
|
494
|
+
{ label: 'yes (default) — enables Happy UI + mobile access', value: true },
|
|
495
|
+
{ label: 'no — I will authenticate later', value: false },
|
|
496
|
+
],
|
|
497
|
+
defaultIndex: authWanted ? 0 : 1,
|
|
498
|
+
});
|
|
499
|
+
return v;
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Auth requires the stack to be running; if you chose "authenticate now", implicitly start.
|
|
503
|
+
if (authWanted) {
|
|
504
|
+
startNow = true;
|
|
505
|
+
}
|
|
506
|
+
} else if (profile === 'dev') {
|
|
507
|
+
// In dev profile, we don't assume you want to run anything immediately.
|
|
508
|
+
// If you choose to auth now, we’ll also start Happy in the background so login can complete.
|
|
509
|
+
const authNow = await withRl(async (rl) => {
|
|
510
|
+
const v = await promptSelect(rl, {
|
|
511
|
+
title: 'Complete authentication now? (optional)',
|
|
512
|
+
options: [
|
|
513
|
+
{ label: 'no (default) — I will do this later', value: false },
|
|
514
|
+
{ label: 'yes — start Happy in background and login', value: true },
|
|
515
|
+
],
|
|
516
|
+
defaultIndex: 0,
|
|
517
|
+
});
|
|
518
|
+
return v;
|
|
519
|
+
});
|
|
520
|
+
authWanted = authNow;
|
|
521
|
+
if (authNow) {
|
|
522
|
+
startNow = true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
installPath = await withRl(async (rl) => {
|
|
527
|
+
const v = await promptSelect(rl, {
|
|
528
|
+
title: `Add ${join(getCanonicalHomeDir(), 'bin')} to your shell PATH?`,
|
|
529
|
+
options: [
|
|
530
|
+
{ label: 'no (default)', value: false },
|
|
531
|
+
{ label: 'yes', value: true },
|
|
532
|
+
],
|
|
533
|
+
defaultIndex: installPath ? 1 : 0,
|
|
534
|
+
});
|
|
535
|
+
return v;
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Enforce OS support gates even if flags were passed.
|
|
540
|
+
if (!supportsAutostart) autostartWanted = false;
|
|
541
|
+
if (!supportsMenubar) menubarWanted = false;
|
|
542
|
+
|
|
543
|
+
const menubarMode = profile === 'selfhost' ? 'selfhost' : 'dev';
|
|
544
|
+
|
|
545
|
+
const config = {
|
|
546
|
+
profile,
|
|
547
|
+
platform,
|
|
548
|
+
interactive,
|
|
549
|
+
serverComponent,
|
|
550
|
+
authWanted,
|
|
551
|
+
tailscaleWanted,
|
|
552
|
+
autostartWanted,
|
|
553
|
+
menubarWanted,
|
|
554
|
+
startNow,
|
|
555
|
+
installPath,
|
|
556
|
+
runtimeDir: getRuntimeDir(),
|
|
557
|
+
};
|
|
558
|
+
if (json) {
|
|
559
|
+
printResult({ json, data: config });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// 1) Ensure plumbing exists (runtime + shims + pointer env). Avoid auto-bootstrap here; setup drives bootstrap explicitly.
|
|
564
|
+
await runNodeScript({
|
|
565
|
+
rootDir,
|
|
566
|
+
rel: 'scripts/init.mjs',
|
|
567
|
+
args: [
|
|
568
|
+
'--no-bootstrap',
|
|
569
|
+
...(installPath ? ['--install-path'] : []),
|
|
570
|
+
],
|
|
571
|
+
env: { ...process.env, HAPPY_STACKS_SETUP_CHILD: '1' },
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// 2) Persist profile defaults to stack env (server flavor, repo source, tailscale preference, menubar mode).
|
|
575
|
+
await ensureSetupConfigPersisted({
|
|
576
|
+
rootDir,
|
|
577
|
+
profile,
|
|
578
|
+
serverComponent,
|
|
579
|
+
tailscaleWanted,
|
|
580
|
+
menubarMode,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// 3) Bootstrap components. Selfhost defaults to upstream; dev defaults to existing bootstrap wizard (forks by default).
|
|
584
|
+
if (profile === 'dev') {
|
|
585
|
+
// Developer setup: keep the existing bootstrap wizard.
|
|
586
|
+
await runNodeScript({ rootDir, rel: 'scripts/install.mjs', args: ['--interactive'] });
|
|
587
|
+
|
|
588
|
+
// Optional: offer to create a dedicated dev stack (keeps main stable).
|
|
589
|
+
if (interactive) {
|
|
590
|
+
const createStack = await withRl(async (rl) => {
|
|
591
|
+
return await promptSelect(rl, {
|
|
592
|
+
title: 'Create an additional isolated stack for development?',
|
|
593
|
+
options: [
|
|
594
|
+
{ label: 'no (default)', value: false },
|
|
595
|
+
{ label: 'yes', value: true },
|
|
596
|
+
],
|
|
597
|
+
defaultIndex: 0,
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
if (createStack) {
|
|
601
|
+
await runNodeScript({ rootDir, rel: 'scripts/stack.mjs', args: ['new', '--interactive'] });
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Guided maintainer-friendly auth defaults (dev key → main → legacy).
|
|
605
|
+
await maybeConfigureAuthDefaults({ rootDir, profile, interactive });
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
// Selfhost setup: run non-interactively and keep it simple.
|
|
609
|
+
await runNodeScript({
|
|
610
|
+
rootDir,
|
|
611
|
+
rel: 'scripts/install.mjs',
|
|
612
|
+
args: [`--server=${serverComponent}`, '--upstream'],
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// 4) Optional: install autostart (macOS launchd / Linux systemd user).
|
|
617
|
+
if (autostartWanted) {
|
|
618
|
+
if (process.platform === 'linux') {
|
|
619
|
+
const ok = await ensureSystemdAvailable();
|
|
620
|
+
if (!ok) {
|
|
621
|
+
// eslint-disable-next-line no-console
|
|
622
|
+
console.log('[setup] autostart skipped: systemd user services not available on this Linux distro.');
|
|
623
|
+
} else {
|
|
624
|
+
await installService();
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
await installService();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// 5) Optional: install menubar assets (macOS only).
|
|
632
|
+
if (menubarWanted && process.platform === 'darwin') {
|
|
633
|
+
await runNodeScript({ rootDir, rel: 'scripts/menubar.mjs', args: ['install'] });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// 6) Optional: enable tailscale serve (best-effort).
|
|
637
|
+
if (tailscaleWanted) {
|
|
638
|
+
try {
|
|
639
|
+
const internalPort = await resolveMainServerPort();
|
|
640
|
+
const internalServerUrl = `http://127.0.0.1:${internalPort}`;
|
|
641
|
+
const res = await tailscaleServeEnable({ internalServerUrl });
|
|
642
|
+
if (res?.enableUrl && !res?.httpsUrl) {
|
|
643
|
+
// eslint-disable-next-line no-console
|
|
644
|
+
console.log('[setup] tailscale serve requires enabling in your tailnet. Open this URL to continue:');
|
|
645
|
+
// eslint-disable-next-line no-console
|
|
646
|
+
console.log(res.enableUrl);
|
|
647
|
+
// Best-effort open
|
|
648
|
+
await openUrl(res.enableUrl);
|
|
649
|
+
}
|
|
650
|
+
} catch (e) {
|
|
651
|
+
// eslint-disable-next-line no-console
|
|
652
|
+
console.log('[setup] tailscale not available. Install it from: https://tailscale.com/download');
|
|
653
|
+
await openUrl('https://tailscale.com/download');
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// 7) Optional: start now (without requiring setup to keep running).
|
|
658
|
+
if (startNow) {
|
|
659
|
+
const port = await resolveMainServerPort();
|
|
660
|
+
const internalServerUrl = `http://127.0.0.1:${port}`;
|
|
661
|
+
|
|
662
|
+
if (!autostartWanted) {
|
|
663
|
+
// Detached background start.
|
|
664
|
+
await spawnDetachedNodeScript({ rootDir, rel: 'scripts/run.mjs', args: [] });
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const ready = await waitForHealthOk(internalServerUrl, { timeoutMs: 90_000 });
|
|
668
|
+
if (!ready) {
|
|
669
|
+
// eslint-disable-next-line no-console
|
|
670
|
+
console.log(`[setup] started, but server did not become healthy yet: ${internalServerUrl}`);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Prefer tailscale HTTPS URL if available.
|
|
674
|
+
let openTarget = `http://localhost:${port}/`;
|
|
675
|
+
if (tailscaleWanted) {
|
|
676
|
+
const https = await tailscaleServeHttpsUrlForInternalServerUrl(internalServerUrl);
|
|
677
|
+
if (https) {
|
|
678
|
+
openTarget = https.replace(/\/+$/, '') + '/';
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// 8) Optional: auth login (runs interactive browser flow via happy-cli).
|
|
683
|
+
if (authWanted) {
|
|
684
|
+
const ctx = profile === 'selfhost' ? 'selfhost' : 'dev';
|
|
685
|
+
const cliHomeDir = mainCliHomeDirForEnvPath(resolveStackEnvPath('main').envPath);
|
|
686
|
+
const accessKey = join(cliHomeDir, 'access.key');
|
|
687
|
+
if (existsSync(accessKey)) {
|
|
688
|
+
// eslint-disable-next-line no-console
|
|
689
|
+
console.log('[setup] auth: already configured (access.key exists)');
|
|
690
|
+
} else {
|
|
691
|
+
// Before starting an interactive login, offer the best available shortcut in this order:
|
|
692
|
+
// For selfhost profile:
|
|
693
|
+
// - prefer reusing legacy ~/.happy creds if present (maintainers often already have a local install)
|
|
694
|
+
// - otherwise, run the normal login flow
|
|
695
|
+
let reused = false;
|
|
696
|
+
if (interactive) {
|
|
697
|
+
const legacyAccessKey = join(homedir(), '.happy', 'cli', 'access.key');
|
|
698
|
+
const allowLegacy = !isSandboxed() || sandboxAllowsGlobalSideEffects();
|
|
699
|
+
const hasLegacy = allowLegacy && existsSync(legacyAccessKey);
|
|
700
|
+
|
|
701
|
+
if (hasLegacy) {
|
|
702
|
+
const options = [];
|
|
703
|
+
if (hasLegacy) {
|
|
704
|
+
options.push({ label: 'reuse legacy ~/.happy (symlink; stays up to date)', value: 'legacy-link' });
|
|
705
|
+
options.push({ label: 'reuse legacy ~/.happy (copy; more isolated)', value: 'legacy-copy' });
|
|
706
|
+
}
|
|
707
|
+
options.push({ label: 'do login flow instead', value: 'login' });
|
|
708
|
+
|
|
709
|
+
const choice = await withRl(async (rl) => {
|
|
710
|
+
return await promptSelect(rl, {
|
|
711
|
+
title:
|
|
712
|
+
'We found existing credentials on this machine. How should Happy Stacks main authenticate?',
|
|
713
|
+
options,
|
|
714
|
+
defaultIndex: 0,
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
if (choice === 'legacy-link') {
|
|
719
|
+
await runNodeScript({ rootDir, rel: 'scripts/auth.mjs', args: ['copy-from', 'legacy', '--allow-main', '--link'] });
|
|
720
|
+
reused = existsSync(accessKey);
|
|
721
|
+
} else if (choice === 'legacy-copy') {
|
|
722
|
+
await runNodeScript({ rootDir, rel: 'scripts/auth.mjs', args: ['copy-from', 'legacy', '--allow-main'] });
|
|
723
|
+
reused = existsSync(accessKey);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (!reused) {
|
|
729
|
+
await runNodeScript({ rootDir, rel: 'scripts/auth.mjs', args: ['login', `--context=${ctx}`] });
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (!existsSync(accessKey)) {
|
|
733
|
+
// eslint-disable-next-line no-console
|
|
734
|
+
console.log('[setup] auth: not completed yet (missing access.key). You can retry with: happys auth login');
|
|
735
|
+
} else {
|
|
736
|
+
// eslint-disable-next-line no-console
|
|
737
|
+
console.log('[setup] auth: complete');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
} else {
|
|
741
|
+
// eslint-disable-next-line no-console
|
|
742
|
+
console.log('[setup] tip: when you are ready, authenticate with: happys auth login');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
await openUrl(openTarget);
|
|
746
|
+
// eslint-disable-next-line no-console
|
|
747
|
+
console.log(`[setup] open: ${openTarget}`);
|
|
748
|
+
}
|
|
749
|
+
if (authWanted && !startNow) {
|
|
750
|
+
// eslint-disable-next-line no-console
|
|
751
|
+
console.log('[setup] auth: skipped because Happy was not started. When ready:');
|
|
752
|
+
// eslint-disable-next-line no-console
|
|
753
|
+
console.log(' happys start');
|
|
754
|
+
// eslint-disable-next-line no-console
|
|
755
|
+
console.log(' happys auth login');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Final tips (keep short).
|
|
759
|
+
if (profile === 'selfhost') {
|
|
760
|
+
// eslint-disable-next-line no-console
|
|
761
|
+
console.log('[setup] done. Useful commands:');
|
|
762
|
+
// eslint-disable-next-line no-console
|
|
763
|
+
console.log(' happys start');
|
|
764
|
+
// eslint-disable-next-line no-console
|
|
765
|
+
console.log(' happys tailscale enable');
|
|
766
|
+
// eslint-disable-next-line no-console
|
|
767
|
+
console.log(' happys service install # macOS/Linux autostart');
|
|
768
|
+
} else {
|
|
769
|
+
// eslint-disable-next-line no-console
|
|
770
|
+
console.log('[setup] done. Useful commands:');
|
|
771
|
+
// eslint-disable-next-line no-console
|
|
772
|
+
console.log(' happys dev');
|
|
773
|
+
// eslint-disable-next-line no-console
|
|
774
|
+
console.log(' happys wt ...');
|
|
775
|
+
// eslint-disable-next-line no-console
|
|
776
|
+
console.log(' happys stack ...');
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function main() {
|
|
781
|
+
const rootDir = getRootDir(import.meta.url);
|
|
782
|
+
const argv = process.argv.slice(2);
|
|
783
|
+
await cmdSetup({ rootDir, argv });
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
main().catch((err) => {
|
|
787
|
+
console.error('[setup] failed:', err);
|
|
788
|
+
process.exit(1);
|
|
789
|
+
});
|
|
790
|
+
|