@happier-dev/stack 0.1.0-preview.100.1 → 0.1.0-preview.138.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 +15 -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 +97 -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/build.mjs +3 -18
- package/scripts/bundleWorkspaceDeps.mjs +5 -1
- package/scripts/bundleWorkspaceDeps.test.mjs +16 -0
- package/scripts/mobile.mjs +32 -1
- 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 +101 -0
- package/scripts/providers_cmd.mjs +262 -0
- package/scripts/release_binary_smoke.integration.test.mjs +25 -6
- package/scripts/remote_cmd.mjs +240 -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 +1403 -312
- package/scripts/self_host_runtime.test.mjs +361 -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 +2 -0
- package/scripts/stack_daemon_cmd.integration.test.mjs +37 -0
- package/scripts/stack_happy_cmd.integration.test.mjs +36 -0
- package/scripts/utils/auth/credentials_paths.mjs +9 -9
- package/scripts/utils/auth/credentials_paths.test.mjs +8 -0
- package/scripts/utils/auth/stable_scope_id.mjs +1 -1
- package/scripts/utils/cli/cli_registry.mjs +18 -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/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
package/scripts/mobile.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { expoExec } from './utils/expo/command.mjs';
|
|
|
14
14
|
import { ensureDevExpoServer } from './utils/dev/expo_dev.mjs';
|
|
15
15
|
import { resolveMobileReachableServerUrl } from './utils/server/mobile_api_url.mjs';
|
|
16
16
|
import { patchIosXcodeProjectsForSigningAndIdentity, resolveIosAppXcodeProjects } from './utils/mobile/ios_xcodeproj_patch.mjs';
|
|
17
|
+
import { pickMetroPort, resolveStablePortStart } from './utils/expo/metro_ports.mjs';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Mobile dev helper for the Happier UI Expo app (typically `apps/ui`).
|
|
@@ -75,6 +76,7 @@ async function main() {
|
|
|
75
76
|
const uiRepoDir = getComponentDir(rootDir, 'happier-ui');
|
|
76
77
|
await requireDir('happier-ui', uiRepoDir);
|
|
77
78
|
await ensureDepsInstalled(uiRepoDir, 'happier-ui');
|
|
79
|
+
const happyDir = uiRepoDir;
|
|
78
80
|
|
|
79
81
|
// Happy monorepo layouts (historical):
|
|
80
82
|
// - legacy: <happyDir>/expo-app (split-repo era)
|
|
@@ -104,7 +106,9 @@ async function main() {
|
|
|
104
106
|
// (Info.plist includes `dev.happier.app.dev`), so iOS will open the dev build instead of the App Store app.
|
|
105
107
|
const appEnv = process.env.APP_ENV ?? kv.get('--app-env') ?? 'development';
|
|
106
108
|
const host = kv.get('--host') ?? process.env.HAPPIER_STACK_MOBILE_HOST ?? 'lan';
|
|
107
|
-
const
|
|
109
|
+
const portFromArg = kv.get('--port') ?? '';
|
|
110
|
+
const portFromEnv = process.env.HAPPIER_STACK_MOBILE_PORT ?? '';
|
|
111
|
+
const portRaw = portFromArg || portFromEnv || '8081';
|
|
108
112
|
// Default behavior:
|
|
109
113
|
// - `hstack mobile` starts Metro and keeps running.
|
|
110
114
|
// - `hstack mobile --run-ios` / `hstack mobile:ios` just builds/installs and exits (unless --metro is provided).
|
|
@@ -126,6 +130,29 @@ async function main() {
|
|
|
126
130
|
const stackCtx = resolveStackContext({ env, autostart });
|
|
127
131
|
const { stackMode, runtimeStatePath, stackName, envPath } = stackCtx;
|
|
128
132
|
|
|
133
|
+
// Expo/React Native native build steps can probe the Metro port even when we pass `--no-bundler`.
|
|
134
|
+
// Defaulting to 8081 makes builds much more likely to fail late if another Metro/Expo starts on the same port.
|
|
135
|
+
//
|
|
136
|
+
// Strategy:
|
|
137
|
+
// - If the user explicitly sets --port or HAPPIER_STACK_MOBILE_PORT, honor it.
|
|
138
|
+
// - Otherwise pick a stable, collision-resistant port in a higher range for build-only steps.
|
|
139
|
+
const needsNativeBuildPort = flags.has('--prebuild') || flags.has('--run-ios');
|
|
140
|
+
if (needsNativeBuildPort) {
|
|
141
|
+
const forcedPort = (portFromArg || portFromEnv).toString().trim();
|
|
142
|
+
const stableKey = (stackMode && stackName ? stackName : '') || iosBundleId || scheme || 'happier';
|
|
143
|
+
const startPort = resolveStablePortStart({
|
|
144
|
+
env,
|
|
145
|
+
stackName: stableKey,
|
|
146
|
+
baseKey: 'HAPPIER_STACK_MOBILE_BUILD_PORT_BASE',
|
|
147
|
+
rangeKey: 'HAPPIER_STACK_MOBILE_BUILD_PORT_RANGE',
|
|
148
|
+
defaultBase: 19000,
|
|
149
|
+
defaultRange: 10000,
|
|
150
|
+
});
|
|
151
|
+
const metroPort = await pickMetroPort({ startPort, forcedPort, host: '127.0.0.1' });
|
|
152
|
+
env.RCT_METRO_PORT = String(metroPort);
|
|
153
|
+
env.EXPO_PACKAGER_PORT = String(metroPort);
|
|
154
|
+
}
|
|
155
|
+
|
|
129
156
|
// Ensure the built iOS app registers the same scheme we use for dev-client QR links.
|
|
130
157
|
// (Happy app reads EXPO_APP_SCHEME in app.config.js; default remains unchanged when unset.)
|
|
131
158
|
env.EXPO_APP_SCHEME = scheme;
|
|
@@ -306,7 +333,11 @@ async function main() {
|
|
|
306
333
|
}
|
|
307
334
|
|
|
308
335
|
const configuration = kv.get('--configuration') ?? 'Debug';
|
|
336
|
+
const buildMetroPort = (env.RCT_METRO_PORT ?? env.EXPO_PACKAGER_PORT ?? '').toString().trim();
|
|
309
337
|
const args = ['run:ios', '--no-bundler', '--no-build-cache', '--configuration', configuration];
|
|
338
|
+
if (buildMetroPort) {
|
|
339
|
+
args.push('-p', buildMetroPort);
|
|
340
|
+
}
|
|
310
341
|
if (device) {
|
|
311
342
|
args.push('-d', device);
|
|
312
343
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { getStackRootFromMeta, runNodeCapture } from './testkit/auth_testkit.mjs';
|
|
8
|
+
|
|
9
|
+
test('hstack mobile --prebuild does not crash with undefined happyDir', async () => {
|
|
10
|
+
const rootDir = getStackRootFromMeta(import.meta.url);
|
|
11
|
+
const mobileScript = join(rootDir, 'scripts', 'mobile.mjs');
|
|
12
|
+
|
|
13
|
+
const tmp = await mkdtemp(join(tmpdir(), 'hstack-mobile-'));
|
|
14
|
+
const repoDir = join(tmp, 'repo');
|
|
15
|
+
const storageDir = join(tmp, 'storage');
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Minimal fixture: the script expects a "happier-ui" dir to exist at <repo>/apps/ui.
|
|
19
|
+
// Avoid creating package.json to keep the run hermetic (ensureDepsInstalled will no-op).
|
|
20
|
+
await mkdir(join(repoDir, 'apps', 'ui'), { recursive: true });
|
|
21
|
+
|
|
22
|
+
// Ensure stack storage stays within this tmp dir (avoid touching real ~/.happier paths).
|
|
23
|
+
await mkdir(join(storageDir, 'main'), { recursive: true });
|
|
24
|
+
|
|
25
|
+
const env = {
|
|
26
|
+
...process.env,
|
|
27
|
+
HAPPIER_STACK_REPO_DIR: repoDir,
|
|
28
|
+
HAPPIER_STACK_HOME_DIR: join(tmp, 'home'),
|
|
29
|
+
HAPPIER_STACK_STORAGE_DIR: storageDir,
|
|
30
|
+
HAPPIER_STACK_STACK: 'main',
|
|
31
|
+
// Keep resolveServerUrls from probing tailscale in tests.
|
|
32
|
+
HAPPIER_STACK_TAILSCALE_PREFER_PUBLIC_URL: '0',
|
|
33
|
+
HAPPIER_STACK_TAILSCALE_SERVE: '0',
|
|
34
|
+
// Prevent env auto-loading from a real machine stack env file.
|
|
35
|
+
HAPPIER_STACK_ENV_FILE: join(tmp, 'nonexistent-env'),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const res = await runNodeCapture([mobileScript, '--prebuild', '--no-metro'], { cwd: rootDir, env });
|
|
39
|
+
assert.doesNotMatch(
|
|
40
|
+
res.stderr,
|
|
41
|
+
/\bhappyDir is not defined\b/,
|
|
42
|
+
`expected prebuild to fail for a real reason (e.g. missing expo), not a ReferenceError\nstderr:\n${res.stderr}\nstdout:\n${res.stdout}`
|
|
43
|
+
);
|
|
44
|
+
} finally {
|
|
45
|
+
await rm(tmp, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { chmod, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { getStackRootFromMeta, runNodeCapture } from './testkit/auth_testkit.mjs';
|
|
8
|
+
|
|
9
|
+
function parseKeyValueLines(text) {
|
|
10
|
+
const out = {};
|
|
11
|
+
for (const line of String(text ?? '').split(/\r?\n/)) {
|
|
12
|
+
const idx = line.indexOf('=');
|
|
13
|
+
if (idx === -1) continue;
|
|
14
|
+
const key = line.slice(0, idx).trim();
|
|
15
|
+
const value = line.slice(idx + 1).trim();
|
|
16
|
+
if (key) out[key] = value;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('hstack mobile --prebuild sets RCT_METRO_PORT for native build steps', async () => {
|
|
22
|
+
const rootDir = getStackRootFromMeta(import.meta.url);
|
|
23
|
+
const mobileScript = join(rootDir, 'scripts', 'mobile.mjs');
|
|
24
|
+
|
|
25
|
+
const tmp = await mkdtemp(join(tmpdir(), 'hstack-mobile-port-'));
|
|
26
|
+
const repoDir = join(tmp, 'repo');
|
|
27
|
+
const storageDir = join(tmp, 'storage');
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const uiDir = join(repoDir, 'apps', 'ui');
|
|
31
|
+
const expoBin = join(uiDir, 'node_modules', '.bin', 'expo');
|
|
32
|
+
await mkdir(join(uiDir, 'node_modules', '.bin'), { recursive: true });
|
|
33
|
+
|
|
34
|
+
// Stub Expo CLI so the test doesn't require installing or running the real toolchain.
|
|
35
|
+
await writeFile(
|
|
36
|
+
expoBin,
|
|
37
|
+
`#!/usr/bin/env node
|
|
38
|
+
console.log('RCT_METRO_PORT=' + (process.env.RCT_METRO_PORT ?? ''));
|
|
39
|
+
console.log('EXPO_PACKAGER_PORT=' + (process.env.EXPO_PACKAGER_PORT ?? ''));
|
|
40
|
+
process.exit(0);
|
|
41
|
+
`,
|
|
42
|
+
'utf-8'
|
|
43
|
+
);
|
|
44
|
+
if (process.platform !== 'win32') {
|
|
45
|
+
await chmod(expoBin, 0o755);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Ensure stack storage stays within this tmp dir (avoid touching real ~/.happier paths).
|
|
49
|
+
await mkdir(join(storageDir, 'main'), { recursive: true });
|
|
50
|
+
|
|
51
|
+
const env = {
|
|
52
|
+
...process.env,
|
|
53
|
+
HAPPIER_STACK_REPO_DIR: repoDir,
|
|
54
|
+
HAPPIER_STACK_HOME_DIR: join(tmp, 'home'),
|
|
55
|
+
HAPPIER_STACK_STORAGE_DIR: storageDir,
|
|
56
|
+
HAPPIER_STACK_STACK: 'main',
|
|
57
|
+
// Keep resolveServerUrls from probing tailscale in tests.
|
|
58
|
+
HAPPIER_STACK_TAILSCALE_PREFER_PUBLIC_URL: '0',
|
|
59
|
+
HAPPIER_STACK_TAILSCALE_SERVE: '0',
|
|
60
|
+
// Prevent env auto-loading from a real machine stack env file.
|
|
61
|
+
HAPPIER_STACK_ENV_FILE: join(tmp, 'nonexistent-env'),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const res = await runNodeCapture([mobileScript, '--prebuild', '--platform=android', '--no-metro'], { cwd: rootDir, env });
|
|
65
|
+
const kv = parseKeyValueLines(res.stdout);
|
|
66
|
+
assert.ok(kv.RCT_METRO_PORT, `expected stub expo to print RCT_METRO_PORT\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
67
|
+
assert.equal(
|
|
68
|
+
kv.EXPO_PACKAGER_PORT,
|
|
69
|
+
kv.RCT_METRO_PORT,
|
|
70
|
+
`expected EXPO_PACKAGER_PORT to match RCT_METRO_PORT\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`
|
|
71
|
+
);
|
|
72
|
+
assert.notEqual(
|
|
73
|
+
kv.RCT_METRO_PORT,
|
|
74
|
+
'8081',
|
|
75
|
+
`expected native build steps to avoid default Metro port 8081 (more collision-prone)\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`
|
|
76
|
+
);
|
|
77
|
+
} finally {
|
|
78
|
+
await rm(tmp, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { chmod, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { getStackRootFromMeta, runNodeCapture } from './testkit/auth_testkit.mjs';
|
|
8
|
+
|
|
9
|
+
function parseKeyValueLines(text) {
|
|
10
|
+
const out = {};
|
|
11
|
+
for (const line of String(text ?? '').split(/\r?\n/)) {
|
|
12
|
+
const idx = line.indexOf('=');
|
|
13
|
+
if (idx === -1) continue;
|
|
14
|
+
const key = line.slice(0, idx).trim();
|
|
15
|
+
const value = line.slice(idx + 1).trim();
|
|
16
|
+
if (key) out[key] = value;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('hstack mobile --run-ios passes -p/--port to Expo (avoids default 8081)', async () => {
|
|
22
|
+
const rootDir = getStackRootFromMeta(import.meta.url);
|
|
23
|
+
const mobileScript = join(rootDir, 'scripts', 'mobile.mjs');
|
|
24
|
+
|
|
25
|
+
const tmp = await mkdtemp(join(tmpdir(), 'hstack-mobile-runios-port-'));
|
|
26
|
+
const repoDir = join(tmp, 'repo');
|
|
27
|
+
const storageDir = join(tmp, 'storage');
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const binDir = join(tmp, 'bin');
|
|
31
|
+
await mkdir(binDir, { recursive: true });
|
|
32
|
+
const xcrunStub = join(binDir, 'xcrun');
|
|
33
|
+
await writeFile(
|
|
34
|
+
xcrunStub,
|
|
35
|
+
`#!/bin/bash
|
|
36
|
+
set -euo pipefail
|
|
37
|
+
if [[ "\${1:-}" == "xcdevice" && "\${2:-}" == "list" ]]; then
|
|
38
|
+
echo "[]"
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
echo "xcrun stub: unsupported args: $*" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
`,
|
|
44
|
+
'utf-8'
|
|
45
|
+
);
|
|
46
|
+
if (process.platform !== 'win32') {
|
|
47
|
+
await chmod(xcrunStub, 0o755);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const uiDir = join(repoDir, 'apps', 'ui');
|
|
51
|
+
const expoBin = join(uiDir, 'node_modules', '.bin', 'expo');
|
|
52
|
+
await mkdir(join(uiDir, 'node_modules', '.bin'), { recursive: true });
|
|
53
|
+
|
|
54
|
+
// Stub Expo CLI: print argv + env-derived ports and exit successfully.
|
|
55
|
+
// Use an absolute node shebang so the test can safely clear PATH.
|
|
56
|
+
await writeFile(
|
|
57
|
+
expoBin,
|
|
58
|
+
`#!${process.execPath}
|
|
59
|
+
console.log('ARGS=' + JSON.stringify(process.argv.slice(2)));
|
|
60
|
+
console.log('RCT_METRO_PORT=' + (process.env.RCT_METRO_PORT ?? ''));
|
|
61
|
+
console.log('EXPO_PACKAGER_PORT=' + (process.env.EXPO_PACKAGER_PORT ?? ''));
|
|
62
|
+
process.exit(0);
|
|
63
|
+
`,
|
|
64
|
+
'utf-8'
|
|
65
|
+
);
|
|
66
|
+
if (process.platform !== 'win32') {
|
|
67
|
+
await chmod(expoBin, 0o755);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await mkdir(join(storageDir, 'main'), { recursive: true });
|
|
71
|
+
|
|
72
|
+
const env = {
|
|
73
|
+
...process.env,
|
|
74
|
+
// Ensure xcrun runs fast/deterministically in tests.
|
|
75
|
+
PATH: binDir,
|
|
76
|
+
HAPPIER_STACK_REPO_DIR: repoDir,
|
|
77
|
+
HAPPIER_STACK_HOME_DIR: join(tmp, 'home'),
|
|
78
|
+
HAPPIER_STACK_STORAGE_DIR: storageDir,
|
|
79
|
+
HAPPIER_STACK_STACK: 'main',
|
|
80
|
+
HAPPIER_STACK_TAILSCALE_PREFER_PUBLIC_URL: '0',
|
|
81
|
+
HAPPIER_STACK_TAILSCALE_SERVE: '0',
|
|
82
|
+
HAPPIER_STACK_ENV_FILE: join(tmp, 'nonexistent-env'),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const res = await runNodeCapture([mobileScript, '--run-ios', '--no-metro'], { cwd: rootDir, env });
|
|
86
|
+
const kv = parseKeyValueLines(res.stdout);
|
|
87
|
+
|
|
88
|
+
const args = JSON.parse(kv.ARGS ?? '[]');
|
|
89
|
+
const port = kv.RCT_METRO_PORT ?? '';
|
|
90
|
+
|
|
91
|
+
assert.ok(port, `expected RCT_METRO_PORT to be set\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
92
|
+
assert.equal(kv.EXPO_PACKAGER_PORT, port, `expected EXPO_PACKAGER_PORT to match\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
93
|
+
assert.ok(args.includes('-p') || args.includes('--port'), `expected expo args to include -p/--port\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
94
|
+
const pIdx = args.indexOf('-p') !== -1 ? args.indexOf('-p') : args.indexOf('--port');
|
|
95
|
+
assert.equal(args[pIdx + 1], port, `expected expo -p to match RCT_METRO_PORT\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
96
|
+
assert.notEqual(port, '8081', `expected non-default port to reduce collisions\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
97
|
+
} finally {
|
|
98
|
+
await rm(tmp, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
@@ -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,6 +27,25 @@ 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(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,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
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
|
+
}
|
|
48
|
+
|
|
30
49
|
function currentTarget() {
|
|
31
50
|
const os = process.platform === 'linux' ? 'linux' : process.platform === 'darwin' ? 'darwin' : '';
|
|
32
51
|
const arch = process.arch === 'x64' ? 'x64' : process.arch === 'arm64' ? 'arm64' : '';
|
|
@@ -65,7 +84,7 @@ test('compiled happier and server binaries execute from isolated cwd', async (t)
|
|
|
65
84
|
const repoRoot = resolve(fileURLToPath(new URL('../../..', import.meta.url)));
|
|
66
85
|
const version = `0.0.0-smoke.${Date.now()}`;
|
|
67
86
|
|
|
68
|
-
const buildCli =
|
|
87
|
+
const buildCli = runWithHardTimeout(
|
|
69
88
|
process.execPath,
|
|
70
89
|
[
|
|
71
90
|
'scripts/release/build-cli-binaries.mjs',
|
|
@@ -91,13 +110,13 @@ test('compiled happier and server binaries execute from isolated cwd', async (t)
|
|
|
91
110
|
t.after(() => {
|
|
92
111
|
spawnSync('bash', ['-lc', `rm -rf "${cliExtract.extractDir.replaceAll('"', '\\"')}"`], { stdio: 'ignore' });
|
|
93
112
|
});
|
|
94
|
-
const cliVersion =
|
|
113
|
+
const cliVersion = runWithHardTimeout(cliExtract.binaryPath, ['--version'], {
|
|
95
114
|
cwd: '/tmp',
|
|
96
115
|
encoding: 'utf-8',
|
|
97
116
|
env: { ...process.env, HAPPIER_NONINTERACTIVE: '1' },
|
|
98
117
|
timeout: 7000,
|
|
99
118
|
});
|
|
100
|
-
const cliTimedOut = cliVersion
|
|
119
|
+
const cliTimedOut = didCommandTimeout(cliVersion);
|
|
101
120
|
const cliExited = (cliVersion.status ?? 1) === 0;
|
|
102
121
|
assert.ok(cliTimedOut || cliExited, cliVersion.stderr || cliVersion.stdout);
|
|
103
122
|
const versionText = `${cliVersion.stdout || ''}${cliVersion.stderr || ''}`.trim();
|
|
@@ -107,7 +126,7 @@ test('compiled happier and server binaries execute from isolated cwd', async (t)
|
|
|
107
126
|
);
|
|
108
127
|
|
|
109
128
|
if (isLinuxTarget(target)) {
|
|
110
|
-
const buildServer =
|
|
129
|
+
const buildServer = runWithHardTimeout(
|
|
111
130
|
process.execPath,
|
|
112
131
|
[
|
|
113
132
|
'scripts/release/build-server-binaries.mjs',
|
|
@@ -135,7 +154,7 @@ test('compiled happier and server binaries execute from isolated cwd', async (t)
|
|
|
135
154
|
t.after(() => {
|
|
136
155
|
spawnSync('bash', ['-lc', `rm -rf "${serverDataDir.replaceAll('"', '\\"')}"`], { stdio: 'ignore' });
|
|
137
156
|
});
|
|
138
|
-
const serverBoot =
|
|
157
|
+
const serverBoot = runWithHardTimeout(serverExtract.binaryPath, [], {
|
|
139
158
|
cwd: '/tmp',
|
|
140
159
|
encoding: 'utf-8',
|
|
141
160
|
env: {
|
|
@@ -150,7 +169,7 @@ test('compiled happier and server binaries execute from isolated cwd', async (t)
|
|
|
150
169
|
},
|
|
151
170
|
timeout: 7000,
|
|
152
171
|
});
|
|
153
|
-
const timedOut = serverBoot
|
|
172
|
+
const timedOut = didCommandTimeout(serverBoot);
|
|
154
173
|
const cleanExit = (serverBoot.status ?? 1) === 0;
|
|
155
174
|
const serverOutput = `${serverBoot.stderr || ''}\n${serverBoot.stdout || ''}`;
|
|
156
175
|
assert.ok(timedOut || cleanExit, serverOutput);
|