@happier-dev/stack 0.1.0-preview.142.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.
@@ -11,5 +11,9 @@ export declare function buildLaunchdPlistXml(params: Readonly<{
11
11
  workingDirectory?: string;
12
12
  keepAliveOnFailure?: boolean;
13
13
  startIntervalSec?: number;
14
+ startCalendarInterval?: Readonly<{
15
+ hour: number;
16
+ minute: number;
17
+ }>;
14
18
  }>): string;
15
19
  //# sourceMappingURL=launchd.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"launchd.d.ts","sourceRoot":"","sources":["../../src/service/launchd.ts"],"names":[],"mappings":"AASA,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,QAAQ,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAM,GAAG,MAAM,CAmBxG;AAWD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC,GAAG,MAAM,CAmEV"}
1
+ {"version":3,"file":"launchd.d.ts","sourceRoot":"","sources":["../../src/service/launchd.ts"],"names":[],"mappings":"AASA,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,QAAQ,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAM,GAAG,MAAM,CAmBxG;AAWD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,QAAQ,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpE,CAAC,GAAG,MAAM,CA0FV"}
@@ -65,6 +65,26 @@ export function buildLaunchdPlistXml(params) {
65
65
  const startInterval = interval
66
66
  ? `\n <key>StartInterval</key>\n <integer>${interval}</integer>\n`
67
67
  : '';
68
+ const calendar = params.startCalendarInterval;
69
+ const calHourRaw = Number(calendar?.hour);
70
+ const calMinuteRaw = Number(calendar?.minute);
71
+ const calHour = Number.isFinite(calHourRaw) ? Math.floor(calHourRaw) : NaN;
72
+ const calMinute = Number.isFinite(calMinuteRaw) ? Math.floor(calMinuteRaw) : NaN;
73
+ const hasCalendar = Number.isFinite(calHour)
74
+ && Number.isFinite(calMinute)
75
+ && calHour >= 0
76
+ && calHour <= 23
77
+ && calMinute >= 0
78
+ && calMinute <= 59;
79
+ const startCalendarInterval = hasCalendar
80
+ ? (`\n <key>StartCalendarInterval</key>\n` +
81
+ ` <dict>\n` +
82
+ ` <key>Hour</key>\n` +
83
+ ` <integer>${calHour}</integer>\n` +
84
+ ` <key>Minute</key>\n` +
85
+ ` <integer>${calMinute}</integer>\n` +
86
+ ` </dict>\n`)
87
+ : '';
68
88
  return `<?xml version="1.0" encoding="UTF-8"?>
69
89
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
70
90
  <plist version="1.0">
@@ -80,7 +100,7 @@ ${programArgsXml}
80
100
  <key>RunAtLoad</key>
81
101
  <true/>
82
102
  ${keepAlive}
83
- ${startInterval}
103
+ ${startCalendarInterval || startInterval}
84
104
  ${workingDirXml} <key>StandardOutPath</key>
85
105
  <string>${xmlEscape(stdoutPath)}</string>
86
106
  <key>StandardErrorPath</key>
@@ -1 +1 @@
1
- {"version":3,"file":"launchd.js","sourceRoot":"","sources":["../../src/service/launchd.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,SAAS,SAAS,CAAC,CAAS;IAC1B,OAAO,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;SACnB,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,SAA6D,EAAE;IAC9F,6EAA6E;IAC7E,gFAAgF;IAChF,0CAA0C;IAC1C,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACtD,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACtD,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAClD,MAAM,QAAQ,GAAG,SAAS,CAAC,gEAAgE,CAAC,CAAC;IAC7F,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1C,MAAM,OAAO,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IAEpC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,QAAQ,EAAE,GAAG,OAAO,EAAE,GAAG,QAAQ,CAAC,EAAE,CAAC;QAC1D,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAC7B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACf,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,+BAA+B,CAAC;AAC1D,CAAC;AAED,SAAS,SAAS,CAAC,CAAS;IAC1B,OAAO,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;SACnB,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;SACxB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC;SACzB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MASnC;IACA,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAChD,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IAEjD,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC;QACnD,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;QAChE,CAAC,CAAC,EAAE,CAAC;IACP,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAEzE,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACtF,MAAM,cAAc,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnG,MAAM,MAAM,GAAG,UAAU;SACtB,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,SAAS,CAAC,CAAC,CAAC,yBAAyB,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,CAAC;SACzG,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1D,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1D,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAEtE,MAAM,aAAa,GAAG,gBAAgB;QACpC,CAAC,CAAC,kDAAkD,SAAS,CAAC,gBAAgB,CAAC,aAAa;QAC5F,CAAC,CAAC,IAAI,CAAC;IAET,MAAM,SAAS,GAAG,MAAM,CAAC,kBAAkB,KAAK,KAAK;QACnD,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,CACE,8BAA8B;YAC9B,cAAc;YACd,mCAAmC;YACnC,kBAAkB;YAClB,eAAe,CAChB,CAAC;IAEN,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/F,MAAM,aAAa,GAAG,QAAQ;QAC5B,CAAC,CAAC,gDAAgD,QAAQ,cAAc;QACxE,CAAC,CAAC,EAAE,CAAC;IAEP,OAAO;;;;;cAKK,SAAS,CAAC,KAAK,CAAC;;;;EAI5B,cAAc;;;;;EAKd,SAAS;EACT,aAAa;EACb,aAAa;cACD,SAAS,CAAC,UAAU,CAAC;;cAErB,SAAS,CAAC,UAAU,CAAC;;;;EAIjC,MAAM;;;;CAIP,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"launchd.js","sourceRoot":"","sources":["../../src/service/launchd.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@happier-dev/stack",
3
3
  "type": "module",
4
- "version": "0.1.0-preview.142.1",
4
+ "version": "0.1.0-preview.21.1",
5
5
  "repository": "happier-dev/happier",
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org",
@@ -119,6 +119,8 @@ test('guided auth login fails closed when Expo web UI is not ready (does not fal
119
119
  /guid(ed)? login web UI is still not ready|startup failed/i,
120
120
  `stderr:\n${res.stderr}`
121
121
  );
122
+ assert.match(res.stderr, /Stack runtime path:/i, `stderr:\n${res.stderr}`);
123
+ assert.match(res.stderr, /server health:/i, `stderr:\n${res.stderr}`);
122
124
  assert.doesNotMatch(res.stdout, new RegExp(`URL: http://localhost:${fixture.port}\\b`), `stdout:\n${res.stdout}`);
123
125
  } finally {
124
126
  if (fixture) await fixture.cleanup();
@@ -2,10 +2,13 @@ import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
- import { join, resolve } from 'node:path';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
6
7
 
7
8
  import { bundleWorkspaceDeps } from './bundleWorkspaceDeps.mjs';
8
9
 
10
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
11
+
9
12
  function writeJson(path, value) {
10
13
  writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
11
14
  }
@@ -91,3 +94,25 @@ test('bundleWorkspaceDeps throws when cli-common package.json is malformed', ()
91
94
  rmSync(repoRoot, { recursive: true, force: true });
92
95
  }
93
96
  });
