@happier-dev/stack 0.1.0-preview.17.1 → 0.1.0-preview.21.1
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/docs/server-flavors.md +6 -6
- package/node_modules/@happier-dev/cli-common/dist/index.d.ts +2 -0
- package/node_modules/@happier-dev/cli-common/dist/index.d.ts.map +1 -1
- package/node_modules/@happier-dev/cli-common/dist/index.js +2 -0
- package/node_modules/@happier-dev/cli-common/dist/index.js.map +1 -1
- package/node_modules/@happier-dev/cli-common/dist/providers/index.d.ts +51 -0
- package/node_modules/@happier-dev/cli-common/dist/providers/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/providers/index.js +129 -0
- package/node_modules/@happier-dev/cli-common/dist/providers/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.d.ts +5 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.js +5 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.d.ts +19 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.js +117 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.d.ts +55 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.js +302 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.d.ts +12 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.js +75 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.d.ts +8 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.js +29 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/package.json +11 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.d.ts +22 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.js +44 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.d.ts +5 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.js +21 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.d.ts +14 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.js +39 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.d.ts +20 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.js +60 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.d.ts +7 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.js +7 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.d.ts +7 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.js +92 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.d.ts +26 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.js +53 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/package.json +38 -0
- package/package.json +4 -2
- package/scripts/auth.mjs +3 -2
- package/scripts/auth_copy_from_pglite_lock_in_use.integration.test.mjs +1 -0
- package/scripts/auth_copy_from_runCapture.integration.test.mjs +8 -1
- package/scripts/auth_login_guided_server_no_expo.test.mjs +2 -0
- package/scripts/build.mjs +3 -18
- package/scripts/bundleWorkspaceDeps.mjs +5 -1
- package/scripts/bundleWorkspaceDeps.test.mjs +42 -1
- package/scripts/mobile.mjs +30 -2
- package/scripts/mobile_dev_client.mjs +7 -32
- package/scripts/mobile_dev_client_help_smoke.test.mjs +24 -0
- package/scripts/mobile_prebuild_happyDir_defined.test.mjs +47 -0
- package/scripts/mobile_prebuild_sets_rct_metro_port.test.mjs +81 -0
- package/scripts/mobile_run_ios_passes_port.integration.test.mjs +103 -0
- package/scripts/mobile_run_ios_uses_long_port_flag.test.mjs +106 -0
- package/scripts/providers_cmd.mjs +262 -0
- package/scripts/release_binary_smoke.integration.test.mjs +45 -37
- package/scripts/remote_cmd.mjs +352 -0
- package/scripts/self_host_daemon.real.integration.test.mjs +296 -0
- package/scripts/self_host_launchd.real.integration.test.mjs +211 -0
- package/scripts/self_host_runtime.mjs +1829 -327
- package/scripts/self_host_runtime.test.mjs +523 -1
- package/scripts/self_host_schtasks.real.integration.test.mjs +217 -0
- package/scripts/self_host_service_e2e_harness.mjs +93 -0
- package/scripts/self_host_systemd.real.integration.test.mjs +8 -86
- package/scripts/service.mjs +156 -26
- package/scripts/stack/command_arguments.mjs +1 -0
- package/scripts/stack/help_text.mjs +3 -1
- package/scripts/stack.mjs +2 -1
- package/scripts/stack_daemon_cmd.integration.test.mjs +37 -0
- package/scripts/stack_happy_cmd.integration.test.mjs +36 -0
- package/scripts/stack_pr_help_cmd.test.mjs +38 -0
- package/scripts/stop.mjs +2 -3
- package/scripts/utils/auth/credentials_paths.mjs +9 -9
- package/scripts/utils/auth/credentials_paths.test.mjs +8 -0
- package/scripts/utils/auth/orchestrated_stack_auth_flow.mjs +64 -3
- package/scripts/utils/auth/stable_scope_id.mjs +1 -1
- package/scripts/utils/cli/cli_registry.mjs +21 -0
- package/scripts/utils/cli/progress.mjs +8 -1
- package/scripts/utils/cli/progress.test.mjs +43 -0
- package/scripts/utils/dev/expo_dev.buildEnv.test.mjs +17 -0
- package/scripts/utils/dev/expo_dev.mjs +35 -5
- package/scripts/utils/dev/expo_dev_restart_port_reservation.test.mjs +180 -1
- package/scripts/utils/dev/expo_dev_runtime_metadata.test.mjs +126 -0
- package/scripts/utils/dev/expo_dev_verbose_logs.test.mjs +9 -2
- package/scripts/utils/mobile/dev_client_install_invocation.mjs +68 -0
- package/scripts/utils/mobile/dev_client_install_invocation.test.mjs +27 -0
- package/scripts/utils/server/port.mjs +20 -2
- package/scripts/utils/service/service_manager.definition.test.mjs +66 -0
- package/scripts/utils/service/service_manager.mjs +96 -0
- package/scripts/utils/service/service_manager.plan.test.mjs +37 -0
- package/scripts/utils/service/service_manager.test.mjs +20 -0
- package/scripts/utils/service/systemd_service_unit.mjs +1 -0
- package/scripts/utils/service/systemd_service_unit.test.mjs +42 -0
- package/scripts/utils/service/windows_schtasks_wrapper.mjs +1 -0
- package/scripts/utils/service/windows_schtasks_wrapper.test.mjs +25 -0
- package/scripts/utils/ui/ui_export_env.mjs +29 -0
- package/scripts/utils/ui/ui_export_env.test.mjs +25 -0
- package/scripts/worktrees.mjs +3 -0
- package/scripts/worktrees_status_default_target.test.mjs +56 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
4
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
5
|
+
import { createStepPrinter, runCommandLogged } from './utils/cli/progress.mjs';
|
|
6
|
+
import { AGENT_IDS, getProviderCliInstallSpec } from '@happier-dev/agents';
|
|
7
|
+
import { installProviderCli, planProviderCliInstall, resolvePlatformFromNodePlatform } from '@happier-dev/cli-common/providers';
|
|
8
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
|
|
13
|
+
function usageText() {
|
|
14
|
+
return [
|
|
15
|
+
'[providers] usage:',
|
|
16
|
+
' hstack providers list [--json]',
|
|
17
|
+
' hstack providers install --providers=<id1,id2> [--dry-run] [--force] [--json]',
|
|
18
|
+
' hstack providers install <id1> <id2> [--dry-run] [--force] [--json]',
|
|
19
|
+
'',
|
|
20
|
+
'notes:',
|
|
21
|
+
' - Provider CLIs are external binaries used by Happier backends (claude/codex/gemini/etc).',
|
|
22
|
+
' - This command installs provider CLIs (best-effort). Some providers require manual installation.',
|
|
23
|
+
' - Claude install uses the upstream native installer by default (not npm).',
|
|
24
|
+
' - Use --force to re-run the installer even if the binary is already present on PATH.',
|
|
25
|
+
].join('\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function splitProviders(raw) {
|
|
29
|
+
const v = String(raw ?? '').trim();
|
|
30
|
+
if (!v) return [];
|
|
31
|
+
return v
|
|
32
|
+
.split(',')
|
|
33
|
+
.map((s) => s.trim().toLowerCase())
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolvePlatform() {
|
|
38
|
+
return resolvePlatformFromNodePlatform(process.platform) ?? 'unsupported';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function commandExists(cmd, env) {
|
|
42
|
+
const name = String(cmd ?? '').trim();
|
|
43
|
+
if (!name) return false;
|
|
44
|
+
|
|
45
|
+
const pathEnv = env?.PATH ?? process.env.PATH;
|
|
46
|
+
if (process.platform === 'win32') {
|
|
47
|
+
const res = spawnSync('where', [name], { stdio: 'ignore', env: { ...process.env, ...(env ?? {}), PATH: pathEnv } });
|
|
48
|
+
return (res.status ?? 1) === 0;
|
|
49
|
+
}
|
|
50
|
+
const res = spawnSync('sh', ['-lc', `command -v ${name} >/dev/null 2>&1`], { stdio: 'ignore', env: { ...process.env, ...(env ?? {}), PATH: pathEnv } });
|
|
51
|
+
return (res.status ?? 1) === 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveProviderInstallLogPath(providerId) {
|
|
55
|
+
const base = join(tmpdir(), 'happier-provider-installs');
|
|
56
|
+
mkdirSync(base, { recursive: true });
|
|
57
|
+
return join(base, `install-provider-${providerId}-${Date.now()}.log`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function planForProvider(providerId) {
|
|
61
|
+
const platform = resolvePlatform();
|
|
62
|
+
if (platform === 'unsupported') {
|
|
63
|
+
return { ok: false, provider: providerId, error: 'Unsupported platform' };
|
|
64
|
+
}
|
|
65
|
+
const planned = planProviderCliInstall({ providerId, platform });
|
|
66
|
+
if (!planned.ok) {
|
|
67
|
+
return { ok: false, provider: providerId, error: planned.errorMessage };
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, provider: providerId, commands: planned.plan.commands };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function cmdList({ argv }) {
|
|
73
|
+
const { flags } = parseArgs(argv);
|
|
74
|
+
const json = wantsJson(argv, { flags });
|
|
75
|
+
const platform = resolvePlatform();
|
|
76
|
+
const rows = AGENT_IDS.map((id) => {
|
|
77
|
+
const spec = getProviderCliInstallSpec(id);
|
|
78
|
+
const planned = planForProvider(id);
|
|
79
|
+
return {
|
|
80
|
+
id: spec.id,
|
|
81
|
+
title: spec.title,
|
|
82
|
+
binaries: spec.binaries,
|
|
83
|
+
autoInstall: planned.ok,
|
|
84
|
+
note: planned.ok ? null : planned.error,
|
|
85
|
+
platform,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
printResult({
|
|
90
|
+
json,
|
|
91
|
+
data: { ok: true, platform, providers: rows },
|
|
92
|
+
text: json
|
|
93
|
+
? null
|
|
94
|
+
: rows
|
|
95
|
+
.map((r) => `${r.autoInstall ? '✓' : '-'} ${r.id}${r.title ? ` (${r.title})` : ''}${r.note ? ` — ${r.note}` : ''}`)
|
|
96
|
+
.join('\n'),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function cmdInstall({ argv }) {
|
|
101
|
+
const { flags, kv } = parseArgs(argv);
|
|
102
|
+
const json = wantsJson(argv, { flags });
|
|
103
|
+
const dryRun = flags.has('--dry-run') || flags.has('--plan');
|
|
104
|
+
const force = flags.has('--force') || flags.has('--reinstall');
|
|
105
|
+
const skipIfInstalled = !force;
|
|
106
|
+
|
|
107
|
+
const positionals = argv.filter((a) => a && a !== '--' && !a.startsWith('-'));
|
|
108
|
+
const inputFromFlag = kv.get('--providers') ?? '';
|
|
109
|
+
const inputFromPositional = positionals;
|
|
110
|
+
|
|
111
|
+
const wanted = [
|
|
112
|
+
...splitProviders(inputFromFlag),
|
|
113
|
+
...inputFromPositional.flatMap((s) => splitProviders(String(s).trim().toLowerCase())),
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
if (wanted.length === 0) {
|
|
117
|
+
throw new Error('[providers] missing providers. Use --providers=claude,codex or pass ids as positionals.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const resolved = wanted.map((id) => {
|
|
121
|
+
if (!AGENT_IDS.includes(id)) {
|
|
122
|
+
const e = new Error(`[providers] unknown provider: ${id}`);
|
|
123
|
+
e.code = 'EUNKNOWN_PROVIDER';
|
|
124
|
+
throw e;
|
|
125
|
+
}
|
|
126
|
+
return id;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const platform = resolvePlatform();
|
|
130
|
+
if (platform === 'unsupported') {
|
|
131
|
+
throw new Error('[providers] unsupported platform');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// In json mode, preserve the existing structured behavior (no progress output).
|
|
135
|
+
if (json) {
|
|
136
|
+
const results = resolved.map((providerId) =>
|
|
137
|
+
installProviderCli({ providerId, platform, dryRun, skipIfInstalled, env: process.env }),
|
|
138
|
+
);
|
|
139
|
+
const failures = results.filter((r) => !r.ok);
|
|
140
|
+
if (failures.length > 0) {
|
|
141
|
+
const first = failures[0];
|
|
142
|
+
const extra = first.logPath ? `\nlog: ${first.logPath}` : '';
|
|
143
|
+
throw new Error(`[providers] install failed: ${first.errorMessage}${extra}`.trim());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const plan = results.map((r) => (r.ok ? r.plan : null)).filter(Boolean);
|
|
147
|
+
|
|
148
|
+
printResult({
|
|
149
|
+
json,
|
|
150
|
+
data: {
|
|
151
|
+
ok: true,
|
|
152
|
+
providers: resolved,
|
|
153
|
+
dryRun,
|
|
154
|
+
skipIfInstalled,
|
|
155
|
+
plan,
|
|
156
|
+
results: results.map((r) => (r.ok ? { ok: true, providerId: r.plan.providerId, alreadyInstalled: r.alreadyInstalled, logPath: r.logPath } : r)),
|
|
157
|
+
},
|
|
158
|
+
text: null,
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Human-friendly progress output (TTY spinner when interactive; simple lines otherwise).
|
|
164
|
+
const steps = createStepPrinter({ enabled: true });
|
|
165
|
+
const results = [];
|
|
166
|
+
for (const providerId of resolved) {
|
|
167
|
+
const spec = getProviderCliInstallSpec(providerId);
|
|
168
|
+
const planned = planProviderCliInstall({ providerId, platform });
|
|
169
|
+
if (!planned.ok) {
|
|
170
|
+
throw new Error(`[providers] install failed: ${planned.errorMessage}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const label = `Installing ${spec.title || `${providerId} CLI`}`;
|
|
174
|
+
const binariesPresent = skipIfInstalled && spec.binaries.every((b) => commandExists(b, process.env));
|
|
175
|
+
if (binariesPresent) {
|
|
176
|
+
steps.info(`- [✓] ${label} (already installed)`);
|
|
177
|
+
results.push({ ok: true, providerId, alreadyInstalled: true, logPath: null, plan: planned.plan });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
steps.start(label);
|
|
182
|
+
if (dryRun) {
|
|
183
|
+
steps.stop('✓', `${label} (dry-run)`);
|
|
184
|
+
results.push({ ok: true, providerId, alreadyInstalled: false, logPath: null, plan: planned.plan });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const logPath = resolveProviderInstallLogPath(providerId);
|
|
189
|
+
writeFileSync(
|
|
190
|
+
logPath,
|
|
191
|
+
[`# providerId: ${providerId}`, `# platform: ${platform}`, ''].join('\n'),
|
|
192
|
+
'utf8',
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
for (const c of planned.plan.commands) {
|
|
197
|
+
// eslint-disable-next-line no-await-in-loop
|
|
198
|
+
await runCommandLogged({
|
|
199
|
+
label,
|
|
200
|
+
cmd: c.cmd,
|
|
201
|
+
args: c.args,
|
|
202
|
+
cwd: process.cwd(),
|
|
203
|
+
env: process.env,
|
|
204
|
+
logPath,
|
|
205
|
+
showSteps: false,
|
|
206
|
+
quiet: true,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
steps.stop('x', label);
|
|
211
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
212
|
+
throw new Error(`[providers] install failed: ${message}\nlog: ${logPath}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
steps.stop('✓', label);
|
|
216
|
+
results.push({ ok: true, providerId, alreadyInstalled: false, logPath, plan: planned.plan });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
printResult({
|
|
220
|
+
json,
|
|
221
|
+
data: {
|
|
222
|
+
ok: true,
|
|
223
|
+
providers: resolved,
|
|
224
|
+
dryRun,
|
|
225
|
+
skipIfInstalled,
|
|
226
|
+
plan: results.map((r) => r.plan),
|
|
227
|
+
results: results.map((r) => ({ ok: true, providerId: r.providerId, alreadyInstalled: r.alreadyInstalled, logPath: r.logPath })),
|
|
228
|
+
},
|
|
229
|
+
text: json ? null : `✓ providers installed: ${resolved.join(', ')}`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function main() {
|
|
234
|
+
const argv = process.argv.slice(2);
|
|
235
|
+
const { flags } = parseArgs(argv);
|
|
236
|
+
const json = wantsJson(argv, { flags });
|
|
237
|
+
|
|
238
|
+
if (argv.length === 0 || wantsHelp(argv, { flags })) {
|
|
239
|
+
printResult({ json, data: { usage: usageText() }, text: usageText() });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const positionals = argv.filter((a) => a && a !== '--' && !a.startsWith('-'));
|
|
244
|
+
const sub = String(positionals[0] ?? '').trim();
|
|
245
|
+
if (sub === 'list') {
|
|
246
|
+
await cmdList({ argv: argv.slice(1) });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (sub === 'install') {
|
|
250
|
+
await cmdInstall({ argv: argv.slice(1) });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
printResult({ json, data: { usage: usageText() }, text: usageText() });
|
|
255
|
+
process.exit(2);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
main().catch((error) => {
|
|
259
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
260
|
+
process.stderr.write(`${msg}\n`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
});
|
|
@@ -27,23 +27,23 @@ function commandExists(cmd) {
|
|
|
27
27
|
return spawnSync('bash', ['-lc', `command -v ${cmd} >/dev/null 2>&1`], { stdio: 'ignore' }).status === 0;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function runWithHardTimeout(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
maxBuffer,
|
|
30
|
+
function runWithHardTimeout(command, args, options) {
|
|
31
|
+
const timeoutMs = Number(options.timeout ?? 0);
|
|
32
|
+
if (process.platform === 'linux' && commandExists('timeout') && timeoutMs > 0) {
|
|
33
|
+
const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000));
|
|
34
|
+
return spawnSync('timeout', ['--signal=KILL', '--kill-after=30s', `${timeoutSeconds}s`, command, ...args], {
|
|
35
|
+
...options,
|
|
36
|
+
timeout: undefined,
|
|
38
37
|
});
|
|
39
38
|
}
|
|
40
|
-
return spawnSync(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
return spawnSync(command, args, options);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function didCommandTimeout(result) {
|
|
43
|
+
if (result?.error?.code === 'ETIMEDOUT') return true;
|
|
44
|
+
if (result?.status === 124 || result?.status === 137) return true;
|
|
45
|
+
if (result?.signal === 'SIGKILL') return true;
|
|
46
|
+
return false;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function currentTarget() {
|
|
@@ -84,20 +84,25 @@ test('compiled happier and server binaries execute from isolated cwd', async (t)
|
|
|
84
84
|
const repoRoot = resolve(fileURLToPath(new URL('../../..', import.meta.url)));
|
|
85
85
|
const version = `0.0.0-smoke.${Date.now()}`;
|
|
86
86
|
|
|
87
|
-
const buildCli = runWithHardTimeout(
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
const buildCli = runWithHardTimeout(
|
|
88
|
+
process.execPath,
|
|
89
|
+
[
|
|
90
90
|
'scripts/release/build-cli-binaries.mjs',
|
|
91
91
|
'--channel=preview',
|
|
92
92
|
`--version=${version}`,
|
|
93
93
|
`--targets=${target}`,
|
|
94
94
|
],
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
{
|
|
96
|
+
cwd: repoRoot,
|
|
97
|
+
encoding: 'utf-8',
|
|
98
|
+
env: { ...process.env },
|
|
99
|
+
// `inherit` makes it easier to read interactively, but on CI failures we need the full output.
|
|
100
|
+
// Increase buffer because build logs can be large.
|
|
101
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
102
|
+
// If this ever hangs on CI, fail with a clear timeout rather than blocking the entire suite.
|
|
103
|
+
timeout: 15 * 60 * 1000,
|
|
104
|
+
}
|
|
105
|
+
);
|
|
101
106
|
assert.equal(buildCli.status, 0, formatSpawnSyncResult(buildCli));
|
|
102
107
|
|
|
103
108
|
const cliArtifactPath = join(repoRoot, 'dist', 'release-assets', 'cli', `happier-v${version}-${target}.tar.gz`);
|
|
@@ -105,13 +110,13 @@ test('compiled happier and server binaries execute from isolated cwd', async (t)
|
|
|
105
110
|
t.after(() => {
|
|
106
111
|
spawnSync('bash', ['-lc', `rm -rf "${cliExtract.extractDir.replaceAll('"', '\\"')}"`], { stdio: 'ignore' });
|
|
107
112
|
});
|
|
108
|
-
const cliVersion =
|
|
113
|
+
const cliVersion = runWithHardTimeout(cliExtract.binaryPath, ['--version'], {
|
|
109
114
|
cwd: '/tmp',
|
|
110
115
|
encoding: 'utf-8',
|
|
111
116
|
env: { ...process.env, HAPPIER_NONINTERACTIVE: '1' },
|
|
112
117
|
timeout: 7000,
|
|
113
118
|
});
|
|
114
|
-
const cliTimedOut = cliVersion
|
|
119
|
+
const cliTimedOut = didCommandTimeout(cliVersion);
|
|
115
120
|
const cliExited = (cliVersion.status ?? 1) === 0;
|
|
116
121
|
assert.ok(cliTimedOut || cliExited, cliVersion.stderr || cliVersion.stdout);
|
|
117
122
|
const versionText = `${cliVersion.stdout || ''}${cliVersion.stderr || ''}`.trim();
|
|
@@ -121,20 +126,23 @@ test('compiled happier and server binaries execute from isolated cwd', async (t)
|
|
|
121
126
|
);
|
|
122
127
|
|
|
123
128
|
if (isLinuxTarget(target)) {
|
|
124
|
-
const buildServer = runWithHardTimeout(
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
const buildServer = runWithHardTimeout(
|
|
130
|
+
process.execPath,
|
|
131
|
+
[
|
|
127
132
|
'scripts/release/build-server-binaries.mjs',
|
|
128
133
|
'--channel=preview',
|
|
129
134
|
`--version=${version}`,
|
|
130
135
|
`--targets=${target}`,
|
|
131
136
|
],
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
{
|
|
138
|
+
cwd: repoRoot,
|
|
139
|
+
encoding: 'utf-8',
|
|
140
|
+
env: { ...process.env },
|
|
141
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
142
|
+
// If this ever hangs on CI, fail with a clear timeout rather than blocking the entire suite.
|
|
143
|
+
timeout: 15 * 60 * 1000,
|
|
144
|
+
}
|
|
145
|
+
);
|
|
138
146
|
assert.equal(buildServer.status, 0, formatSpawnSyncResult(buildServer));
|
|
139
147
|
|
|
140
148
|
const serverArtifactPath = join(repoRoot, 'dist', 'release-assets', 'server', `happier-server-v${version}-${target}.tar.gz`);
|
|
@@ -146,7 +154,7 @@ test('compiled happier and server binaries execute from isolated cwd', async (t)
|
|
|
146
154
|
t.after(() => {
|
|
147
155
|
spawnSync('bash', ['-lc', `rm -rf "${serverDataDir.replaceAll('"', '\\"')}"`], { stdio: 'ignore' });
|
|
148
156
|
});
|
|
149
|
-
const serverBoot =
|
|
157
|
+
const serverBoot = runWithHardTimeout(serverExtract.binaryPath, [], {
|
|
150
158
|
cwd: '/tmp',
|
|
151
159
|
encoding: 'utf-8',
|
|
152
160
|
env: {
|
|
@@ -161,7 +169,7 @@ test('compiled happier and server binaries execute from isolated cwd', async (t)
|
|
|
161
169
|
},
|
|
162
170
|
timeout: 7000,
|
|
163
171
|
});
|
|
164
|
-
const timedOut = serverBoot
|
|
172
|
+
const timedOut = didCommandTimeout(serverBoot);
|
|
165
173
|
const cleanExit = (serverBoot.status ?? 1) === 0;
|
|
166
174
|
const serverOutput = `${serverBoot.stderr || ''}\n${serverBoot.stdout || ''}`;
|
|
167
175
|
assert.ok(timedOut || cleanExit, serverOutput);
|