@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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdtemp, readdir } from 'node:fs/promises';
|
|
4
|
+
import { createServer } from 'node:net';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const SELF_HOST_PORT_MIN = 20_000;
|
|
9
|
+
const SELF_HOST_PORT_MAX = 29_999;
|
|
10
|
+
const SELF_HOST_PORT_ATTEMPTS = 40;
|
|
11
|
+
|
|
12
|
+
export function commandExists(cmd) {
|
|
13
|
+
const name = String(cmd ?? '').trim();
|
|
14
|
+
if (!name) return false;
|
|
15
|
+
if (process.platform === 'win32') {
|
|
16
|
+
return spawnSync('where', [name], { stdio: 'ignore' }).status === 0;
|
|
17
|
+
}
|
|
18
|
+
return spawnSync('sh', ['-lc', `command -v ${name} >/dev/null 2>&1`], { stdio: 'ignore' }).status === 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function run(cmd, args, { label, cwd, env, timeoutMs = 0, allowFail = false, stdio = 'pipe' } = {}) {
|
|
22
|
+
const prefix = label ? `[${label}] ` : '';
|
|
23
|
+
const result = spawnSync(cmd, args, {
|
|
24
|
+
cwd,
|
|
25
|
+
env: env ?? process.env,
|
|
26
|
+
encoding: 'utf-8',
|
|
27
|
+
stdio,
|
|
28
|
+
timeout: timeoutMs || undefined,
|
|
29
|
+
});
|
|
30
|
+
const timedOut = result.error && result.error.code === 'ETIMEDOUT';
|
|
31
|
+
if (!allowFail && (timedOut || (result.status ?? 1) !== 0)) {
|
|
32
|
+
const stderr = String(result.stderr ?? '').trim();
|
|
33
|
+
const stdout = String(result.stdout ?? '').trim();
|
|
34
|
+
const reason = timedOut ? 'timed out' : `exited with status ${result.status}`;
|
|
35
|
+
throw new Error(`${prefix}${cmd} ${args.join(' ')} ${reason}\nstdout:\n${stdout}\nstderr:\n${stderr}`);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function extractBinaryFromArtifact({ artifactPath, binaryName, label } = {}) {
|
|
41
|
+
const path = String(artifactPath ?? '').trim();
|
|
42
|
+
const name = String(binaryName ?? '').trim();
|
|
43
|
+
assert.ok(path, 'artifactPath is required');
|
|
44
|
+
assert.ok(name, 'binaryName is required');
|
|
45
|
+
const extractDir = await mkdtemp(join(tmpdir(), 'happier-self-host-artifact-'));
|
|
46
|
+
run('tar', ['-xzf', path, '-C', extractDir], { label, timeoutMs: 60_000 });
|
|
47
|
+
const roots = await readdir(extractDir);
|
|
48
|
+
assert.ok(roots.length > 0, `expected extracted root directory for ${path}`);
|
|
49
|
+
return {
|
|
50
|
+
extractDir,
|
|
51
|
+
binaryPath: join(extractDir, roots[0], name),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function waitForHealth(url, timeoutMs = 60_000) {
|
|
56
|
+
const started = Date.now();
|
|
57
|
+
while (Date.now() - started < timeoutMs) {
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(String(url), {
|
|
60
|
+
headers: { accept: 'application/json' },
|
|
61
|
+
});
|
|
62
|
+
if (response.ok) {
|
|
63
|
+
const payload = await response.json().catch(() => ({}));
|
|
64
|
+
if (payload?.ok === true) return true;
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// keep polling until timeout
|
|
68
|
+
}
|
|
69
|
+
await new Promise((resolveSleep) => setTimeout(resolveSleep, 1500));
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function canBindLocalhostPort(port) {
|
|
75
|
+
return await new Promise((resolvePort) => {
|
|
76
|
+
const server = createServer();
|
|
77
|
+
server.unref();
|
|
78
|
+
server.once('error', () => resolvePort(false));
|
|
79
|
+
server.listen(port, '127.0.0.1', () => {
|
|
80
|
+
server.close((error) => resolvePort(!error));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function reserveLocalhostPort() {
|
|
86
|
+
for (let attempt = 0; attempt < SELF_HOST_PORT_ATTEMPTS; attempt += 1) {
|
|
87
|
+
const candidate =
|
|
88
|
+
SELF_HOST_PORT_MIN + Math.floor(Math.random() * (SELF_HOST_PORT_MAX - SELF_HOST_PORT_MIN + 1));
|
|
89
|
+
if (await canBindLocalhostPort(candidate)) return candidate;
|
|
90
|
+
}
|
|
91
|
+
throw new Error('failed to reserve localhost port from non-ephemeral range');
|
|
92
|
+
}
|
|
93
|
+
|
|
@@ -1,38 +1,13 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import {
|
|
3
|
-
import { chmod, cp, mkdir, mkdtemp, readdir, rm } from 'node:fs/promises';
|
|
4
|
-
import { createServer } from 'node:net';
|
|
2
|
+
import { chmod, cp, mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
5
3
|
import { tmpdir } from 'node:os';
|
|
6
4
|
import { join, resolve } from 'node:path';
|
|
7
5
|
import test from 'node:test';
|
|
8
6
|
import { fileURLToPath } from 'node:url';
|
|
9
7
|
|
|
10
|
-
const SELF_HOST_PORT_MIN = 20_000;
|
|
11
|
-
const SELF_HOST_PORT_MAX = 29_999;
|
|
12
|
-
const SELF_HOST_PORT_ATTEMPTS = 40;
|
|
13
8
|
const SELF_HOST_INSTALL_TIMEOUT_MS = 420_000;
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
return spawnSync('bash', ['-lc', `command -v ${cmd} >/dev/null 2>&1`], { stdio: 'ignore' }).status === 0;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function run(cmd, args, { cwd, env, timeoutMs = 0, allowFail = false, stdio = 'pipe' } = {}) {
|
|
20
|
-
const result = spawnSync(cmd, args, {
|
|
21
|
-
cwd,
|
|
22
|
-
env: env ?? process.env,
|
|
23
|
-
encoding: 'utf-8',
|
|
24
|
-
stdio,
|
|
25
|
-
timeout: timeoutMs || undefined,
|
|
26
|
-
});
|
|
27
|
-
const timedOut = result.error && result.error.code === 'ETIMEDOUT';
|
|
28
|
-
if (!allowFail && (timedOut || (result.status ?? 1) !== 0)) {
|
|
29
|
-
const stderr = String(result.stderr ?? '').trim();
|
|
30
|
-
const stdout = String(result.stdout ?? '').trim();
|
|
31
|
-
const reason = timedOut ? 'timed out' : `exited with status ${result.status}`;
|
|
32
|
-
throw new Error(`[self-host-systemd] ${cmd} ${args.join(' ')} ${reason}\nstdout:\n${stdout}\nstderr:\n${stderr}`);
|
|
33
|
-
}
|
|
34
|
-
return result;
|
|
35
|
-
}
|
|
10
|
+
import { commandExists, extractBinaryFromArtifact, reserveLocalhostPort, run, waitForHealth } from './self_host_service_e2e_harness.mjs';
|
|
36
11
|
|
|
37
12
|
function runAsRoot(cmd, args, { cwd, env, timeoutMs = 0, allowFail = false, stdio = 'pipe' } = {}) {
|
|
38
13
|
const mergedEnv = {
|
|
@@ -40,13 +15,14 @@ function runAsRoot(cmd, args, { cwd, env, timeoutMs = 0, allowFail = false, stdi
|
|
|
40
15
|
...(env ?? {}),
|
|
41
16
|
};
|
|
42
17
|
if (typeof process.getuid === 'function' && process.getuid() === 0) {
|
|
43
|
-
return run(cmd, args, { cwd, env: mergedEnv, timeoutMs, allowFail, stdio });
|
|
18
|
+
return run(cmd, args, { label: 'self-host-systemd', cwd, env: mergedEnv, timeoutMs, allowFail, stdio });
|
|
44
19
|
}
|
|
45
20
|
if (!commandExists('sudo')) {
|
|
46
21
|
throw new Error('[self-host-systemd] sudo is required for systemd self-host test');
|
|
47
22
|
}
|
|
48
23
|
const envArgs = Object.entries(mergedEnv).map(([key, value]) => `${key}=${String(value ?? '')}`);
|
|
49
24
|
return run('sudo', ['-E', 'env', ...envArgs, cmd, ...args], {
|
|
25
|
+
label: 'self-host-systemd',
|
|
50
26
|
cwd,
|
|
51
27
|
env: process.env,
|
|
52
28
|
timeoutMs,
|
|
@@ -55,62 +31,6 @@ function runAsRoot(cmd, args, { cwd, env, timeoutMs = 0, allowFail = false, stdi
|
|
|
55
31
|
});
|
|
56
32
|
}
|
|
57
33
|
|
|
58
|
-
async function extractBinaryFromArtifact({ artifactPath, binaryName }) {
|
|
59
|
-
const extractDir = await mkdtemp(join(tmpdir(), 'happier-self-host-systemd-artifact-'));
|
|
60
|
-
run('tar', ['-xzf', artifactPath, '-C', extractDir], { timeoutMs: 30_000 });
|
|
61
|
-
const roots = await readdir(extractDir);
|
|
62
|
-
assert.ok(roots.length > 0, `expected extracted root directory for ${artifactPath}`);
|
|
63
|
-
return {
|
|
64
|
-
extractDir,
|
|
65
|
-
binaryPath: join(extractDir, roots[0], binaryName),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function waitForHealth(url, timeoutMs = 60_000) {
|
|
70
|
-
const started = Date.now();
|
|
71
|
-
while (Date.now() - started < timeoutMs) {
|
|
72
|
-
try {
|
|
73
|
-
const response = await fetch(url, {
|
|
74
|
-
headers: { accept: 'application/json' },
|
|
75
|
-
});
|
|
76
|
-
if (response.ok) {
|
|
77
|
-
const payload = await response.json().catch(() => ({}));
|
|
78
|
-
if (payload?.ok === true) {
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} catch {
|
|
83
|
-
// keep polling until timeout
|
|
84
|
-
}
|
|
85
|
-
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
86
|
-
}
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function canBindLocalhostPort(port) {
|
|
91
|
-
return await new Promise((resolvePort) => {
|
|
92
|
-
const server = createServer();
|
|
93
|
-
server.unref();
|
|
94
|
-
server.once('error', () => resolvePort(false));
|
|
95
|
-
server.listen(port, '127.0.0.1', () => {
|
|
96
|
-
server.close((error) => {
|
|
97
|
-
resolvePort(!error);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function reserveLocalhostPort() {
|
|
104
|
-
for (let attempt = 0; attempt < SELF_HOST_PORT_ATTEMPTS; attempt += 1) {
|
|
105
|
-
const candidate =
|
|
106
|
-
SELF_HOST_PORT_MIN + Math.floor(Math.random() * (SELF_HOST_PORT_MAX - SELF_HOST_PORT_MIN + 1));
|
|
107
|
-
if (await canBindLocalhostPort(candidate)) {
|
|
108
|
-
return candidate;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
throw new Error('failed to reserve localhost port from non-ephemeral range');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
34
|
test(
|
|
115
35
|
'compiled hstack self-host install/uninstall works on systemd host without repo checkout',
|
|
116
36
|
{ timeout: 15 * 60_000 },
|
|
@@ -148,6 +68,7 @@ test(
|
|
|
148
68
|
'--targets=linux-x64',
|
|
149
69
|
],
|
|
150
70
|
{
|
|
71
|
+
label: 'self-host-systemd',
|
|
151
72
|
cwd: repoRoot,
|
|
152
73
|
env: { ...process.env },
|
|
153
74
|
timeoutMs: 8 * 60_000,
|
|
@@ -162,6 +83,7 @@ test(
|
|
|
162
83
|
'--targets=linux-x64',
|
|
163
84
|
],
|
|
164
85
|
{
|
|
86
|
+
label: 'self-host-systemd',
|
|
165
87
|
cwd: repoRoot,
|
|
166
88
|
env: { ...process.env },
|
|
167
89
|
timeoutMs: 8 * 60_000,
|
|
@@ -171,8 +93,8 @@ test(
|
|
|
171
93
|
const hstackArtifact = join(repoRoot, 'dist', 'release-assets', 'stack', `hstack-v${version}-linux-x64.tar.gz`);
|
|
172
94
|
const serverArtifact = join(repoRoot, 'dist', 'release-assets', 'server', `happier-server-v${version}-linux-x64.tar.gz`);
|
|
173
95
|
|
|
174
|
-
const extractedHstack = await extractBinaryFromArtifact({ artifactPath: hstackArtifact, binaryName: 'hstack' });
|
|
175
|
-
const extractedServer = await extractBinaryFromArtifact({ artifactPath: serverArtifact, binaryName: 'happier-server' });
|
|
96
|
+
const extractedHstack = await extractBinaryFromArtifact({ label: 'self-host-systemd', artifactPath: hstackArtifact, binaryName: 'hstack' });
|
|
97
|
+
const extractedServer = await extractBinaryFromArtifact({ label: 'self-host-systemd', artifactPath: serverArtifact, binaryName: 'happier-server' });
|
|
176
98
|
|
|
177
99
|
t.after(async () => {
|
|
178
100
|
await rm(extractedHstack.extractDir, { recursive: true, force: true });
|
package/scripts/service.mjs
CHANGED
|
@@ -3,9 +3,11 @@ import { run, runCapture } from './utils/proc/proc.mjs';
|
|
|
3
3
|
import { getComponentDir, getDefaultAutostartPaths, getRootDir, getSystemdUnitInfo, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
4
4
|
import { getInternalServerUrl, getPublicServerUrlEnvOverride } from './utils/server/urls.mjs';
|
|
5
5
|
import { ensureMacAutostartDisabled, ensureMacAutostartEnabled } from './utils/service/autostart_darwin.mjs';
|
|
6
|
+
import { installService as installManagedService, uninstallService as uninstallManagedService } from './utils/service/service_manager.mjs';
|
|
6
7
|
import { getCanonicalHomeDir } from './utils/env/config.mjs';
|
|
7
8
|
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
8
9
|
import { expandHome } from './utils/paths/canonical_home.mjs';
|
|
10
|
+
import { resolveInstalledCliRoot, resolveInstalledPath } from './utils/paths/runtime.mjs';
|
|
9
11
|
import { spawn } from 'node:child_process';
|
|
10
12
|
import { homedir } from 'node:os';
|
|
11
13
|
import { existsSync } from 'node:fs';
|
|
@@ -109,6 +111,33 @@ function ensureLinuxSystemModeSupported({ mode }) {
|
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
async function resolveStackAutostartProgramArgs({ rootDir, mode, systemUser }) {
|
|
115
|
+
const base = getCanonicalHomeDir();
|
|
116
|
+
const candidates =
|
|
117
|
+
process.platform === 'win32'
|
|
118
|
+
? [join(base, 'bin', 'hstack.exe'), join(base, 'bin', 'hstack.cmd'), join(base, 'bin', 'hstack')]
|
|
119
|
+
: [join(base, 'bin', 'hstack')];
|
|
120
|
+
|
|
121
|
+
let shimPath = candidates.find((p) => existsSync(p)) ?? '';
|
|
122
|
+
if (mode === 'system' && systemUser) {
|
|
123
|
+
const home = await resolveHomeDirForUser(systemUser);
|
|
124
|
+
if (home) {
|
|
125
|
+
const systemCandidates =
|
|
126
|
+
process.platform === 'win32'
|
|
127
|
+
? [
|
|
128
|
+
join(home, '.happier-stack', 'bin', 'hstack.exe'),
|
|
129
|
+
join(home, '.happier-stack', 'bin', 'hstack.cmd'),
|
|
130
|
+
join(home, '.happier-stack', 'bin', 'hstack'),
|
|
131
|
+
]
|
|
132
|
+
: [join(home, '.happier-stack', 'bin', 'hstack')];
|
|
133
|
+
shimPath = systemCandidates.find((p) => existsSync(p)) ?? shimPath;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (shimPath) return [shimPath, 'start'];
|
|
138
|
+
return [process.execPath, resolveInstalledPath(rootDir, 'bin/hstack.mjs'), 'start'];
|
|
139
|
+
}
|
|
140
|
+
|
|
112
141
|
export async function installService({ mode = 'user', systemUser = null } = {}) {
|
|
113
142
|
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
114
143
|
throw new Error(
|
|
@@ -118,11 +147,8 @@ export async function installService({ mode = 'user', systemUser = null } = {})
|
|
|
118
147
|
);
|
|
119
148
|
}
|
|
120
149
|
ensureLinuxSystemModeSupported({ mode });
|
|
121
|
-
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
122
|
-
throw new Error('[local] service install is only supported on macOS (launchd) and Linux (systemd).');
|
|
123
|
-
}
|
|
124
150
|
const rootDir = getRootDir(import.meta.url);
|
|
125
|
-
const { label } = getDefaultAutostartPaths();
|
|
151
|
+
const { label, stdoutPath, stderrPath, baseDir } = getDefaultAutostartPaths();
|
|
126
152
|
const env = getAutostartEnv({ rootDir, mode });
|
|
127
153
|
// Ensure the env file exists so the service never points at a missing path.
|
|
128
154
|
try {
|
|
@@ -137,17 +163,43 @@ export async function installService({ mode = 'user', systemUser = null } = {})
|
|
|
137
163
|
} catch {
|
|
138
164
|
// ignore
|
|
139
165
|
}
|
|
166
|
+
const programArgs = await resolveStackAutostartProgramArgs({ rootDir, mode, systemUser });
|
|
167
|
+
const workingDirectory =
|
|
168
|
+
process.platform === 'linux'
|
|
169
|
+
? '%h'
|
|
170
|
+
: process.platform === 'darwin'
|
|
171
|
+
? resolveInstalledCliRoot(rootDir)
|
|
172
|
+
: baseDir;
|
|
173
|
+
|
|
174
|
+
await installManagedService({
|
|
175
|
+
platform: process.platform,
|
|
176
|
+
mode,
|
|
177
|
+
homeDir: homedir(),
|
|
178
|
+
spec: {
|
|
179
|
+
label,
|
|
180
|
+
description: `Happier Stack (${label})`,
|
|
181
|
+
programArgs,
|
|
182
|
+
workingDirectory,
|
|
183
|
+
env,
|
|
184
|
+
runAsUser: mode === 'system' && systemUser ? systemUser : '',
|
|
185
|
+
stdoutPath,
|
|
186
|
+
stderrPath,
|
|
187
|
+
},
|
|
188
|
+
persistent: true,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (process.platform === 'win32') {
|
|
192
|
+
console.log(`${green('✓')} service installed ${dim('(Windows scheduled task)')}`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
140
195
|
if (process.platform === 'darwin') {
|
|
141
|
-
await ensureMacAutostartEnabled({ rootDir, label, env });
|
|
142
196
|
console.log(`${green('✓')} service installed ${dim('(macOS launchd)')}`);
|
|
143
197
|
return;
|
|
144
198
|
}
|
|
145
199
|
if (mode === 'system') {
|
|
146
|
-
await ensureSystemdSystemServiceEnabled({ rootDir, label, env, systemUser });
|
|
147
200
|
console.log(`${green('✓')} service installed ${dim('(Linux systemd system)')}`);
|
|
148
201
|
return;
|
|
149
202
|
}
|
|
150
|
-
await ensureSystemdUserServiceEnabled({ rootDir, label, env });
|
|
151
203
|
console.log(`${green('✓')} service installed ${dim('(Linux systemd --user)')}`);
|
|
152
204
|
}
|
|
153
205
|
|
|
@@ -157,25 +209,40 @@ export async function uninstallService({ mode = 'user' } = {}) {
|
|
|
157
209
|
return;
|
|
158
210
|
}
|
|
159
211
|
ensureLinuxSystemModeSupported({ mode });
|
|
160
|
-
|
|
212
|
+
const rootDir = getRootDir(import.meta.url);
|
|
213
|
+
const { label, stdoutPath, stderrPath, baseDir } = getDefaultAutostartPaths();
|
|
214
|
+
const env = getAutostartEnv({ rootDir, mode });
|
|
215
|
+
const programArgs = await resolveStackAutostartProgramArgs({ rootDir, mode, systemUser: null });
|
|
216
|
+
const workingDirectory =
|
|
217
|
+
process.platform === 'linux'
|
|
218
|
+
? '%h'
|
|
219
|
+
: process.platform === 'darwin'
|
|
220
|
+
? resolveInstalledCliRoot(rootDir)
|
|
221
|
+
: baseDir;
|
|
222
|
+
|
|
223
|
+
await uninstallManagedService({
|
|
224
|
+
platform: process.platform,
|
|
225
|
+
mode,
|
|
226
|
+
homeDir: homedir(),
|
|
227
|
+
spec: {
|
|
228
|
+
label,
|
|
229
|
+
description: `Happier Stack (${label})`,
|
|
230
|
+
programArgs,
|
|
231
|
+
workingDirectory,
|
|
232
|
+
env,
|
|
233
|
+
stdoutPath,
|
|
234
|
+
stderrPath,
|
|
235
|
+
},
|
|
236
|
+
persistent: true,
|
|
237
|
+
});
|
|
161
238
|
|
|
162
|
-
if (process.platform === '
|
|
163
|
-
|
|
164
|
-
await ensureSystemdSystemServiceDisabled({ remove: true });
|
|
165
|
-
console.log(`${green('✓')} service uninstalled ${dim('(systemd system unit removed)')}`);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
await ensureSystemdUserServiceDisabled({ remove: true });
|
|
169
|
-
console.log(`${green('✓')} service uninstalled ${dim('(systemd user unit removed)')}`);
|
|
239
|
+
if (process.platform === 'win32') {
|
|
240
|
+
console.log(`${green('✓')} service uninstalled ${dim('(Windows task removed)')}`);
|
|
170
241
|
return;
|
|
171
242
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
await rm(plistPath, { force: true });
|
|
177
|
-
} catch {
|
|
178
|
-
// ignore
|
|
243
|
+
if (process.platform === 'linux') {
|
|
244
|
+
console.log(`${green('✓')} service uninstalled ${dim('(systemd unit removed)')}`);
|
|
245
|
+
return;
|
|
179
246
|
}
|
|
180
247
|
console.log(`${green('✓')} service uninstalled ${dim('(plist removed)')}`);
|
|
181
248
|
}
|
|
@@ -633,8 +700,8 @@ async function main() {
|
|
|
633
700
|
const systemUser = resolveSystemUser(helpScopeArgv);
|
|
634
701
|
ensureLinuxSystemModeSupported({ mode });
|
|
635
702
|
|
|
636
|
-
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
637
|
-
throw new Error('[local] service commands are only supported on macOS (launchd)
|
|
703
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux' && process.platform !== 'win32') {
|
|
704
|
+
throw new Error('[local] service commands are only supported on macOS (launchd), Linux (systemd), and Windows (schtasks).');
|
|
638
705
|
}
|
|
639
706
|
const positionals = helpScopeArgv.filter((a) => a && a !== '--' && !a.startsWith('-'));
|
|
640
707
|
const cmd = positionals[0] ?? 'help';
|
|
@@ -711,7 +778,17 @@ async function main() {
|
|
|
711
778
|
health = { ok: false, status: null, body: null };
|
|
712
779
|
}
|
|
713
780
|
|
|
714
|
-
if (process.platform === '
|
|
781
|
+
if (process.platform === 'win32') {
|
|
782
|
+
const { label, stdoutPath, stderrPath } = getDefaultAutostartPaths();
|
|
783
|
+
const taskName = `Happier\\${label}`;
|
|
784
|
+
let schtasksStatus = null;
|
|
785
|
+
try {
|
|
786
|
+
schtasksStatus = await runCapture('schtasks', ['/Query', '/TN', taskName, '/FO', 'LIST', '/V']);
|
|
787
|
+
} catch (e) {
|
|
788
|
+
schtasksStatus = e && typeof e === 'object' && 'out' in e ? e.out : null;
|
|
789
|
+
}
|
|
790
|
+
printResult({ json, data: { label, taskName, stdoutPath, stderrPath, internalUrl, schtasksStatus, health } });
|
|
791
|
+
} else if (process.platform === 'darwin') {
|
|
715
792
|
const { plistPath, stdoutPath, stderrPath, label } = getDefaultAutostartPaths();
|
|
716
793
|
let launchctlLine = null;
|
|
717
794
|
try {
|
|
@@ -736,6 +813,11 @@ async function main() {
|
|
|
736
813
|
printResult({ json, data: { mode, unitName, unitPath, internalUrl, systemctlStatus, health } });
|
|
737
814
|
}
|
|
738
815
|
} else {
|
|
816
|
+
if (process.platform === 'win32') {
|
|
817
|
+
const { label } = getDefaultAutostartPaths();
|
|
818
|
+
await run('schtasks', ['/Query', '/TN', `Happier\\${label}`]);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
739
821
|
if (process.platform === 'darwin') {
|
|
740
822
|
await showStatus();
|
|
741
823
|
} else {
|
|
@@ -749,6 +831,13 @@ async function main() {
|
|
|
749
831
|
}
|
|
750
832
|
return;
|
|
751
833
|
case 'start':
|
|
834
|
+
if (process.platform === 'win32') {
|
|
835
|
+
const { label } = getDefaultAutostartPaths();
|
|
836
|
+
await run('schtasks', ['/Run', '/TN', `Happier\\${label}`]).catch(() => {});
|
|
837
|
+
await postStartDiagnostics();
|
|
838
|
+
if (json) printResult({ json, data: { ok: true, action: 'start' } });
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
752
841
|
if (process.platform === 'darwin') {
|
|
753
842
|
await startLaunchAgent({ persistent: false });
|
|
754
843
|
} else {
|
|
@@ -763,6 +852,12 @@ async function main() {
|
|
|
763
852
|
if (json) printResult({ json, data: { ok: true, action: 'start' } });
|
|
764
853
|
return;
|
|
765
854
|
case 'stop':
|
|
855
|
+
if (process.platform === 'win32') {
|
|
856
|
+
const { label } = getDefaultAutostartPaths();
|
|
857
|
+
await run('schtasks', ['/End', '/TN', `Happier\\${label}`]).catch(() => {});
|
|
858
|
+
if (json) printResult({ json, data: { ok: true, action: 'stop' } });
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
766
861
|
if (process.platform === 'darwin') {
|
|
767
862
|
await stopLaunchAgent({ persistent: false });
|
|
768
863
|
} else {
|
|
@@ -776,6 +871,14 @@ async function main() {
|
|
|
776
871
|
if (json) printResult({ json, data: { ok: true, action: 'stop' } });
|
|
777
872
|
return;
|
|
778
873
|
case 'restart':
|
|
874
|
+
if (process.platform === 'win32') {
|
|
875
|
+
const { label } = getDefaultAutostartPaths();
|
|
876
|
+
await run('schtasks', ['/End', '/TN', `Happier\\${label}`]).catch(() => {});
|
|
877
|
+
await run('schtasks', ['/Run', '/TN', `Happier\\${label}`]).catch(() => {});
|
|
878
|
+
await postStartDiagnostics();
|
|
879
|
+
if (json) printResult({ json, data: { ok: true, action: 'restart' } });
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
779
882
|
if (process.platform === 'darwin') {
|
|
780
883
|
if (!(await restartLaunchAgentBestEffort())) {
|
|
781
884
|
await stopLaunchAgent({ persistent: false });
|
|
@@ -794,6 +897,14 @@ async function main() {
|
|
|
794
897
|
if (json) printResult({ json, data: { ok: true, action: 'restart' } });
|
|
795
898
|
return;
|
|
796
899
|
case 'enable':
|
|
900
|
+
if (process.platform === 'win32') {
|
|
901
|
+
const { label } = getDefaultAutostartPaths();
|
|
902
|
+
await run('schtasks', ['/Change', '/TN', `Happier\\${label}`, '/Enable']).catch(() => {});
|
|
903
|
+
await run('schtasks', ['/Run', '/TN', `Happier\\${label}`]).catch(() => {});
|
|
904
|
+
await postStartDiagnostics();
|
|
905
|
+
if (json) printResult({ json, data: { ok: true, action: 'enable' } });
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
797
908
|
if (process.platform === 'darwin') {
|
|
798
909
|
await startLaunchAgent({ persistent: true });
|
|
799
910
|
} else {
|
|
@@ -808,6 +919,13 @@ async function main() {
|
|
|
808
919
|
if (json) printResult({ json, data: { ok: true, action: 'enable' } });
|
|
809
920
|
return;
|
|
810
921
|
case 'disable':
|
|
922
|
+
if (process.platform === 'win32') {
|
|
923
|
+
const { label } = getDefaultAutostartPaths();
|
|
924
|
+
await run('schtasks', ['/End', '/TN', `Happier\\${label}`]).catch(() => {});
|
|
925
|
+
await run('schtasks', ['/Change', '/TN', `Happier\\${label}`, '/Disable']).catch(() => {});
|
|
926
|
+
if (json) printResult({ json, data: { ok: true, action: 'disable' } });
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
811
929
|
if (process.platform === 'darwin') {
|
|
812
930
|
await stopLaunchAgent({ persistent: true });
|
|
813
931
|
} else {
|
|
@@ -821,6 +939,15 @@ async function main() {
|
|
|
821
939
|
if (json) printResult({ json, data: { ok: true, action: 'disable' } });
|
|
822
940
|
return;
|
|
823
941
|
case 'logs':
|
|
942
|
+
if (process.platform === 'win32') {
|
|
943
|
+
const { stdoutPath, stderrPath } = getDefaultAutostartPaths();
|
|
944
|
+
const out = await readLastLines(stdoutPath, 200).catch(() => '');
|
|
945
|
+
const err = await readLastLines(stderrPath, 200).catch(() => '');
|
|
946
|
+
console.log(bullets([kv('stdout:', stdoutPath), kv('stderr:', stderrPath)]));
|
|
947
|
+
if (out.trim()) console.log(out.trimEnd());
|
|
948
|
+
if (err.trim()) console.log(err.trimEnd());
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
824
951
|
if (process.platform === 'darwin') {
|
|
825
952
|
await showLogs();
|
|
826
953
|
} else {
|
|
@@ -834,6 +961,9 @@ async function main() {
|
|
|
834
961
|
}
|
|
835
962
|
return;
|
|
836
963
|
case 'tail':
|
|
964
|
+
if (process.platform === 'win32') {
|
|
965
|
+
throw new Error('[local] service tail is not supported on Windows yet. Use: hstack service logs');
|
|
966
|
+
}
|
|
837
967
|
if (process.platform === 'darwin') {
|
|
838
968
|
await tailLogs();
|
|
839
969
|
} else {
|
|
@@ -10,6 +10,7 @@ const STACK_HELP_USAGE_LINES = [
|
|
|
10
10
|
'hstack stack create-dev-auth-seed [name] [--server=happier-server|happier-server-light] [--login|--no-login] [--skip-default-seed] [--non-interactive] [--json]',
|
|
11
11
|
'hstack stack daemon <name> start|stop|restart|status [--json]',
|
|
12
12
|
'hstack stack happier <name> [-- ...]',
|
|
13
|
+
'hstack stack bug-report <name> [-- ...]',
|
|
13
14
|
'hstack stack env <name> set KEY=VALUE [KEY2=VALUE2...] | unset KEY [KEY2...] | get KEY | list | path [--json]',
|
|
14
15
|
'hstack stack auth <name> status|login|copy-from [--json]',
|
|
15
16
|
'hstack stack dev <name> [-- ...]',
|
|
@@ -48,6 +49,7 @@ export const STACK_HELP_COMMANDS = [
|
|
|
48
49
|
'daemon',
|
|
49
50
|
'eas',
|
|
50
51
|
'happier',
|
|
52
|
+
'bug-report',
|
|
51
53
|
'env',
|
|
52
54
|
'auth',
|
|
53
55
|
'dev',
|
|
@@ -448,6 +448,43 @@ test('hstack stack daemon <name> start uses runtime server port when env port is
|
|
|
448
448
|
);
|
|
449
449
|
});
|
|
450
450
|
|
|
451
|
+
test('hstack stack daemon <name> start uses explicit HAPPIER_SERVER_URL when env port and runtime port are missing', async (t) => {
|
|
452
|
+
const fixture = await createDaemonFixture(t, {
|
|
453
|
+
prefix: 'happy-stacks-stack-daemon-explicit-server-url-',
|
|
454
|
+
stackName: 'exp-test',
|
|
455
|
+
serverPort: 4101,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
await writeDummyAuth({ cliHomeDir: fixture.stackCliHome });
|
|
459
|
+
|
|
460
|
+
const explicitPort = fixture.serverPort + 9;
|
|
461
|
+
const envPath = join(fixture.storageDir, fixture.stackName, 'env');
|
|
462
|
+
await mkdir(dirname(envPath), { recursive: true });
|
|
463
|
+
await writeFile(
|
|
464
|
+
envPath,
|
|
465
|
+
[
|
|
466
|
+
`HAPPIER_STACK_REPO_DIR=${fixture.baseEnv.HAPPIER_STACK_WORKSPACE_DIR}/happier`,
|
|
467
|
+
`HAPPIER_STACK_CLI_HOME_DIR=${fixture.stackCliHome}`,
|
|
468
|
+
`HAPPIER_SERVER_URL=http://127.0.0.1:${explicitPort}`,
|
|
469
|
+
`HAPPIER_WEBAPP_URL=http://happier-exp-test.localhost:${explicitPort}`,
|
|
470
|
+
'',
|
|
471
|
+
].join('\n'),
|
|
472
|
+
'utf-8'
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
registerDaemonCleanup(t, { env: fixture.baseEnv, stackName: fixture.stackName });
|
|
476
|
+
|
|
477
|
+
const startRes = await runHstack(['stack', 'daemon', fixture.stackName, 'start', '--json'], { env: fixture.baseEnv });
|
|
478
|
+
assertExitOk(startRes, 'stack daemon start uses explicit HAPPIER_SERVER_URL');
|
|
479
|
+
|
|
480
|
+
const logPath = join(fixture.stackCliHome, 'stub-daemon.log');
|
|
481
|
+
const logText = await readLogText(logPath);
|
|
482
|
+
assert.ok(
|
|
483
|
+
logText.includes(`server_url=http://127.0.0.1:${explicitPort}`),
|
|
484
|
+
`expected daemon env to target explicit HAPPIER_SERVER_URL port ${explicitPort}\n${logText}`
|
|
485
|
+
);
|
|
486
|
+
});
|
|
487
|
+
|
|
451
488
|
test('hstack stack auth <name> login --identity=<name> --print prints identity-scoped HAPPIER_HOME_DIR', async (t) => {
|
|
452
489
|
const fixture = await createDaemonFixture(t, {
|
|
453
490
|
prefix: 'happier-stack-auth-identity-',
|
|
@@ -25,6 +25,7 @@ async function writeStubHappyCli({ cliDir, message }) {
|
|
|
25
25
|
[
|
|
26
26
|
`console.log(JSON.stringify({`,
|
|
27
27
|
` message: ${JSON.stringify(message)},`,
|
|
28
|
+
` args: process.argv.slice(2),`,
|
|
28
29
|
` stack: process.env.HAPPIER_STACK_STACK || null,`,
|
|
29
30
|
` envFile: process.env.HAPPIER_STACK_ENV_FILE || null,`,
|
|
30
31
|
` homeDir: process.env.HAPPIER_HOME_DIR || null,`,
|
|
@@ -261,3 +262,38 @@ test('hstack stack <name> happier ... stack-name-first shorthand works', async (
|
|
|
261
262
|
assert.equal(out.message, 'name-first');
|
|
262
263
|
assert.equal(out.stack, fixture.stackName);
|
|
263
264
|
});
|
|
265
|
+
|
|
266
|
+
test('hstack stack bug-report <name> forwards bug-report command under stack env', async (t) => {
|
|
267
|
+
const fixture = await createHappyStackFixture(t, {
|
|
268
|
+
prefix: 'happier-stack-stack-bug-report-',
|
|
269
|
+
message: 'bug-report-alias',
|
|
270
|
+
serverPort: 4099,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const res = await runNodeCapture(
|
|
274
|
+
[
|
|
275
|
+
join(rootDir, 'bin', 'hstack.mjs'),
|
|
276
|
+
'stack',
|
|
277
|
+
'bug-report',
|
|
278
|
+
fixture.stackName,
|
|
279
|
+
'--',
|
|
280
|
+
'--title',
|
|
281
|
+
'CLI bug',
|
|
282
|
+
'--summary',
|
|
283
|
+
'summary',
|
|
284
|
+
'--current-behavior',
|
|
285
|
+
'current',
|
|
286
|
+
'--expected-behavior',
|
|
287
|
+
'expected',
|
|
288
|
+
'--accept-privacy-notice',
|
|
289
|
+
'--no-include-diagnostics',
|
|
290
|
+
],
|
|
291
|
+
{ cwd: rootDir, env: fixture.baseEnv }
|
|
292
|
+
);
|
|
293
|
+
assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
294
|
+
|
|
295
|
+
const out = JSON.parse(res.stdout.trim());
|
|
296
|
+
assert.equal(out.message, 'bug-report-alias');
|
|
297
|
+
assert.equal(out.stack, fixture.stackName);
|
|
298
|
+
assert.equal(out.args[0], 'bug-report');
|
|
299
|
+
});
|