happy-stacks 0.0.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 +314 -0
- package/bin/happys.mjs +168 -0
- package/docs/menubar.md +186 -0
- package/docs/mobile-ios.md +134 -0
- package/docs/remote-access.md +43 -0
- package/docs/server-flavors.md +79 -0
- package/docs/stacks.md +218 -0
- package/docs/tauri.md +62 -0
- package/docs/worktrees-and-forks.md +395 -0
- package/extras/swiftbar/auth-login.sh +31 -0
- package/extras/swiftbar/happy-stacks.5s.sh +218 -0
- package/extras/swiftbar/icons/happy-green.png +0 -0
- package/extras/swiftbar/icons/happy-orange.png +0 -0
- package/extras/swiftbar/icons/happy-red.png +0 -0
- package/extras/swiftbar/icons/logo-white.png +0 -0
- package/extras/swiftbar/install.sh +191 -0
- package/extras/swiftbar/lib/git.sh +330 -0
- package/extras/swiftbar/lib/icons.sh +105 -0
- package/extras/swiftbar/lib/render.sh +774 -0
- package/extras/swiftbar/lib/system.sh +190 -0
- package/extras/swiftbar/lib/utils.sh +205 -0
- package/extras/swiftbar/pnpm-term.sh +125 -0
- package/extras/swiftbar/pnpm.sh +21 -0
- package/extras/swiftbar/set-interval.sh +62 -0
- package/extras/swiftbar/set-server-flavor.sh +57 -0
- package/extras/swiftbar/wt-pr.sh +95 -0
- package/package.json +58 -0
- package/scripts/auth.mjs +272 -0
- package/scripts/build.mjs +204 -0
- package/scripts/cli-link.mjs +58 -0
- package/scripts/completion.mjs +364 -0
- package/scripts/daemon.mjs +349 -0
- package/scripts/dev.mjs +181 -0
- package/scripts/doctor.mjs +342 -0
- package/scripts/happy.mjs +79 -0
- package/scripts/init.mjs +232 -0
- package/scripts/install.mjs +379 -0
- package/scripts/menubar.mjs +107 -0
- package/scripts/mobile.mjs +305 -0
- package/scripts/run.mjs +236 -0
- package/scripts/self.mjs +298 -0
- package/scripts/server_flavor.mjs +125 -0
- package/scripts/service.mjs +526 -0
- package/scripts/stack.mjs +815 -0
- package/scripts/tailscale.mjs +278 -0
- package/scripts/uninstall.mjs +190 -0
- package/scripts/utils/args.mjs +17 -0
- package/scripts/utils/cli.mjs +24 -0
- package/scripts/utils/cli_registry.mjs +262 -0
- package/scripts/utils/config.mjs +40 -0
- package/scripts/utils/dotenv.mjs +30 -0
- package/scripts/utils/env.mjs +138 -0
- package/scripts/utils/env_file.mjs +59 -0
- package/scripts/utils/env_local.mjs +25 -0
- package/scripts/utils/fs.mjs +11 -0
- package/scripts/utils/paths.mjs +184 -0
- package/scripts/utils/pm.mjs +294 -0
- package/scripts/utils/ports.mjs +66 -0
- package/scripts/utils/proc.mjs +66 -0
- package/scripts/utils/runtime.mjs +30 -0
- package/scripts/utils/server.mjs +41 -0
- package/scripts/utils/smoke_help.mjs +45 -0
- package/scripts/utils/validate.mjs +47 -0
- package/scripts/utils/wizard.mjs +69 -0
- package/scripts/utils/worktrees.mjs +78 -0
- package/scripts/where.mjs +105 -0
- package/scripts/worktrees.mjs +1721 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import './utils/env.mjs';
|
|
2
|
+
import { parseArgs } from './utils/args.mjs';
|
|
3
|
+
import { pathExists } from './utils/fs.mjs';
|
|
4
|
+
import { runCapture } from './utils/proc.mjs';
|
|
5
|
+
import { getComponentDir, getDefaultAutostartPaths, getHappyStacksHomeDir, getRootDir, getWorkspaceDir, resolveStackEnvPath } from './utils/paths.mjs';
|
|
6
|
+
import { killPortListeners } from './utils/ports.mjs';
|
|
7
|
+
import { getServerComponentName } from './utils/server.mjs';
|
|
8
|
+
import { daemonStatusSummary } from './daemon.mjs';
|
|
9
|
+
import { tailscaleServeStatus, resolvePublicServerUrl } from './tailscale.mjs';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { readFile } from 'node:fs/promises';
|
|
14
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
15
|
+
import { getRuntimeDir } from './utils/runtime.mjs';
|
|
16
|
+
import { assertServerComponentDirMatches } from './utils/validate.mjs';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Doctor script for common happy-stacks failure modes.
|
|
20
|
+
*
|
|
21
|
+
* Checks:
|
|
22
|
+
* - server port in use / server health
|
|
23
|
+
* - UI build directory existence
|
|
24
|
+
* - daemon status
|
|
25
|
+
* - tailscale serve status (if available)
|
|
26
|
+
* - launch agent status (macOS)
|
|
27
|
+
*
|
|
28
|
+
* Flags:
|
|
29
|
+
* - --fix : best-effort fixes (kill server port listener)
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
async function fetchHealth(url) {
|
|
33
|
+
const tryGet = async (path) => {
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`${url}${path}`, { method: 'GET' });
|
|
36
|
+
const body = await res.text();
|
|
37
|
+
return { ok: res.ok, status: res.status, body: body.trim() };
|
|
38
|
+
} catch {
|
|
39
|
+
return { ok: false, status: null, body: null };
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Prefer /health when available, but fall back to / (matches waitForServerReady).
|
|
44
|
+
const health = await tryGet('/health');
|
|
45
|
+
if (health.ok) {
|
|
46
|
+
return health;
|
|
47
|
+
}
|
|
48
|
+
const root = await tryGet('/');
|
|
49
|
+
if (root.ok && root.body?.includes('Welcome to Happy Server!')) {
|
|
50
|
+
return root;
|
|
51
|
+
}
|
|
52
|
+
return health.ok ? health : root;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function readJsonSafe(path) {
|
|
56
|
+
try {
|
|
57
|
+
const raw = await readFile(path, 'utf-8');
|
|
58
|
+
return JSON.parse(raw);
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readPkgVersion(path) {
|
|
65
|
+
try {
|
|
66
|
+
const raw = await readFile(path, 'utf-8');
|
|
67
|
+
const pkg = JSON.parse(raw);
|
|
68
|
+
const v = String(pkg.version ?? '').trim();
|
|
69
|
+
return v || null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function resolveSwiftbarPluginsDir() {
|
|
76
|
+
if (process.platform !== 'darwin') {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const dir = (await runCapture('bash', [
|
|
81
|
+
'-lc',
|
|
82
|
+
'DIR="$(defaults read com.ameba.SwiftBar PluginDirectory 2>/dev/null)"; if [[ -n "$DIR" && -d "$DIR" ]]; then echo "$DIR"; exit 0; fi; D="$HOME/Library/Application Support/SwiftBar/Plugins"; if [[ -d "$D" ]]; then echo "$D"; exit 0; fi; echo ""',
|
|
83
|
+
])).trim();
|
|
84
|
+
return dir || null;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function main() {
|
|
91
|
+
const argv = process.argv.slice(2);
|
|
92
|
+
const { flags, kv } = parseArgs(argv);
|
|
93
|
+
const fix = flags.has('--fix');
|
|
94
|
+
const json = wantsJson(argv, { flags });
|
|
95
|
+
|
|
96
|
+
if (wantsHelp(argv, { flags })) {
|
|
97
|
+
printResult({
|
|
98
|
+
json,
|
|
99
|
+
data: { flags: ['--fix', '--server=happy-server|happy-server-light'], json: true },
|
|
100
|
+
text: [
|
|
101
|
+
'[doctor] usage:',
|
|
102
|
+
' happys doctor [--fix] [--json]',
|
|
103
|
+
' node scripts/doctor.mjs [--fix] [--server=happy-server|happy-server-light] [--json]',
|
|
104
|
+
].join('\n'),
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const rootDir = getRootDir(import.meta.url);
|
|
110
|
+
const homeDir = getHappyStacksHomeDir();
|
|
111
|
+
const runtimeDir = getRuntimeDir();
|
|
112
|
+
const workspaceDir = getWorkspaceDir(rootDir);
|
|
113
|
+
const updateCachePath = join(homeDir, 'cache', 'update.json');
|
|
114
|
+
const runtimePkgJson = join(runtimeDir, 'node_modules', 'happy-stacks', 'package.json');
|
|
115
|
+
const runtimeVersion = await readPkgVersion(runtimePkgJson);
|
|
116
|
+
const updateCache = existsSync(updateCachePath) ? await readJsonSafe(updateCachePath) : null;
|
|
117
|
+
|
|
118
|
+
const serverPort = process.env.HAPPY_LOCAL_SERVER_PORT?.trim() ? Number(process.env.HAPPY_LOCAL_SERVER_PORT) : 3005;
|
|
119
|
+
const internalServerUrl = `http://127.0.0.1:${serverPort}`;
|
|
120
|
+
|
|
121
|
+
const defaultPublicUrl = `http://localhost:${serverPort}`;
|
|
122
|
+
const envPublicUrl = process.env.HAPPY_LOCAL_SERVER_URL?.trim() ? process.env.HAPPY_LOCAL_SERVER_URL.trim() : '';
|
|
123
|
+
const resolved = await resolvePublicServerUrl({
|
|
124
|
+
internalServerUrl,
|
|
125
|
+
defaultPublicUrl,
|
|
126
|
+
envPublicUrl,
|
|
127
|
+
allowEnable: false,
|
|
128
|
+
});
|
|
129
|
+
const publicServerUrl = resolved.publicServerUrl;
|
|
130
|
+
|
|
131
|
+
const cliHomeDir = process.env.HAPPY_LOCAL_CLI_HOME_DIR?.trim()
|
|
132
|
+
? process.env.HAPPY_LOCAL_CLI_HOME_DIR.trim().replace(/^~(?=\/)/, homedir())
|
|
133
|
+
: join(getDefaultAutostartPaths().baseDir, 'cli');
|
|
134
|
+
|
|
135
|
+
const serveUi = (process.env.HAPPY_LOCAL_SERVE_UI ?? '1') !== '0';
|
|
136
|
+
const uiBuildDir = process.env.HAPPY_LOCAL_UI_BUILD_DIR?.trim()
|
|
137
|
+
? process.env.HAPPY_LOCAL_UI_BUILD_DIR.trim()
|
|
138
|
+
: join(getDefaultAutostartPaths().baseDir, 'ui');
|
|
139
|
+
|
|
140
|
+
const serverComponentName = getServerComponentName({ kv });
|
|
141
|
+
if (serverComponentName === 'both') {
|
|
142
|
+
throw new Error(`[local] --server=both is not supported for doctor (pick one: happy-server-light or happy-server)`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const serverDir = getComponentDir(rootDir, serverComponentName);
|
|
146
|
+
const cliDir = getComponentDir(rootDir, 'happy-cli');
|
|
147
|
+
const cliBin = join(cliDir, 'bin', 'happy.mjs');
|
|
148
|
+
|
|
149
|
+
assertServerComponentDirMatches({ rootDir, serverComponentName, serverDir });
|
|
150
|
+
|
|
151
|
+
const report = {
|
|
152
|
+
paths: {
|
|
153
|
+
rootDir,
|
|
154
|
+
homeDir,
|
|
155
|
+
runtimeDir,
|
|
156
|
+
workspaceDir,
|
|
157
|
+
updateCachePath,
|
|
158
|
+
},
|
|
159
|
+
runtime: {
|
|
160
|
+
installed: Boolean(runtimeVersion),
|
|
161
|
+
version: runtimeVersion,
|
|
162
|
+
packageJson: runtimePkgJson,
|
|
163
|
+
updateCache,
|
|
164
|
+
},
|
|
165
|
+
env: {
|
|
166
|
+
homeEnv: join(homeDir, '.env'),
|
|
167
|
+
homeLocal: join(homeDir, 'env.local'),
|
|
168
|
+
mainStackEnv: resolveStackEnvPath('main').envPath,
|
|
169
|
+
activeEnv: process.env.HAPPY_STACKS_ENV_FILE?.trim() || process.env.HAPPY_LOCAL_ENV_FILE?.trim() || null,
|
|
170
|
+
},
|
|
171
|
+
internalServerUrl,
|
|
172
|
+
publicServerUrl,
|
|
173
|
+
serverComponentName,
|
|
174
|
+
uiBuildDir,
|
|
175
|
+
cliHomeDir,
|
|
176
|
+
checks: {},
|
|
177
|
+
};
|
|
178
|
+
if (!json) {
|
|
179
|
+
console.log('🩺 happy-stacks doctor\n');
|
|
180
|
+
console.log(`- internal: ${internalServerUrl}`);
|
|
181
|
+
console.log(`- public: ${publicServerUrl}`);
|
|
182
|
+
console.log(`- server: ${serverComponentName}`);
|
|
183
|
+
console.log(`- uiBuild: ${uiBuildDir}`);
|
|
184
|
+
console.log(`- cliHome: ${cliHomeDir}`);
|
|
185
|
+
console.log(`- home: ${homeDir}`);
|
|
186
|
+
console.log(`- runtime: ${runtimeVersion ? `${runtimeDir} (${runtimeVersion})` : `${runtimeDir} (not installed)`}`);
|
|
187
|
+
console.log(`- workspace:${workspaceDir}`);
|
|
188
|
+
console.log('');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!(await pathExists(serverDir))) {
|
|
192
|
+
report.checks.serverDir = { ok: false, missing: serverDir };
|
|
193
|
+
if (!json) console.log(`❌ missing component: ${serverDir}`);
|
|
194
|
+
}
|
|
195
|
+
if (!(await pathExists(cliDir))) {
|
|
196
|
+
report.checks.cliDir = { ok: false, missing: cliDir };
|
|
197
|
+
if (!json) console.log(`❌ missing component: ${cliDir}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Server health / port conflicts
|
|
201
|
+
const health = await fetchHealth(internalServerUrl);
|
|
202
|
+
if (health.ok) {
|
|
203
|
+
report.checks.serverHealth = { ok: true, status: health.status, body: health.body };
|
|
204
|
+
if (!json) console.log(`✅ server health: ${health.status} ${health.body}`);
|
|
205
|
+
} else {
|
|
206
|
+
report.checks.serverHealth = { ok: false };
|
|
207
|
+
if (!json) console.log(`❌ server health: unreachable (${internalServerUrl})`);
|
|
208
|
+
if (fix) {
|
|
209
|
+
if (!json) console.log(`↪ attempting fix: freeing tcp:${serverPort}`);
|
|
210
|
+
await killPortListeners(serverPort, { label: 'doctor' });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// UI build dir check
|
|
215
|
+
if (serveUi) {
|
|
216
|
+
if (serverComponentName !== 'happy-server-light') {
|
|
217
|
+
report.checks.uiServing = { ok: false, reason: `requires happy-server-light (current: ${serverComponentName})` };
|
|
218
|
+
if (!json) console.log(`ℹ️ ui serving requires happy-server-light (current: ${serverComponentName})`);
|
|
219
|
+
}
|
|
220
|
+
if (await pathExists(uiBuildDir)) {
|
|
221
|
+
report.checks.uiBuildDir = { ok: true, path: uiBuildDir };
|
|
222
|
+
if (!json) console.log('✅ ui build dir present');
|
|
223
|
+
} else {
|
|
224
|
+
report.checks.uiBuildDir = { ok: false, missing: uiBuildDir };
|
|
225
|
+
if (!json) console.log(`❌ ui build dir missing (${uiBuildDir}) → run: happys build`);
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
report.checks.uiServing = { ok: false, reason: 'disabled (HAPPY_LOCAL_SERVE_UI=0)' };
|
|
229
|
+
if (!json) console.log('ℹ️ ui serving disabled (HAPPY_LOCAL_SERVE_UI=0)');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Daemon status
|
|
233
|
+
try {
|
|
234
|
+
const out = await daemonStatusSummary({
|
|
235
|
+
cliBin,
|
|
236
|
+
cliHomeDir,
|
|
237
|
+
internalServerUrl,
|
|
238
|
+
publicServerUrl,
|
|
239
|
+
});
|
|
240
|
+
const line = out.split('\n').find((l) => l.includes('Daemon is running'))?.trim();
|
|
241
|
+
report.checks.daemon = { ok: true, line: line || null };
|
|
242
|
+
if (!json) console.log(`✅ daemon: ${line ? line : 'status ok'}`);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
const accessKeyPath = join(cliHomeDir, 'access.key');
|
|
245
|
+
const hasAccessKey = existsSync(accessKeyPath);
|
|
246
|
+
report.checks.daemon = { ok: false, hasAccessKey, accessKeyPath };
|
|
247
|
+
if (!json) {
|
|
248
|
+
console.log('❌ daemon: not running / status failed');
|
|
249
|
+
if (!hasAccessKey) {
|
|
250
|
+
const stackName = (process.env.HAPPY_STACKS_STACK ?? process.env.HAPPY_LOCAL_STACK ?? '').trim() || 'main';
|
|
251
|
+
console.log(` ↪ likely cause: missing credentials at ${accessKeyPath}`);
|
|
252
|
+
console.log(` ↪ fix: authenticate for this stack:`);
|
|
253
|
+
console.log(` ${stackName === 'main' ? 'happys auth login' : `happys stack auth ${stackName} login`}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Tailscale Serve status (best-effort)
|
|
259
|
+
try {
|
|
260
|
+
const status = await tailscaleServeStatus();
|
|
261
|
+
const httpsLine = status.split('\n').find((l) => l.toLowerCase().includes('https://'))?.trim();
|
|
262
|
+
report.checks.tailscaleServe = { ok: true, httpsLine: httpsLine || null };
|
|
263
|
+
if (!json) console.log(`✅ tailscale serve: ${httpsLine ? httpsLine : 'configured'}`);
|
|
264
|
+
} catch {
|
|
265
|
+
report.checks.tailscaleServe = { ok: false };
|
|
266
|
+
if (!json) console.log('ℹ️ tailscale serve: unavailable (tailscale not installed / not running)');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// macOS LaunchAgent status
|
|
270
|
+
if (process.platform === 'darwin') {
|
|
271
|
+
try {
|
|
272
|
+
const list = await runCapture('launchctl', ['list']);
|
|
273
|
+
const { primaryLabel, legacyLabel } = getDefaultAutostartPaths();
|
|
274
|
+
const primaryLine = list.split('\n').find((l) => l.includes(primaryLabel))?.trim() || null;
|
|
275
|
+
const legacyLine = list.split('\n').find((l) => l.includes(legacyLabel))?.trim() || null;
|
|
276
|
+
const line = primaryLine || legacyLine;
|
|
277
|
+
report.checks.launchd = { ok: true, line: line || null };
|
|
278
|
+
if (!json) console.log(`✅ launchd: ${line ? line : 'not loaded'}`);
|
|
279
|
+
} catch {
|
|
280
|
+
report.checks.launchd = { ok: false };
|
|
281
|
+
if (!json) console.log('ℹ️ launchd: unable to query');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// SwiftBar plugin status (macOS)
|
|
286
|
+
if (process.platform === 'darwin') {
|
|
287
|
+
const pluginsDir = await resolveSwiftbarPluginsDir();
|
|
288
|
+
const pluginInstalled =
|
|
289
|
+
pluginsDir && existsSync(pluginsDir)
|
|
290
|
+
? Boolean((await runCapture('bash', ['-lc', `ls -1 "${pluginsDir}"/happy-stacks.*.sh 2>/dev/null | head -n 1 || true`])).trim())
|
|
291
|
+
: false;
|
|
292
|
+
report.checks.swiftbar = { ok: true, pluginsDir, pluginInstalled };
|
|
293
|
+
if (!json) {
|
|
294
|
+
console.log(`✅ swiftbar: ${pluginInstalled ? 'plugin installed' : 'not installed'}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// happy wrapper
|
|
299
|
+
try {
|
|
300
|
+
const happyPath = (await runCapture('sh', ['-lc', 'command -v happy'])).trim();
|
|
301
|
+
if (happyPath) {
|
|
302
|
+
report.checks.happyOnPath = { ok: true, path: happyPath };
|
|
303
|
+
if (!json) console.log(`✅ happy on PATH: ${happyPath}`);
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
report.checks.happyOnPath = { ok: false };
|
|
307
|
+
if (!json) console.log('ℹ️ happy on PATH: not found (run: happys init --install-path, or add ~/.happy-stacks/bin to PATH)');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// happys on PATH
|
|
311
|
+
try {
|
|
312
|
+
const happysPath = (await runCapture('sh', ['-lc', 'command -v happys'])).trim();
|
|
313
|
+
if (happysPath) {
|
|
314
|
+
report.checks.happysOnPath = { ok: true, path: happysPath };
|
|
315
|
+
if (!json) console.log(`✅ happys on PATH: ${happysPath}`);
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
report.checks.happysOnPath = { ok: false };
|
|
319
|
+
if (!json) console.log('ℹ️ happys on PATH: not found (run: happys init --install-path, or add ~/.happy-stacks/bin to PATH)');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!json) {
|
|
323
|
+
if (!runtimeVersion) {
|
|
324
|
+
console.log('');
|
|
325
|
+
console.log('Tips:');
|
|
326
|
+
console.log('- Install a stable runtime (recommended for SwiftBar/services): happys self update');
|
|
327
|
+
}
|
|
328
|
+
if (!report.checks.happysOnPath?.ok) {
|
|
329
|
+
console.log('- Add shims to PATH: export PATH="$HOME/.happy-stacks/bin:$PATH" (or: happys init --install-path)');
|
|
330
|
+
}
|
|
331
|
+
console.log('');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (json) {
|
|
335
|
+
printResult({ json, data: report });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
main().catch((err) => {
|
|
340
|
+
console.error('[local] doctor failed:', err);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import './utils/env.mjs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { parseArgs } from './utils/args.mjs';
|
|
7
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
8
|
+
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
|
|
9
|
+
|
|
10
|
+
function expandHome(p) {
|
|
11
|
+
return p.replace(/^~(?=\/)/, homedir());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveCliHomeDir() {
|
|
15
|
+
const fromExplicit = (process.env.HAPPY_HOME_DIR ?? '').trim();
|
|
16
|
+
if (fromExplicit) {
|
|
17
|
+
return expandHome(fromExplicit);
|
|
18
|
+
}
|
|
19
|
+
const fromStacks = (process.env.HAPPY_STACKS_CLI_HOME_DIR ?? process.env.HAPPY_LOCAL_CLI_HOME_DIR ?? '').trim();
|
|
20
|
+
if (fromStacks) {
|
|
21
|
+
return expandHome(fromStacks);
|
|
22
|
+
}
|
|
23
|
+
return join(getDefaultAutostartPaths().baseDir, 'cli');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
const argv = process.argv.slice(2);
|
|
28
|
+
const { flags } = parseArgs(argv);
|
|
29
|
+
const json = wantsJson(argv, { flags });
|
|
30
|
+
|
|
31
|
+
if (wantsHelp(argv, { flags })) {
|
|
32
|
+
printResult({
|
|
33
|
+
json,
|
|
34
|
+
data: { passthrough: true },
|
|
35
|
+
text: [
|
|
36
|
+
'[happy] usage:',
|
|
37
|
+
' happys happy <happy-cli args...>',
|
|
38
|
+
'',
|
|
39
|
+
'notes:',
|
|
40
|
+
' - This runs the `happy-cli` component from your configured workspace/components.',
|
|
41
|
+
' - It auto-fills HAPPY_HOME_DIR / HAPPY_SERVER_URL / HAPPY_WEBAPP_URL when missing.',
|
|
42
|
+
].join('\n'),
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rootDir = getRootDir(import.meta.url);
|
|
48
|
+
|
|
49
|
+
const portRaw = (process.env.HAPPY_STACKS_SERVER_PORT ?? process.env.HAPPY_LOCAL_SERVER_PORT ?? '').trim();
|
|
50
|
+
const port = portRaw ? Number(portRaw) : 3005;
|
|
51
|
+
const serverPort = Number.isFinite(port) ? port : 3005;
|
|
52
|
+
|
|
53
|
+
const internalServerUrl = `http://127.0.0.1:${serverPort}`;
|
|
54
|
+
const publicServerUrl = (process.env.HAPPY_STACKS_SERVER_URL ?? process.env.HAPPY_LOCAL_SERVER_URL ?? '').trim() || `http://localhost:${serverPort}`;
|
|
55
|
+
|
|
56
|
+
const cliHomeDir = resolveCliHomeDir();
|
|
57
|
+
|
|
58
|
+
const cliDir = getComponentDir(rootDir, 'happy-cli');
|
|
59
|
+
const entrypoint = join(cliDir, 'dist', 'index.mjs');
|
|
60
|
+
if (!existsSync(entrypoint)) {
|
|
61
|
+
throw new Error(`[happy] missing happy-cli build at: ${entrypoint}\nRun: happys bootstrap`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const env = { ...process.env };
|
|
65
|
+
env.HAPPY_HOME_DIR = env.HAPPY_HOME_DIR || cliHomeDir;
|
|
66
|
+
env.HAPPY_SERVER_URL = env.HAPPY_SERVER_URL || internalServerUrl;
|
|
67
|
+
env.HAPPY_WEBAPP_URL = env.HAPPY_WEBAPP_URL || publicServerUrl;
|
|
68
|
+
|
|
69
|
+
execFileSync(process.execPath, ['--no-warnings', '--no-deprecation', entrypoint, ...argv], {
|
|
70
|
+
stdio: 'inherit',
|
|
71
|
+
env,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main().catch((err) => {
|
|
76
|
+
console.error('[happy] failed:', err);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
|
79
|
+
|
package/scripts/init.mjs
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { ensureHomeEnvUpdated } from './utils/config.mjs';
|
|
7
|
+
|
|
8
|
+
function expandHome(p) {
|
|
9
|
+
return p.replace(/^~(?=\/)/, homedir());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getCliRootDir() {
|
|
13
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseArgValue(argv, key) {
|
|
17
|
+
const long = `--${key}=`;
|
|
18
|
+
const hit = argv.find((a) => a.startsWith(long));
|
|
19
|
+
if (hit) return hit.slice(long.length);
|
|
20
|
+
const idx = argv.indexOf(`--${key}`);
|
|
21
|
+
if (idx >= 0 && argv[idx + 1]) return argv[idx + 1];
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function writeExecutable(path, contents) {
|
|
26
|
+
await writeFile(path, contents, { mode: 0o755 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function escapeForDoubleQuotes(s) {
|
|
30
|
+
return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function ensurePathInstalled({ homeDir }) {
|
|
34
|
+
const shell = (process.env.SHELL ?? '').toLowerCase();
|
|
35
|
+
const isDarwin = process.platform === 'darwin';
|
|
36
|
+
|
|
37
|
+
const zshrc = join(homedir(), '.zshrc');
|
|
38
|
+
const bashrc = join(homedir(), '.bashrc');
|
|
39
|
+
const bashProfile = join(homedir(), '.bash_profile');
|
|
40
|
+
const fishDir = join(homedir(), '.config', 'fish', 'conf.d');
|
|
41
|
+
const fishConf = join(fishDir, 'happy-stacks.fish');
|
|
42
|
+
|
|
43
|
+
const markerStart = '# >>> happy-stacks >>>';
|
|
44
|
+
const markerEnd = '# <<< happy-stacks <<<';
|
|
45
|
+
|
|
46
|
+
const lineSh = `export PATH="${escapeForDoubleQuotes(join(homeDir, 'bin'))}:$PATH"`;
|
|
47
|
+
const blockSh = `\n${markerStart}\n${lineSh}\n${markerEnd}\n`;
|
|
48
|
+
|
|
49
|
+
const lineFish = `set -gx PATH "${escapeForDoubleQuotes(join(homeDir, 'bin'))}" $PATH`;
|
|
50
|
+
const blockFish = `\n${markerStart}\n${lineFish}\n${markerEnd}\n`;
|
|
51
|
+
|
|
52
|
+
const writeIfMissing = async (path, block) => {
|
|
53
|
+
let existing = '';
|
|
54
|
+
try {
|
|
55
|
+
existing = await readFile(path, 'utf-8');
|
|
56
|
+
} catch {
|
|
57
|
+
existing = '';
|
|
58
|
+
}
|
|
59
|
+
if (existing.includes(markerStart) || existing.includes(lineSh) || existing.includes(lineFish)) {
|
|
60
|
+
return { updated: false, path };
|
|
61
|
+
}
|
|
62
|
+
await writeFile(path, existing.replace(/\s*$/, '') + block, 'utf-8');
|
|
63
|
+
return { updated: true, path };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (shell.includes('fish')) {
|
|
67
|
+
await mkdir(fishDir, { recursive: true });
|
|
68
|
+
return await writeIfMissing(fishConf, blockFish);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (shell.includes('bash')) {
|
|
72
|
+
// macOS interactive bash typically sources ~/.bash_profile; linux usually uses ~/.bashrc.
|
|
73
|
+
const target = isDarwin ? bashProfile : bashrc;
|
|
74
|
+
return await writeIfMissing(target, blockSh);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Default to zsh on modern macOS; also fine for linux users.
|
|
78
|
+
return await writeIfMissing(zshrc, blockSh);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function main() {
|
|
82
|
+
const rawArgv = process.argv.slice(2);
|
|
83
|
+
const sep = rawArgv.indexOf('--');
|
|
84
|
+
const argv = sep >= 0 ? rawArgv.slice(0, sep) : rawArgv;
|
|
85
|
+
const bootstrapArgsRaw = sep >= 0 ? rawArgv.slice(sep + 1) : [];
|
|
86
|
+
const bootstrapArgs = bootstrapArgsRaw[0] === '--' ? bootstrapArgsRaw.slice(1) : bootstrapArgsRaw;
|
|
87
|
+
if (argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') {
|
|
88
|
+
console.log([
|
|
89
|
+
'[init] usage:',
|
|
90
|
+
' happys init [--home-dir=/path] [--workspace-dir=/path] [--runtime-dir=/path] [--install-path] [--no-runtime] [--no-bootstrap] [--] [bootstrap args...]',
|
|
91
|
+
'',
|
|
92
|
+
'notes:',
|
|
93
|
+
' - writes ~/.happy-stacks/.env (stable pointer file)',
|
|
94
|
+
' - default workspace: ~/.happy-stacks/workspace',
|
|
95
|
+
' - default runtime: ~/.happy-stacks/runtime (recommended for services/SwiftBar)',
|
|
96
|
+
' - optional: --install-path adds ~/.happy-stacks/bin to your shell PATH (idempotent)',
|
|
97
|
+
' - by default, runs `happys bootstrap --interactive` at the end (TTY only)',
|
|
98
|
+
].join('\n'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const cliRootDir = getCliRootDir();
|
|
103
|
+
|
|
104
|
+
const homeDirRaw = parseArgValue(argv, 'home-dir');
|
|
105
|
+
const homeDir = expandHome((homeDirRaw ?? '').trim() || (process.env.HAPPY_STACKS_HOME_DIR ?? '').trim() || join(homedir(), '.happy-stacks'));
|
|
106
|
+
process.env.HAPPY_STACKS_HOME_DIR = homeDir;
|
|
107
|
+
|
|
108
|
+
const workspaceDirRaw = parseArgValue(argv, 'workspace-dir');
|
|
109
|
+
const workspaceDir = expandHome((workspaceDirRaw ?? '').trim() || join(homeDir, 'workspace'));
|
|
110
|
+
process.env.HAPPY_STACKS_WORKSPACE_DIR = process.env.HAPPY_STACKS_WORKSPACE_DIR ?? workspaceDir;
|
|
111
|
+
|
|
112
|
+
const runtimeDirRaw = parseArgValue(argv, 'runtime-dir');
|
|
113
|
+
const runtimeDir = expandHome((runtimeDirRaw ?? '').trim() || (process.env.HAPPY_STACKS_RUNTIME_DIR ?? '').trim() || join(homeDir, 'runtime'));
|
|
114
|
+
process.env.HAPPY_STACKS_RUNTIME_DIR = process.env.HAPPY_STACKS_RUNTIME_DIR ?? runtimeDir;
|
|
115
|
+
|
|
116
|
+
const nodePath = process.execPath;
|
|
117
|
+
|
|
118
|
+
await mkdir(homeDir, { recursive: true });
|
|
119
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
120
|
+
await mkdir(join(workspaceDir, 'components'), { recursive: true });
|
|
121
|
+
await mkdir(runtimeDir, { recursive: true });
|
|
122
|
+
await mkdir(join(homeDir, 'bin'), { recursive: true });
|
|
123
|
+
|
|
124
|
+
await ensureHomeEnvUpdated({
|
|
125
|
+
updates: [
|
|
126
|
+
{ key: 'HAPPY_STACKS_HOME_DIR', value: homeDir },
|
|
127
|
+
{ key: 'HAPPY_STACKS_WORKSPACE_DIR', value: workspaceDir },
|
|
128
|
+
{ key: 'HAPPY_STACKS_RUNTIME_DIR', value: runtimeDir },
|
|
129
|
+
{ key: 'HAPPY_STACKS_NODE', value: nodePath },
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const installRuntime = !argv.includes('--no-runtime');
|
|
134
|
+
if (installRuntime) {
|
|
135
|
+
const pkg = JSON.parse(await readFile(join(cliRootDir, 'package.json'), 'utf-8'));
|
|
136
|
+
const version = String(pkg.version ?? '').trim() || 'latest';
|
|
137
|
+
const spec = version === '0.0.0' ? 'happy-stacks@latest' : `happy-stacks@${version}`;
|
|
138
|
+
|
|
139
|
+
console.log(`[init] installing runtime into ${runtimeDir} (${spec})...`);
|
|
140
|
+
let res = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--silent', '--prefix', runtimeDir, spec], { stdio: 'inherit' });
|
|
141
|
+
if (res.status !== 0) {
|
|
142
|
+
// Pre-publish developer experience: if the package isn't on npm yet (E404),
|
|
143
|
+
// fall back to installing the local checkout into the runtime prefix.
|
|
144
|
+
console.log(`[init] runtime install failed; attempting local install from ${cliRootDir}...`);
|
|
145
|
+
res = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--silent', '--prefix', runtimeDir, cliRootDir], { stdio: 'inherit' });
|
|
146
|
+
if (res.status !== 0) {
|
|
147
|
+
process.exit(res.status ?? 1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const happysShimPath = join(homeDir, 'bin', 'happys');
|
|
153
|
+
const happyShimPath = join(homeDir, 'bin', 'happy');
|
|
154
|
+
const shim = [
|
|
155
|
+
'#!/bin/bash',
|
|
156
|
+
'set -euo pipefail',
|
|
157
|
+
'HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"',
|
|
158
|
+
'ENV_FILE="$HOME_DIR/.env"',
|
|
159
|
+
'NODE_BIN=""',
|
|
160
|
+
'if [[ -f "$ENV_FILE" ]]; then',
|
|
161
|
+
' NODE_BIN="$(grep -E \'^HAPPY_STACKS_NODE=\' "$ENV_FILE" | head -n 1 | sed \'s/^HAPPY_STACKS_NODE=//\')"',
|
|
162
|
+
'fi',
|
|
163
|
+
'if [[ -z "$NODE_BIN" ]]; then',
|
|
164
|
+
' NODE_BIN="$(command -v node 2>/dev/null || true)"',
|
|
165
|
+
'fi',
|
|
166
|
+
'RUNTIME_DIR="${HAPPY_STACKS_RUNTIME_DIR:-$HOME_DIR/runtime}"',
|
|
167
|
+
'ENTRY="$RUNTIME_DIR/node_modules/happy-stacks/bin/happys.mjs"',
|
|
168
|
+
'if [[ -f "$ENTRY" ]]; then',
|
|
169
|
+
' exec "$NODE_BIN" "$ENTRY" "$@"',
|
|
170
|
+
'fi',
|
|
171
|
+
'exec happys "$@"',
|
|
172
|
+
'',
|
|
173
|
+
].join('\n');
|
|
174
|
+
|
|
175
|
+
await writeExecutable(happysShimPath, shim);
|
|
176
|
+
await writeExecutable(happyShimPath, `#!/bin/bash\nset -euo pipefail\nexec \"${happysShimPath}\" happy \"$@\"\n`);
|
|
177
|
+
|
|
178
|
+
if (argv.includes('--install-path')) {
|
|
179
|
+
const res = await ensurePathInstalled({ homeDir });
|
|
180
|
+
if (res.updated) {
|
|
181
|
+
console.log(`[init] added ${homeDir}/bin to PATH via ${res.path}`);
|
|
182
|
+
} else {
|
|
183
|
+
console.log(`[init] PATH already configured in ${res.path}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log('[init] complete');
|
|
188
|
+
console.log(`[init] home: ${homeDir}`);
|
|
189
|
+
console.log(`[init] workspace: ${workspaceDir}`);
|
|
190
|
+
console.log(`[init] shims: ${homeDir}/bin`);
|
|
191
|
+
console.log('');
|
|
192
|
+
|
|
193
|
+
if (!argv.includes('--install-path')) {
|
|
194
|
+
console.log('[init] note: to use `happys` / `happy` from any terminal, add shims to PATH:');
|
|
195
|
+
console.log(` export PATH="${homeDir}/bin:$PATH"`);
|
|
196
|
+
console.log(' (or re-run: happys init --install-path)');
|
|
197
|
+
console.log('');
|
|
198
|
+
} else {
|
|
199
|
+
console.log('[init] note: restart your terminal (or source your shell config) to pick up PATH changes.');
|
|
200
|
+
console.log('');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const wantBootstrap = !argv.includes('--no-bootstrap');
|
|
204
|
+
const isTty = process.stdout.isTTY && process.stdin.isTTY;
|
|
205
|
+
const shouldBootstrap = wantBootstrap;
|
|
206
|
+
|
|
207
|
+
if (shouldBootstrap) {
|
|
208
|
+
const nextArgs = [...bootstrapArgs];
|
|
209
|
+
if (isTty && !nextArgs.includes('--interactive') && !nextArgs.includes('-i')) {
|
|
210
|
+
nextArgs.unshift('--interactive');
|
|
211
|
+
}
|
|
212
|
+
console.log('[init] running bootstrap...');
|
|
213
|
+
const res = spawnSync(process.execPath, [join(cliRootDir, 'scripts', 'install.mjs'), ...nextArgs], {
|
|
214
|
+
stdio: 'inherit',
|
|
215
|
+
env: process.env,
|
|
216
|
+
cwd: cliRootDir,
|
|
217
|
+
});
|
|
218
|
+
if (res.status !== 0) {
|
|
219
|
+
process.exit(res.status ?? 1);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log('[init] next steps:');
|
|
225
|
+
console.log(` export PATH=\"${homeDir}/bin:$PATH\"`);
|
|
226
|
+
console.log(' happys bootstrap --interactive');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
main().catch((err) => {
|
|
230
|
+
console.error('[init] failed:', err);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
});
|