happy-stacks 0.1.2 → 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 +121 -83
- 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 +560 -112
- package/scripts/build.mjs +24 -4
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +15 -8
- package/scripts/daemon.mjs +130 -20
- package/scripts/dev.mjs +201 -133
- package/scripts/doctor.mjs +26 -21
- package/scripts/edison.mjs +1828 -0
- package/scripts/happy.mjs +3 -7
- package/scripts/init.mjs +43 -20
- package/scripts/install.mjs +14 -8
- package/scripts/lint.mjs +145 -0
- package/scripts/menubar.mjs +81 -8
- package/scripts/migrate.mjs +25 -15
- package/scripts/mobile.mjs +13 -7
- package/scripts/run.mjs +114 -27
- package/scripts/self.mjs +3 -7
- package/scripts/server_flavor.mjs +3 -3
- package/scripts/service.mjs +15 -2
- package/scripts/setup.mjs +790 -0
- package/scripts/setup_pr.mjs +182 -0
- package/scripts/stack.mjs +1792 -254
- package/scripts/stop.mjs +6 -3
- package/scripts/tailscale.mjs +17 -2
- package/scripts/test.mjs +144 -0
- package/scripts/tui.mjs +556 -0
- package/scripts/typecheck.mjs +2 -2
- package/scripts/ui_gateway.mjs +2 -2
- package/scripts/uninstall.mjs +18 -10
- 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} +48 -0
- package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
- package/scripts/utils/config.mjs +6 -2
- 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 +60 -11
- package/scripts/utils/env_file.mjs +36 -0
- package/scripts/utils/expo.mjs +4 -2
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +100 -46
- 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 +121 -20
- package/scripts/utils/proc.mjs +29 -2
- package/scripts/utils/runtime.mjs +1 -3
- package/scripts/utils/sandbox.mjs +14 -0
- package/scripts/utils/server.mjs +24 -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 +79 -30
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/watch.mjs +63 -0
- package/scripts/utils/worktrees.mjs +57 -1
- package/scripts/where.mjs +14 -7
- package/scripts/worktrees.mjs +82 -8
- /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
package/scripts/build.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import './utils/env.mjs';
|
|
2
|
-
import { parseArgs } from './utils/args.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
3
|
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
|
|
4
4
|
import { ensureDepsInstalled, pmExecBin, requireDir } from './utils/pm.mjs';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { readFile, rm, mkdir, writeFile } from 'node:fs/promises';
|
|
7
7
|
import { tailscaleServeHttpsUrl } from './tailscale.mjs';
|
|
8
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
8
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Build a lightweight static web UI bundle (no Expo dev server).
|
|
@@ -22,17 +22,24 @@ async function main() {
|
|
|
22
22
|
if (wantsHelp(argv, { flags })) {
|
|
23
23
|
printResult({
|
|
24
24
|
json,
|
|
25
|
-
data: { flags: ['--tauri', '--no-tauri'], json: true },
|
|
25
|
+
data: { flags: ['--tauri', '--no-tauri', '--no-ui'], json: true },
|
|
26
26
|
text: [
|
|
27
27
|
'[build] usage:',
|
|
28
28
|
' happys build [--tauri] [--json]',
|
|
29
29
|
' (legacy in a cloned repo): pnpm build [-- --tauri] [--json]',
|
|
30
|
-
' node scripts/build.mjs [--tauri|--no-tauri] [--json]',
|
|
30
|
+
' node scripts/build.mjs [--tauri|--no-tauri] [--no-ui] [--json]',
|
|
31
31
|
].join('\n'),
|
|
32
32
|
});
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
const rootDir = getRootDir(import.meta.url);
|
|
36
|
+
|
|
37
|
+
// Optional: skip building the web UI bundle.
|
|
38
|
+
//
|
|
39
|
+
// This is useful for evidence capture flows that validate non-UI components (e.g. `happy-cli`)
|
|
40
|
+
// but still require a "build" step.
|
|
41
|
+
const skipUi = flags.has('--no-ui');
|
|
42
|
+
|
|
36
43
|
const uiDir = getComponentDir(rootDir, 'happy');
|
|
37
44
|
await requireDir('happy', uiDir);
|
|
38
45
|
|
|
@@ -49,6 +56,19 @@ async function main() {
|
|
|
49
56
|
|
|
50
57
|
// UI is served at root; /ui redirects to /.
|
|
51
58
|
|
|
59
|
+
if (skipUi) {
|
|
60
|
+
// Ensure the output dir exists so server-light doesn't crash if used, but do not run Expo export.
|
|
61
|
+
await rm(outDir, { recursive: true, force: true });
|
|
62
|
+
await mkdir(outDir, { recursive: true });
|
|
63
|
+
await writeFile(join(outDir, '.happy-stacks-build-skipped'), 'no-ui\n', 'utf-8');
|
|
64
|
+
if (json) {
|
|
65
|
+
printResult({ json, data: { ok: true, outDir, skippedUi: true, tauriBuilt: false } });
|
|
66
|
+
} else {
|
|
67
|
+
console.log(`[local] skipping UI export (--no-ui); created empty UI dir at ${outDir}`);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
await ensureDepsInstalled(uiDir, 'happy');
|
|
53
73
|
|
|
54
74
|
// Clean output to avoid stale assets.
|
package/scripts/cli-link.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import './utils/env.mjs';
|
|
2
|
-
import { parseArgs } from './utils/args.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
3
|
import { getComponentDir, getRootDir } from './utils/paths.mjs';
|
|
4
4
|
import { ensureCliBuilt, ensureHappyCliLocalNpmLinked } from './utils/pm.mjs';
|
|
5
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
5
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Link the local Happy CLI wrapper into your PATH.
|
|
@@ -11,7 +11,7 @@ import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
|
11
11
|
*
|
|
12
12
|
* What it does:
|
|
13
13
|
* - optionally builds `components/happy-cli` (controlled by env/flags)
|
|
14
|
-
* - installs `happy`/`happys` shims under `~/.happy-stacks/bin` (recommended over `npm link`)
|
|
14
|
+
* - installs `happy`/`happys` shims under `<homeDir>/bin` (default: `~/.happy-stacks/bin`) (recommended over `npm link`)
|
|
15
15
|
*
|
|
16
16
|
* Env:
|
|
17
17
|
* - HAPPY_LOCAL_CLI_BUILD=0 to skip building happy-cli
|
package/scripts/completion.mjs
CHANGED
|
@@ -5,11 +5,13 @@ import { existsSync } from 'node:fs';
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
|
|
8
|
-
import { parseArgs } from './utils/args.mjs';
|
|
9
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
8
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
9
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
10
10
|
import { runCapture } from './utils/proc.mjs';
|
|
11
|
-
import { getHappysRegistry } from './utils/cli_registry.mjs';
|
|
11
|
+
import { getHappysRegistry } from './utils/cli/cli_registry.mjs';
|
|
12
|
+
import { expandHome } from './utils/canonical_home.mjs';
|
|
12
13
|
import { getHappyStacksHomeDir, getRootDir } from './utils/paths.mjs';
|
|
14
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
13
15
|
|
|
14
16
|
function detectShell() {
|
|
15
17
|
const raw = (process.env.SHELL ?? '').toLowerCase();
|
|
@@ -18,10 +20,6 @@ function detectShell() {
|
|
|
18
20
|
return 'zsh';
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
function expandHome(p) {
|
|
22
|
-
return p.replace(/^~(?=\/)/, homedir());
|
|
23
|
-
}
|
|
24
|
-
|
|
25
23
|
function parseShellArg({ argv, kv }) {
|
|
26
24
|
const fromKv = (kv.get('--shell') ?? '').trim();
|
|
27
25
|
const fromEnv = (process.env.HAPPY_STACKS_SHELL ?? '').trim();
|
|
@@ -217,6 +215,9 @@ function completionPaths({ homeDir, shell }) {
|
|
|
217
215
|
}
|
|
218
216
|
|
|
219
217
|
async function ensureShellInstall({ homeDir, shell }) {
|
|
218
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
219
|
+
return { updated: false, path: null, skipped: 'sandbox' };
|
|
220
|
+
}
|
|
220
221
|
const shellPath = (process.env.SHELL ?? '').toLowerCase();
|
|
221
222
|
const isDarwin = process.platform === 'darwin';
|
|
222
223
|
|
|
@@ -338,7 +339,10 @@ async function main() {
|
|
|
338
339
|
await writeFile(file, contents, 'utf-8');
|
|
339
340
|
|
|
340
341
|
// fish loads completions automatically; zsh/bash need a tiny shell config hook.
|
|
341
|
-
const hook =
|
|
342
|
+
const hook =
|
|
343
|
+
shell === 'fish'
|
|
344
|
+
? { updated: false, path: null }
|
|
345
|
+
: await ensureShellInstall({ homeDir, shell });
|
|
342
346
|
|
|
343
347
|
printResult({
|
|
344
348
|
json,
|
|
@@ -346,6 +350,9 @@ async function main() {
|
|
|
346
350
|
text: [
|
|
347
351
|
`[completion] installed: ${file}`,
|
|
348
352
|
hook?.path ? (hook.updated ? `[completion] enabled via: ${hook.path}` : `[completion] already enabled in: ${hook.path}`) : null,
|
|
353
|
+
hook?.skipped === 'sandbox'
|
|
354
|
+
? '[completion] note: skipped editing shell rc files (sandbox mode). To enable this, re-run with HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1'
|
|
355
|
+
: null,
|
|
349
356
|
'[completion] note: restart your terminal (or source your shell config) to pick it up.',
|
|
350
357
|
]
|
|
351
358
|
.filter(Boolean)
|
package/scripts/daemon.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { spawnProc, run, runCapture } from './utils/proc.mjs';
|
|
2
|
+
import { resolveAuthSeedFromEnv } from './utils/stack_startup.mjs';
|
|
3
|
+
import { getStacksStorageRoot } from './utils/paths.mjs';
|
|
4
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
2
5
|
import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
|
|
3
6
|
import { chmod, copyFile, mkdir } from 'node:fs/promises';
|
|
4
7
|
import { join } from 'node:path';
|
|
@@ -15,7 +18,7 @@ import { homedir } from 'node:os';
|
|
|
15
18
|
* - printing actionable diagnostics
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
|
-
export function cleanupStaleDaemonState(homeDir) {
|
|
21
|
+
export async function cleanupStaleDaemonState(homeDir) {
|
|
19
22
|
const statePath = join(homeDir, 'daemon.state.json');
|
|
20
23
|
const lockPath = join(homeDir, 'daemon.state.json.lock');
|
|
21
24
|
|
|
@@ -23,14 +26,27 @@ export function cleanupStaleDaemonState(homeDir) {
|
|
|
23
26
|
return;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
const lsofHasPath = async (pid, pathNeedle) => {
|
|
30
|
+
try {
|
|
31
|
+
const out = await runCapture('sh', ['-lc', `command -v lsof >/dev/null 2>&1 && lsof -nP -p ${pid} 2>/dev/null || true`]);
|
|
32
|
+
return out.includes(pathNeedle);
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// If lock PID exists and is running, keep lock/state ONLY if it still owns the lock file path.
|
|
27
39
|
try {
|
|
28
40
|
const raw = readFileSync(lockPath, 'utf-8').trim();
|
|
29
41
|
const pid = Number(raw);
|
|
30
42
|
if (Number.isFinite(pid) && pid > 0) {
|
|
31
43
|
try {
|
|
32
44
|
process.kill(pid, 0);
|
|
33
|
-
|
|
45
|
+
// If PID was recycled, refuse to trust it unless we can prove it's associated with this home dir.
|
|
46
|
+
// This prevents cross-stack daemon kills due to stale lock files.
|
|
47
|
+
if (await lsofHasPath(pid, lockPath)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
34
50
|
} catch {
|
|
35
51
|
// stale pid
|
|
36
52
|
}
|
|
@@ -47,7 +63,10 @@ export function cleanupStaleDaemonState(homeDir) {
|
|
|
47
63
|
if (pid) {
|
|
48
64
|
try {
|
|
49
65
|
process.kill(pid, 0);
|
|
50
|
-
|
|
66
|
+
// Only keep if we can prove it still uses this home dir (via state path).
|
|
67
|
+
if (await lsofHasPath(pid, statePath)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
51
70
|
} catch {
|
|
52
71
|
// stale pid
|
|
53
72
|
}
|
|
@@ -107,6 +126,42 @@ export function isDaemonRunning(cliHomeDir) {
|
|
|
107
126
|
return s.status === 'running' || s.status === 'starting';
|
|
108
127
|
}
|
|
109
128
|
|
|
129
|
+
async function readDaemonPsEnv(pid) {
|
|
130
|
+
const n = Number(pid);
|
|
131
|
+
if (!Number.isFinite(n) || n <= 1) return null;
|
|
132
|
+
if (process.platform === 'win32') return null;
|
|
133
|
+
try {
|
|
134
|
+
const out = await runCapture('ps', ['eww', '-p', String(n)]);
|
|
135
|
+
const lines = out.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
136
|
+
// Usually: header + one line.
|
|
137
|
+
return lines.length >= 2 ? lines[1] : lines[0] ?? null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function daemonEnvMatches({ pid, cliHomeDir, internalServerUrl, publicServerUrl }) {
|
|
144
|
+
const line = await readDaemonPsEnv(pid);
|
|
145
|
+
if (!line) return null; // unknown
|
|
146
|
+
const home = String(cliHomeDir ?? '').trim();
|
|
147
|
+
const server = String(internalServerUrl ?? '').trim();
|
|
148
|
+
const web = String(publicServerUrl ?? '').trim();
|
|
149
|
+
|
|
150
|
+
// Must be for the same stack home dir.
|
|
151
|
+
if (home && !line.includes(`HAPPY_HOME_DIR=${home}`)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
// If we have a desired server URL, require it (prevents ephemeral port mismatches).
|
|
155
|
+
if (server && !line.includes(`HAPPY_SERVER_URL=${server}`)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
// Public URL mismatch is less fatal, but prefer it stable too when provided.
|
|
159
|
+
if (web && !line.includes(`HAPPY_WEBAPP_URL=${web}`)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
110
165
|
function getLatestDaemonLogPath(homeDir) {
|
|
111
166
|
try {
|
|
112
167
|
const logsDir = join(homeDir, 'logs');
|
|
@@ -141,17 +196,28 @@ function authLoginHint() {
|
|
|
141
196
|
return stackName === 'main' ? 'happys auth login' : `happys stack auth ${stackName} login`;
|
|
142
197
|
}
|
|
143
198
|
|
|
144
|
-
function
|
|
199
|
+
function authCopyFromSeedHint() {
|
|
145
200
|
const stackName = (process.env.HAPPY_STACKS_STACK ?? process.env.HAPPY_LOCAL_STACK ?? '').trim() || 'main';
|
|
146
|
-
|
|
201
|
+
if (stackName === 'main') return null;
|
|
202
|
+
const seed = resolveAuthSeedFromEnv(process.env);
|
|
203
|
+
return `happys stack auth ${stackName} copy-from ${seed}`;
|
|
147
204
|
}
|
|
148
205
|
|
|
149
206
|
async function seedCredentialsIfMissing({ cliHomeDir }) {
|
|
207
|
+
const stacksRoot = getStacksStorageRoot();
|
|
208
|
+
const allowGlobal = sandboxAllowsGlobalSideEffects();
|
|
209
|
+
|
|
150
210
|
const sources = [
|
|
151
|
-
//
|
|
152
|
-
join(
|
|
153
|
-
|
|
154
|
-
|
|
211
|
+
// New layout: main stack credentials (preferred).
|
|
212
|
+
join(stacksRoot, 'main', 'cli'),
|
|
213
|
+
...((!isSandboxed() || allowGlobal)
|
|
214
|
+
? [
|
|
215
|
+
// Legacy happy-local storage root (most common for existing users).
|
|
216
|
+
join(homedir(), '.happy', 'local', 'cli'),
|
|
217
|
+
// Older global location.
|
|
218
|
+
join(homedir(), '.happy'),
|
|
219
|
+
]
|
|
220
|
+
: []),
|
|
155
221
|
];
|
|
156
222
|
|
|
157
223
|
const copyIfMissing = async ({ relPath, mode, label }) => {
|
|
@@ -227,6 +293,22 @@ async function killDaemonFromLockFile({ cliHomeDir }) {
|
|
|
227
293
|
return false;
|
|
228
294
|
}
|
|
229
295
|
|
|
296
|
+
// Hard safety: only kill if we can prove the PID is associated with this stack home dir.
|
|
297
|
+
// We do this by checking that `lsof -p <pid>` includes the lock path (or state file path).
|
|
298
|
+
let ownsLock = false;
|
|
299
|
+
try {
|
|
300
|
+
const out = await runCapture('sh', ['-lc', `command -v lsof >/dev/null 2>&1 && lsof -nP -p ${pid} 2>/dev/null || true`]);
|
|
301
|
+
ownsLock = out.includes(lockPath) || out.includes(join(cliHomeDir, 'daemon.state.json')) || out.includes(join(cliHomeDir, 'logs'));
|
|
302
|
+
} catch {
|
|
303
|
+
ownsLock = false;
|
|
304
|
+
}
|
|
305
|
+
if (!ownsLock) {
|
|
306
|
+
console.warn(
|
|
307
|
+
`[local] refusing to kill pid ${pid} from lock file (could be unrelated; lsof did not show ownership of ${cliHomeDir})`
|
|
308
|
+
);
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
230
312
|
try {
|
|
231
313
|
process.kill(pid, 'SIGTERM');
|
|
232
314
|
} catch {
|
|
@@ -306,13 +388,32 @@ export async function startLocalDaemonWithAuth({
|
|
|
306
388
|
|
|
307
389
|
// If this is a migrated/new stack home dir, seed credentials from the user's existing login (best-effort)
|
|
308
390
|
// to avoid requiring an interactive auth flow under launchd.
|
|
309
|
-
|
|
391
|
+
const migrateCreds = (baseEnv.HAPPY_STACKS_MIGRATE_CREDENTIALS ?? baseEnv.HAPPY_LOCAL_MIGRATE_CREDENTIALS ?? '1').trim() !== '0';
|
|
392
|
+
if (migrateCreds) {
|
|
393
|
+
await seedCredentialsIfMissing({ cliHomeDir });
|
|
394
|
+
}
|
|
310
395
|
|
|
311
396
|
const existing = checkDaemonState(cliHomeDir);
|
|
312
397
|
if (!forceRestart && (existing.status === 'running' || existing.status === 'starting')) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
398
|
+
const pid = existing.pid;
|
|
399
|
+
const matches = await daemonEnvMatches({ pid, cliHomeDir, internalServerUrl, publicServerUrl });
|
|
400
|
+
if (matches === true) {
|
|
401
|
+
// eslint-disable-next-line no-console
|
|
402
|
+
console.log(`[local] daemon already running for stack home (pid=${pid})`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (matches === false) {
|
|
406
|
+
// eslint-disable-next-line no-console
|
|
407
|
+
console.warn(
|
|
408
|
+
`[local] daemon is running but pointed at a different server URL; restarting (pid=${pid}).\n` +
|
|
409
|
+
`[local] expected: ${internalServerUrl}\n`
|
|
410
|
+
);
|
|
411
|
+
} else {
|
|
412
|
+
// unknown: best-effort keep running to avoid killing an unrelated process
|
|
413
|
+
// eslint-disable-next-line no-console
|
|
414
|
+
console.warn(`[local] daemon status is running but could not verify env; not restarting (pid=${pid})`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
316
417
|
}
|
|
317
418
|
|
|
318
419
|
// Stop any existing daemon for THIS stack home dir.
|
|
@@ -326,7 +427,7 @@ export async function startLocalDaemonWithAuth({
|
|
|
326
427
|
}
|
|
327
428
|
|
|
328
429
|
// Best-effort: for the main stack, also stop the legacy global daemon home (~/.happy) to prevent legacy overlap.
|
|
329
|
-
if (stackName === 'main') {
|
|
430
|
+
if (stackName === 'main' && (!isSandboxed() || sandboxAllowsGlobalSideEffects())) {
|
|
330
431
|
const legacyEnv = { ...daemonEnv, HAPPY_HOME_DIR: join(homedir(), '.happy') };
|
|
331
432
|
try {
|
|
332
433
|
await new Promise((resolve) => {
|
|
@@ -338,14 +439,14 @@ export async function startLocalDaemonWithAuth({
|
|
|
338
439
|
}
|
|
339
440
|
// If state is missing and stop couldn't find it, force-stop the lock PID (otherwise repeated restarts accumulate daemons).
|
|
340
441
|
await killDaemonFromLockFile({ cliHomeDir: join(homedir(), '.happy') });
|
|
341
|
-
cleanupStaleDaemonState(join(homedir(), '.happy'));
|
|
442
|
+
await cleanupStaleDaemonState(join(homedir(), '.happy'));
|
|
342
443
|
}
|
|
343
444
|
|
|
344
445
|
// If state is missing and stop couldn't find it, force-stop the lock PID (otherwise repeated restarts accumulate daemons).
|
|
345
446
|
await killDaemonFromLockFile({ cliHomeDir });
|
|
346
447
|
|
|
347
448
|
// Clean up stale lock/state files that can block daemon start.
|
|
348
|
-
cleanupStaleDaemonState(cliHomeDir);
|
|
449
|
+
await cleanupStaleDaemonState(cliHomeDir);
|
|
349
450
|
|
|
350
451
|
const credentialsPath = join(cliHomeDir, 'access.key');
|
|
351
452
|
|
|
@@ -359,7 +460,9 @@ export async function startLocalDaemonWithAuth({
|
|
|
359
460
|
return { ok: true, exitCode, excerpt: null, logPath: null };
|
|
360
461
|
}
|
|
361
462
|
|
|
362
|
-
const logPath =
|
|
463
|
+
const logPath =
|
|
464
|
+
getLatestDaemonLogPath(cliHomeDir) ||
|
|
465
|
+
((!isSandboxed() || sandboxAllowsGlobalSideEffects()) ? getLatestDaemonLogPath(join(homedir(), '.happy')) : null);
|
|
363
466
|
const excerpt = logPath ? readLastLines(logPath, 120) : null;
|
|
364
467
|
return { ok: false, exitCode, excerpt, logPath };
|
|
365
468
|
};
|
|
@@ -373,7 +476,7 @@ export async function startLocalDaemonWithAuth({
|
|
|
373
476
|
}
|
|
374
477
|
|
|
375
478
|
if (excerptIndicatesMissingAuth(first.excerpt)) {
|
|
376
|
-
const copyHint =
|
|
479
|
+
const copyHint = authCopyFromSeedHint();
|
|
377
480
|
console.error(
|
|
378
481
|
`[local] daemon is not authenticated yet (expected on first run).\n` +
|
|
379
482
|
`[local] Keeping the server running so you can login.\n` +
|
|
@@ -397,7 +500,14 @@ export async function startLocalDaemonWithAuth({
|
|
|
397
500
|
throw new Error('Failed to start daemon (after credentials were created)');
|
|
398
501
|
}
|
|
399
502
|
} else {
|
|
400
|
-
|
|
503
|
+
const copyHint = authCopyFromSeedHint();
|
|
504
|
+
console.error(
|
|
505
|
+
`[local] daemon failed to start (server returned an error).\n` +
|
|
506
|
+
`[local] Try:\n` +
|
|
507
|
+
`- happys doctor\n` +
|
|
508
|
+
(copyHint ? `- ${copyHint}\n` : '') +
|
|
509
|
+
`- ${authLoginHint()}`
|
|
510
|
+
);
|
|
401
511
|
throw new Error('Failed to start daemon');
|
|
402
512
|
}
|
|
403
513
|
}
|