97
+
98
+ test('declares external runtime dependencies required by bundled workspace packages', () => {
99
+ const stackPackageJson = JSON.parse(readFileSync(resolve(repoRoot, 'apps', 'stack', 'package.json'), 'utf8'));
100
+ const bundledWorkspacePackagePaths = [
101
+ resolve(repoRoot, 'packages', 'cli-common', 'package.json'),
102
+ resolve(repoRoot, 'packages', 'release-runtime', 'package.json'),
103
+ ];
104
+
105
+ const stackDependencyNames = new Set(Object.keys(stackPackageJson.dependencies ?? {}));
106
+ const requiredExternalDependencies = new Set();
107
+ for (const packageJsonPath of bundledWorkspacePackagePaths) {
108
+ const bundledPackageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
109
+ for (const dependencyName of Object.keys(bundledPackageJson.dependencies ?? {})) {
110
+ if (!dependencyName.startsWith('@happier-dev/')) {
111
+ requiredExternalDependencies.add(dependencyName);
112
+ }
113
+ }
114
+ }
115
+
116
+ const missingDependencies = [...requiredExternalDependencies].filter((name) => !stackDependencyNames.has(name));
117
+ assert.deepEqual(missingDependencies, []);
118
+ });
@@ -130,8 +130,8 @@ async function main() {
130
130
  const stackCtx = resolveStackContext({ env, autostart });
131
131
  const { stackMode, runtimeStatePath, stackName, envPath } = stackCtx;
132
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.
133
+ // Expo CLI resolves a Metro port early, but it won't actually bind it until late in the native build.
134
+ // If we stick to the default 8081, builds are much more likely to fail late if another Metro/Expo claims 8081 mid-build.
135
135
  //
136
136
  // Strategy:
137
137
  // - If the user explicitly sets --port or HAPPIER_STACK_MOBILE_PORT, honor it.
@@ -333,11 +333,8 @@ async function main() {
333
333
  }
334
334
 
335
335
  const configuration = kv.get('--configuration') ?? 'Debug';
336
- const buildMetroPort = (env.RCT_METRO_PORT ?? env.EXPO_PACKAGER_PORT ?? '').toString().trim();
337
- const args = ['run:ios', '--no-bundler', '--no-build-cache', '--configuration', configuration];
338
- if (buildMetroPort) {
339
- args.push('-p', buildMetroPort);
340
- }
336
+ const metroPort = String(env.RCT_METRO_PORT ?? portRaw ?? '8081');
337
+ const args = ['run:ios', '--port', metroPort, '--no-build-cache', '--configuration', configuration];
341
338
  if (device) {
342
339
  args.push('-d', device);
343
340
  }
@@ -3,11 +3,10 @@ import { parseArgs } from './utils/cli/args.mjs';
3
3
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
4
4
  import { run } from './utils/proc/proc.mjs';
5
5
  import { getRootDir } from './utils/paths/paths.mjs';
6
- import { join } from 'node:path';
7
6
  import { banner, cmd, sectionTitle } from './utils/ui/layout.mjs';
8
7
  import { cyan, dim, yellow } from './utils/ui/ansi.mjs';
9
8
 
10
- import { defaultDevClientIdentity } from './utils/mobile/identifiers.mjs';
9
+ import { buildMobileDevClientInstallInvocation } from './utils/mobile/dev_client_install_invocation.mjs';
11
10
 
12
11
  async function main() {
13
12
  const argv = process.argv.slice(2);
@@ -18,13 +17,13 @@ async function main() {
18
17
  printResult({
19
18
  json,
20
19
  data: {
21
- flags: ['--device=<id-or-name>', '--clean', '--configuration=Debug|Release', '--json'],
20
+ flags: ['--device=<id-or-name>', '--port=<port>', '--clean', '--configuration=Debug|Release', '--json'],
22
21
  },
23
22
  text: [
24
23
  banner('mobile-dev-client', { subtitle: 'Install the shared iOS dev-client app (one-time).' }),
25
24
  '',
26
25
  sectionTitle('usage:'),
27
- ` ${cyan('hstack mobile-dev-client')} --install [--device=...] [--clean] [--configuration=Debug|Release] [--json]`,
26
+ ` ${cyan('hstack mobile-dev-client')} --install [--device=...] [--port=...] [--clean] [--configuration=Debug|Release] [--json]`,
28
27
  '',
29
28
  sectionTitle('notes:'),
30
29
  `- Installs a dedicated ${cyan('hstack Dev')} Expo dev-client app on your iPhone.`,
@@ -45,39 +44,15 @@ async function main() {
45
44
  }
46
45
 
47
46
  const rootDir = getRootDir(import.meta.url);
48
- const mobileScript = join(rootDir, 'scripts', 'mobile.mjs');
49
-
50
- const device = kv.get('--device') ?? '';
51
- const clean = flags.has('--clean');
52
- const configuration = kv.get('--configuration') ?? 'Debug';
53
-
54
- const id = defaultDevClientIdentity({ user: process.env.USER ?? process.env.USERNAME ?? 'user' });
55
-
56
- const args = [
57
- mobileScript,
58
- '--app-env=development',
59
- `--ios-app-name=${id.iosAppName}`,
60
- `--ios-bundle-id=${id.iosBundleId}`,
61
- `--scheme=${id.scheme}`,
62
- '--prebuild',
63
- ...(clean ? ['--clean'] : []),
64
- '--run-ios',
65
- `--configuration=${configuration}`,
66
- '--no-metro',
67
- ...(device ? [`--device=${device}`] : []),
68
- ];
47
+ const invocation = buildMobileDevClientInstallInvocation({ rootDir, argv, baseEnv: process.env });
69
48
 
70
49
  const env = {
71
- ...process.env,
72
- // Ensure Expo app config uses the dev-client scheme.
73
- EXPO_APP_SCHEME: id.scheme,
74
- // Ensure per-stack storage isolation is available during dev-client usage.
75
- EXPO_PUBLIC_HAPPY_STORAGE_SCOPE: process.env.EXPO_PUBLIC_HAPPY_STORAGE_SCOPE ?? '',
50
+ ...invocation.env,
76
51
  };
77
52
 
78
- const out = await run(process.execPath, args, { cwd: rootDir, env });
53
+ const out = await run(process.execPath, invocation.nodeArgs, { cwd: rootDir, env });
79
54
  if (json) {
80
- printResult({ json, data: { ok: true, installed: true, identity: id, out } });
55
+ printResult({ json, data: { ok: true, installed: true, identity: invocation.identity, out } });
81
56
  }
82
57
  }
83
58
 
@@ -0,0 +1,24 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import { runNodeCapture } from './testkit/auth_testkit.mjs';
7
+
8
+ test('mobile-dev-client --help runs without syntax errors', async () => {
9
+ const scriptsDir = dirname(fileURLToPath(import.meta.url));
10
+ const rootDir = dirname(scriptsDir);
11
+ const script = join(rootDir, 'scripts', 'mobile_dev_client.mjs');
12
+
13
+ const env = {
14
+ ...process.env,
15
+ // Prevent env.mjs from selecting a real stack env file (keeps the test fast and hermetic).
16
+ HAPPIER_STACK_ENV_FILE: join(rootDir, 'scripts', 'nonexistent-env'),
17
+ };
18
+
19
+ const res = await runNodeCapture([script, '--help'], { cwd: rootDir, env });
20
+ assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstderr:\n${res.stderr}\nstdout:\n${res.stdout}`);
21
+ assert.match(res.stdout, /\bmobile-dev-client\b/, `expected help to mention command name\nstdout:\n${res.stdout}`);
22
+ assert.match(res.stdout, /--port(?:=|\b)/, `expected help to mention --port\nstdout:\n${res.stdout}`);
23
+ });
24
+
@@ -18,7 +18,7 @@ function parseKeyValueLines(text) {
18
18
  return out;
19
19
  }
20
20
 
21
- test('hstack mobile --run-ios passes -p/--port to Expo (avoids default 8081)', async () => {
21
+ test('hstack mobile --run-ios passes --port to Expo so the native build and dev server use the same Metro port', async () => {
22
22
  const rootDir = getStackRootFromMeta(import.meta.url);
23
23
  const mobileScript = join(rootDir, 'scripts', 'mobile.mjs');
24
24
 
@@ -72,7 +72,8 @@ process.exit(0);
72
72
  const env = {
73
73
  ...process.env,
74
74
  // Ensure xcrun runs fast/deterministically in tests.
75
- PATH: binDir,
75
+ // Include system paths so env.mjs won't prepend /usr/bin ahead of our stub.
76
+ PATH: `${binDir}:/usr/bin:/bin`,
76
77
  HAPPIER_STACK_REPO_DIR: repoDir,
77
78
  HAPPIER_STACK_HOME_DIR: join(tmp, 'home'),
78
79
  HAPPIER_STACK_STORAGE_DIR: storageDir,
@@ -90,12 +91,13 @@ process.exit(0);
90
91
 
91
92
  assert.ok(port, `expected RCT_METRO_PORT to be set\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
92
93
  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}`);
94
+ assert.ok(!args.includes('-p'), `expected expo args to not include -p\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
95
+ assert.ok(!args.includes('--no-bundler'), `expected expo args to not include --no-bundler\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
96
+ assert.ok(args.includes('--port'), `expected expo args to include --port\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
97
+ const portIdx = args.indexOf('--port');
98
+ assert.ok(portIdx >= 0 && args[portIdx + 1] === port, `expected --port to match RCT_METRO_PORT\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
96
99
  assert.notEqual(port, '8081', `expected non-default port to reduce collisions\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
97
100
  } finally {
98
101
  await rm(tmp, { recursive: true, force: true });
99
102
  }
100
103
  });
101
-
@@ -0,0 +1,106 @@
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 --port (long flag) so Expo uses the requested Metro port', 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-longport-'));
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
+ await writeFile(
55
+ expoBin,
56
+ `#!${process.execPath}
57
+ console.log('ARGS=' + JSON.stringify(process.argv.slice(2)));
58
+ console.log('RCT_METRO_PORT=' + (process.env.RCT_METRO_PORT ?? ''));
59
+ console.log('EXPO_PACKAGER_PORT=' + (process.env.EXPO_PACKAGER_PORT ?? ''));
60
+ process.exit(0);
61
+ `,
62
+ 'utf-8'
63
+ );
64
+ if (process.platform !== 'win32') {
65
+ await chmod(expoBin, 0o755);
66
+ }
67
+
68
+ await mkdir(join(storageDir, 'main'), { recursive: true });
69
+
70
+ const env = {
71
+ ...process.env,
72
+ // Ensure our xcrun stub wins over /usr/bin/xcrun even after env.mjs normalizes PATH.
73
+ PATH: `${binDir}:/usr/bin:/bin`,
74
+ HAPPIER_STACK_REPO_DIR: repoDir,
75
+ HAPPIER_STACK_HOME_DIR: join(tmp, 'home'),
76
+ HAPPIER_STACK_STORAGE_DIR: storageDir,
77
+ HAPPIER_STACK_STACK: 'main',
78
+ HAPPIER_STACK_TAILSCALE_PREFER_PUBLIC_URL: '0',
79
+ HAPPIER_STACK_TAILSCALE_SERVE: '0',
80
+ HAPPIER_STACK_ENV_FILE: join(tmp, 'nonexistent-env'),
81
+ };
82
+
83
+ const res = await runNodeCapture([mobileScript, '--run-ios', '--no-metro', '--port=14362'], { cwd: rootDir, env });
84
+ assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstderr:\n${res.stderr}\nstdout:\n${res.stdout}`);
85
+ const kv = parseKeyValueLines(res.stdout);
86
+ const args = JSON.parse(kv.ARGS ?? '[]');
87
+
88
+ assert.ok(!args.includes('--no-bundler'), `expected expo args to not include --no-bundler\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
89
+ assert.ok(args.includes('--port'), `expected expo args to include --port\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
90
+ assert.ok(!args.includes('-p'), `expected expo args to not include -p\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
91
+ const portIdx = args.indexOf('--port');
92
+ assert.ok(portIdx >= 0 && args[portIdx + 1] === '14362', `expected --port 14362\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
93
+ assert.equal(
94
+ kv.RCT_METRO_PORT,
95
+ '14362',
96
+ `expected RCT_METRO_PORT to be set from hstack --port\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`
97
+ );
98
+ assert.equal(
99
+ kv.EXPO_PACKAGER_PORT,
100
+ '14362',
101
+ `expected EXPO_PACKAGER_PORT to match\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`
102
+ );
103
+ } finally {
104
+ await rm(tmp, { recursive: true, force: true });
105
+ }
106
+ });
@@ -84,10 +84,16 @@ function usageText() {
84
84
  ' [--server-url=<url>] [--webapp-url=<url>] [--public-server-url=<url>]',
85
85
  ' [--json]',
86
86
  '',
87
+ ' hstack remote server setup --ssh <user@host> [--preview|--stable] [--channel <stable|preview>]',
88
+ ' [--mode <user|system>]',
89
+ ' [--env KEY=VALUE]...',
90
+ ' [--json]',
91
+ '',
87
92
  'notes:',
88
93
  ' - This command runs remote operations over ssh.',
89
94
  ' - It installs the Happier CLI on the remote host, pairs credentials, and optionally installs/starts the daemon service.',
90
95
  ' - Default service mode is user; set --service none to skip daemon service setup.',
96
+ ' - Remote server setup installs the self-host runtime as a service (default: user mode).',
91
97
  ].join('\n');
92
98
  }
93
99
 
@@ -117,6 +123,43 @@ function resolveService(argv) {
117
123
  return v || 'user';
118
124
  }
119
125
 
126
+ function resolveMode(argv) {
127
+ if (argv.includes('--system')) return 'system';
128
+ if (argv.includes('--user')) return 'user';
129
+ const picked = argv.find((a) => a === '--mode' || a.startsWith('--mode='));
130
+ if (!picked) return 'user';
131
+ if (picked === '--mode') {
132
+ const idx = argv.indexOf('--mode');
133
+ const v = String(argv[idx + 1] ?? '').trim().toLowerCase();
134
+ return v || 'user';
135
+ }
136
+ const v = String(picked.slice('--mode='.length)).trim().toLowerCase();
137
+ return v || 'user';
138
+ }
139
+
140
+ function collectEnvValues(argv) {
141
+ const args = Array.isArray(argv) ? argv.map(String) : [];
142
+ const values = [];
143
+ for (let i = 0; i < args.length; i += 1) {
144
+ const a = args[i] ?? '';
145
+ if (a === '--env') {
146
+ const next = args[i + 1] ?? '';
147
+ if (!next || next.startsWith('--')) {
148
+ throw new Error('[remote] missing value for --env (expected KEY=VALUE)');
149
+ }
150
+ values.push(String(next));
151
+ i += 1;
152
+ continue;
153
+ }
154
+ if (a.startsWith('--env=')) {
155
+ const raw = a.slice('--env='.length);
156
+ if (!raw) throw new Error('[remote] missing value for --env (expected KEY=VALUE)');
157
+ values.push(String(raw));
158
+ }
159
+ }
160
+ return values;
161
+ }
162
+
120
163
  async function runRemoteDaemonSetup(argvRaw) {
121
164
  const argv0 = argvRaw.slice();
122
165
  const json = wantsJson(argv0);
@@ -209,6 +252,71 @@ async function runRemoteDaemonSetup(argvRaw) {
209
252
  });
210
253
  }
211
254
 
255
+ async function runRemoteServerSetup(argvRaw) {
256
+ const argv0 = argvRaw.slice();
257
+ const json = wantsJson(argv0);
258
+
259
+ let args = argv0.slice();
260
+ const ssh = takeFlagValue(args, '--ssh');
261
+ args = ssh.rest;
262
+ if (!ssh.value) {
263
+ process.stderr.write('Missing required flag: --ssh <user@host>\n');
264
+ process.exit(2);
265
+ }
266
+
267
+ const channel = resolveChannel(argv0);
268
+ if (channel !== 'stable' && channel !== 'preview') {
269
+ throw new Error(`[remote] invalid --channel value: ${channel}`);
270
+ }
271
+
272
+ const mode = resolveMode(argv0);
273
+ if (mode !== 'user' && mode !== 'system') {
274
+ throw new Error(`[remote] invalid --mode value: ${mode} (expected user or system)`);
275
+ }
276
+
277
+ const envValues = collectEnvValues(argv0);
278
+
279
+ const installUrl = 'https://happier.dev/install';
280
+ const remoteHstack = '$HOME/.happier/bin/hstack';
281
+
282
+ // Always disable auto-service setup in the installer so this command controls remote service behavior.
283
+ const installCmd = [
284
+ `curl -fsSL ${installUrl} |`,
285
+ `HAPPIER_CHANNEL=${channel} HAPPIER_WITH_DAEMON=0 HAPPIER_NONINTERACTIVE=1 bash`,
286
+ ].join(' ');
287
+
288
+ await runSsh({ target: ssh.value, command: installCmd });
289
+
290
+ const envArgs = envValues.map((value) => `--env ${safeBashSingleQuote(value)}`).join(' ');
291
+ const baseSelfHostCmd = [
292
+ remoteHstack,
293
+ 'self-host',
294
+ 'install',
295
+ `--channel=${channel}`,
296
+ `--mode=${mode}`,
297
+ '--without-cli',
298
+ '--non-interactive',
299
+ '--json',
300
+ ].join(' ');
301
+ const selfHostCmd = `${mode === 'system' ? 'sudo -E ' : ''}${baseSelfHostCmd}${envArgs ? ` ${envArgs}` : ''}`;
302
+
303
+ await runSsh({ target: ssh.value, command: selfHostCmd });
304
+
305
+ printResult({
306
+ json,
307
+ data: { ok: true, ssh: ssh.value, channel, mode, env: envValues },
308
+ text: json
309
+ ? null
310
+ : [
311
+ '✓ Remote server setup complete',
312
+ `- ssh: ${ssh.value}`,
313
+ `- channel: ${channel}`,
314
+ `- mode: ${mode}`,
315
+ `- env: ${envValues.length ? envValues.join(', ') : '(none)'}`,
316
+ ].join('\n'),
317
+ });
318
+ }
319
+
212
320
  async function main() {
213
321
  const argvRaw = process.argv.slice(2);
214
322
  if (argvRaw.length === 0 || wantsHelp(argvRaw)) {
@@ -224,6 +332,10 @@ async function main() {
224
332
  await runRemoteDaemonSetup(argvRaw);
225
333
  return;
226
334
  }
335
+ if (top === 'server' && sub === 'setup') {
336
+ await runRemoteServerSetup(argvRaw);
337
+ return;
338
+ }
227
339
 
228
340
  printResult({
229
341
  json: wantsJson(argvRaw),