happy-stacks 0.6.11 → 0.6.13
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/commit-audits/happy/_tools/generate-plans.mjs +453 -0
- package/docs/commit-audits/happy/_tools/generate-pr-assignment.mjs +430 -0
- package/docs/commit-audits/happy/_tools/init-pr-assignment-working.mjs +107 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +1849 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +747 -1
- package/docs/commit-audits/happy/leeroy-wip.commit-index.json +11740 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-index.tsv +252 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +18 -11
- package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1297 -140
- package/docs/commit-audits/happy/leeroy-wip.maintainers-overview.draft.md +448 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-assignment.draft.tsv +252 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-assignment.working.tsv +288 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-catalog.draft.md +245 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-stack-plan.draft.md +350 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-deferred-fragments.tsv +65 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-ledger.tsv +56 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-process.md +240 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-status.tsv +39 -0
- package/docs/commit-audits/happy/leeroy-wip.split-plan.draft.md +93 -0
- package/docs/commit-audits/happy/leeroy-wip.topic-buckets.md +76 -0
- package/docs/commit-audits/happy/pr-desc.extraction-ledger.tsv +279 -0
- package/docs/commit-audits/happy/pr-desc.original.md +0 -0
- package/docs/commit-audits/happy/pr-desc.post-audit-extraction-ledger.tsv +54 -0
- package/docs/commit-audits/happy/pr-desc.working-document.md +536 -0
- package/docs/happy-development.md +18 -1
- package/docs/isolated-linux-vm.md +23 -1
- package/docs/stacks.md +21 -1
- package/package.json +1 -1
- package/scripts/auth.mjs +46 -8
- package/scripts/daemon.mjs +44 -21
- package/scripts/doctor.mjs +2 -2
- package/scripts/doctor_cmd.test.mjs +67 -0
- package/scripts/happy.mjs +18 -5
- package/scripts/import.mjs +19 -13
- package/scripts/monorepo.mjs +414 -23
- package/scripts/monorepo_port.test.mjs +136 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +5 -1
- package/scripts/provision/macos-lima-happy-vm.sh +34 -2
- package/scripts/review.mjs +347 -124
- package/scripts/review_pr.mjs +78 -2
- package/scripts/run.mjs +2 -1
- package/scripts/stack.mjs +265 -19
- package/scripts/stack_daemon_cmd.test.mjs +196 -0
- package/scripts/stack_happy_cmd.test.mjs +103 -0
- package/scripts/utils/cli/prereqs.mjs +12 -1
- package/scripts/utils/cli/wizard.mjs +19 -1
- package/scripts/utils/dev/daemon.mjs +3 -1
- package/scripts/utils/llm/assist.mjs +64 -31
- package/scripts/utils/llm/happys_runner.mjs +60 -0
- package/scripts/utils/llm/tools.mjs +3 -3
- package/scripts/utils/proc/pm.mjs +1 -1
- package/scripts/utils/review/detached_worktree.mjs +61 -0
- package/scripts/utils/review/detached_worktree.test.mjs +62 -0
- package/scripts/utils/review/findings.mjs +133 -20
- package/scripts/utils/review/findings.test.mjs +88 -1
- package/scripts/utils/review/runners/augment.mjs +71 -0
- package/scripts/utils/review/runners/augment.test.mjs +42 -0
- package/scripts/utils/review/runners/coderabbit.mjs +54 -10
- package/scripts/utils/review/runners/coderabbit.test.mjs +15 -48
- package/scripts/utils/review/sliced_runner.mjs +39 -0
- package/scripts/utils/review/sliced_runner.test.mjs +47 -0
- package/scripts/utils/review/tool_home_seed.mjs +99 -0
- package/scripts/utils/review/tool_home_seed.test.mjs +113 -0
- package/scripts/utils/stack/cli_identities.mjs +29 -0
- package/scripts/utils/stack/startup.mjs +45 -7
- package/scripts/worktrees.mjs +8 -5
|
@@ -23,6 +23,18 @@ On your macOS host (this repo):
|
|
|
23
23
|
This creates the VM if needed and configures **localhost port forwarding** for the port ranges used by our VM defaults.
|
|
24
24
|
(This is important because the Expo web app uses WebCrypto and needs a secure context like `http://localhost`.)
|
|
25
25
|
|
|
26
|
+
It also sets a higher default VM memory size (to avoid Expo/Metro getting OOM-killed and exiting with `code=137`).
|
|
27
|
+
Override if needed:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
LIMA_MEMORY=12GiB ./scripts/provision/macos-lima-happy-vm.sh happy-test
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Port ranges note:
|
|
34
|
+
- `review-pr` runs in a **fully isolated sandbox** (separate happy-stacks home dir), so VM defaults written to
|
|
35
|
+
`~/.happy-stacks/env.local` inside the VM won’t be read automatically.
|
|
36
|
+
- Prefer passing `--vm-ports` (or explicit `--stack-port-start=...`) to `review-pr` so the sandbox uses the forwarded ranges.
|
|
37
|
+
|
|
26
38
|
### 2b) Manual setup (if you prefer)
|
|
27
39
|
|
|
28
40
|
```bash
|
|
@@ -30,6 +42,13 @@ limactl create --name happy-pr --tty=false template://ubuntu-24.04
|
|
|
30
42
|
limactl start happy-pr
|
|
31
43
|
```
|
|
32
44
|
|
|
45
|
+
If you run `review-pr` (Expo web / Metro) inside the VM, **allocate enough memory** (recommend **8GiB+**).
|
|
46
|
+
Edit `~/.lima/happy-pr/lima.yaml` on the macOS host and set:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
memory: "8GiB"
|
|
50
|
+
```
|
|
51
|
+
|
|
33
52
|
### 2c) Host access (ports + browser URLs)
|
|
34
53
|
|
|
35
54
|
When you want to open Happy/Expo URLs in your macOS browser, **use localhost port forwarding**.
|
|
@@ -116,6 +135,7 @@ Inside the VM:
|
|
|
116
135
|
```bash
|
|
117
136
|
npx --yes happy-stacks@latest review-pr \
|
|
118
137
|
--happy=https://github.com/slopus/happy/pull/<PR_NUMBER> \
|
|
138
|
+
--vm-ports \
|
|
119
139
|
--no-mobile \
|
|
120
140
|
--keep-sandbox \
|
|
121
141
|
--verbose \
|
|
@@ -126,6 +146,8 @@ Notes:
|
|
|
126
146
|
- `--no-mobile` keeps the validation focused (Expo mobile dev-client adds more host requirements).
|
|
127
147
|
- You can also add `--keep-sandbox` if you want to inspect the sandbox contents after a failure.
|
|
128
148
|
- For full reproducibility, pin the version: `npx --yes happy-stacks@0.3.0 review-pr ...`
|
|
149
|
+
- `--vm-ports` forces the stack/server and Expo dev-server (web) ports into the forwarded VM ranges
|
|
150
|
+
(pairs with the `portForwards` config in this doc).
|
|
129
151
|
|
|
130
152
|
### Optional: test **unreleased local changes**
|
|
131
153
|
|
|
@@ -170,7 +192,7 @@ On the macOS host:
|
|
|
170
192
|
```bash
|
|
171
193
|
limactl stop happy-pr || true
|
|
172
194
|
limactl delete happy-pr
|
|
173
|
-
limactl create --name happy-pr --tty=false template
|
|
195
|
+
limactl create --name happy-pr --tty=false template:ubuntu-24.04
|
|
174
196
|
limactl start happy-pr
|
|
175
197
|
```
|
|
176
198
|
|
package/docs/stacks.md
CHANGED
|
@@ -312,9 +312,29 @@ happys stack auth <stack> login
|
|
|
312
312
|
```
|
|
313
313
|
|
|
314
314
|
Notes:
|
|
315
|
+
- You can run **multiple daemons for the same stack** on **different accounts** using `--identity=<name>`.
|
|
316
|
+
- `default` (no flag): `~/.happy/stacks/<stack>/cli/...`
|
|
317
|
+
- `--identity=account-b`: `~/.happy/stacks/<stack>/cli-identities/account-b/...`
|
|
318
|
+
- To authenticate an identity without auto-opening a browser, use `--no-open` (it prints the URL so you can open it
|
|
319
|
+
in the browser profile/incognito window you want):
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
happys stack auth <stack> login --identity=account-a --no-open
|
|
323
|
+
happys stack auth <stack> login --identity=account-b --no-open
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
- To start/stop an identity’s daemon explicitly:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
happys stack daemon <stack> start --identity=account-a
|
|
330
|
+
happys stack daemon <stack> stop --identity=account-a
|
|
331
|
+
```
|
|
332
|
+
|
|
315
333
|
- For the **main** stack, use `<stack>=main` and the default `<port>=3005` (unless you changed it).
|
|
316
334
|
- If you use Tailscale Serve, `HAPPY_WEBAPP_URL` should be your HTTPS URL (what you get from `happys tailscale url`).
|
|
317
|
-
- Logs live under
|
|
335
|
+
- Logs live under:
|
|
336
|
+
- default identity: `~/.happy/stacks/<stack>/cli/logs/`
|
|
337
|
+
- named identities: `~/.happy/stacks/<stack>/cli-identities/<identity>/logs/`
|
|
318
338
|
|
|
319
339
|
## JSON mode
|
|
320
340
|
|
package/package.json
CHANGED
package/scripts/auth.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { resolveHandyMasterSecretFromStack } from './utils/auth/handy_master_sec
|
|
|
28
28
|
import { ensureDir, readTextIfExists } from './utils/fs/ops.mjs';
|
|
29
29
|
import { stackExistsSync } from './utils/stack/stacks.mjs';
|
|
30
30
|
import { checkDaemonState } from './daemon.mjs';
|
|
31
|
+
import { parseCliIdentityOrThrow, resolveCliHomeDirForIdentity } from './utils/stack/cli_identities.mjs';
|
|
31
32
|
import {
|
|
32
33
|
getCliHomeDirFromEnvOrDefault,
|
|
33
34
|
getServerLightDataDirFromEnvOrDefault,
|
|
@@ -95,15 +96,35 @@ function resolveServerComponentForCurrentStack() {
|
|
|
95
96
|
|
|
96
97
|
async function cmdDevKey({ argv, json }) {
|
|
97
98
|
const { flags, kv } = parseArgs(argv);
|
|
99
|
+
|
|
100
|
+
// parseArgs currently only supports --k=v, but UX/docs commonly use: --k "value".
|
|
101
|
+
// Support both forms here (without changing global parsing semantics).
|
|
102
|
+
const argvKvValue = (name) => {
|
|
103
|
+
const n = String(name ?? '').trim();
|
|
104
|
+
if (!n) return '';
|
|
105
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
106
|
+
const a = String(argv[i] ?? '');
|
|
107
|
+
if (a === n) {
|
|
108
|
+
const next = String(argv[i + 1] ?? '');
|
|
109
|
+
if (next && !next.startsWith('--')) return next;
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
if (a.startsWith(`${n}=`)) {
|
|
113
|
+
return a.slice(`${n}=`.length);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return '';
|
|
117
|
+
};
|
|
118
|
+
|
|
98
119
|
const wantPrint = flags.has('--print');
|
|
99
|
-
const fmtRaw = (kv.get('--format') ?? '').trim();
|
|
120
|
+
const fmtRaw = (argvKvValue('--format') || (kv.get('--format') ?? '')).trim();
|
|
100
121
|
// UX: the Happy UI restore screen expects the "backup" (XXXXX-...) format.
|
|
101
122
|
//
|
|
102
123
|
// IMPORTANT: the Happy restore screen treats any key containing '-' as "backup format",
|
|
103
124
|
// so printing a base64url key (which may contain '-') is *not reliably pasteable*.
|
|
104
125
|
// Default to backup always unless explicitly overridden.
|
|
105
126
|
const fmt = fmtRaw || 'backup'; // base64url | backup
|
|
106
|
-
const set = (kv.get('--set') ?? '').trim();
|
|
127
|
+
const set = (argvKvValue('--set') || (kv.get('--set') ?? '')).trim();
|
|
107
128
|
const clear = flags.has('--clear');
|
|
108
129
|
|
|
109
130
|
if (set) {
|
|
@@ -737,6 +758,9 @@ async function cmdCopyFrom({ argv, json }) {
|
|
|
737
758
|
async function cmdStatus({ json }) {
|
|
738
759
|
const rootDir = getRootDir(import.meta.url);
|
|
739
760
|
const stackName = getStackName();
|
|
761
|
+
const argv = process.argv.slice(2);
|
|
762
|
+
const { kv } = parseArgs(argv);
|
|
763
|
+
const identity = parseCliIdentityOrThrow((kv.get('--identity') ?? '').trim());
|
|
740
764
|
|
|
741
765
|
const { port, url: internalServerUrl } = getInternalServerUrlCompat();
|
|
742
766
|
const { defaultPublicUrl, envPublicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port, stackName });
|
|
@@ -748,7 +772,7 @@ async function cmdStatus({ json }) {
|
|
|
748
772
|
stackName,
|
|
749
773
|
});
|
|
750
774
|
|
|
751
|
-
const cliHomeDir = resolveCliHomeDir();
|
|
775
|
+
const cliHomeDir = resolveCliHomeDirForIdentity({ cliHomeDir: resolveCliHomeDir(), identity });
|
|
752
776
|
const accessKeyPath = join(cliHomeDir, 'access.key');
|
|
753
777
|
const settingsPath = join(cliHomeDir, 'settings.json');
|
|
754
778
|
|
|
@@ -773,6 +797,7 @@ async function cmdStatus({ json }) {
|
|
|
773
797
|
internalServerUrl,
|
|
774
798
|
publicServerUrl,
|
|
775
799
|
cliHomeDir,
|
|
800
|
+
cliIdentity: identity,
|
|
776
801
|
auth,
|
|
777
802
|
daemon,
|
|
778
803
|
serverHealth: health,
|
|
@@ -844,11 +869,13 @@ async function cmdLogin({ argv, json }) {
|
|
|
844
869
|
const webappUrl = await preferStackLocalhostUrl(webappUrlRaw, { stackName });
|
|
845
870
|
const webappUrlSource = expoWebappUrl ? 'expo' : envWebappUrl ? 'stack env override' : 'server';
|
|
846
871
|
|
|
847
|
-
const
|
|
872
|
+
const identity = parseCliIdentityOrThrow((kv.get('--identity') ?? '').trim());
|
|
873
|
+
const cliHomeDir = resolveCliHomeDirForIdentity({ cliHomeDir: resolveCliHomeDir(), identity });
|
|
848
874
|
const cliBin = join(getComponentDir(rootDir, 'happy-cli'), 'bin', 'happy.mjs');
|
|
849
875
|
|
|
850
876
|
const force = !argv.includes('--no-force');
|
|
851
877
|
const wantPrint = argv.includes('--print');
|
|
878
|
+
const noOpen = flags.has('--no-open') || flags.has('--no-browser') || flags.has('--no-browser-open');
|
|
852
879
|
const contextRaw =
|
|
853
880
|
(kv.get('--context') ?? process.env.HAPPY_STACKS_AUTH_LOGIN_CONTEXT ?? process.env.HAPPY_LOCAL_AUTH_LOGIN_CONTEXT ?? '')
|
|
854
881
|
.toString()
|
|
@@ -859,18 +886,29 @@ async function cmdLogin({ argv, json }) {
|
|
|
859
886
|
if (force || argv.includes('--force')) {
|
|
860
887
|
nodeArgs.push('--force');
|
|
861
888
|
}
|
|
889
|
+
if (noOpen) {
|
|
890
|
+
nodeArgs.push('--no-open');
|
|
891
|
+
}
|
|
862
892
|
|
|
863
893
|
const env = {
|
|
864
894
|
...process.env,
|
|
865
895
|
HAPPY_HOME_DIR: cliHomeDir,
|
|
866
896
|
HAPPY_SERVER_URL: internalServerUrl,
|
|
867
897
|
HAPPY_WEBAPP_URL: webappUrl,
|
|
898
|
+
...(noOpen ? { HAPPY_NO_BROWSER_OPEN: '1' } : {}),
|
|
868
899
|
};
|
|
869
900
|
|
|
870
901
|
if (wantPrint) {
|
|
871
|
-
const cmd =
|
|
902
|
+
const cmd =
|
|
903
|
+
`HAPPY_HOME_DIR="${cliHomeDir}" ` +
|
|
904
|
+
`HAPPY_SERVER_URL="${internalServerUrl}" ` +
|
|
905
|
+
`HAPPY_WEBAPP_URL="${webappUrl}" ` +
|
|
906
|
+
(noOpen ? `HAPPY_NO_BROWSER_OPEN="1" ` : '') +
|
|
907
|
+
`node "${cliBin}" auth login` +
|
|
908
|
+
(nodeArgs.includes('--force') ? ' --force' : '') +
|
|
909
|
+
(noOpen ? ' --no-open' : '');
|
|
872
910
|
if (json) {
|
|
873
|
-
printResult({ json, data: { ok: true, stackName, cmd } });
|
|
911
|
+
printResult({ json, data: { ok: true, stackName, cliIdentity: identity, cmd } });
|
|
874
912
|
} else {
|
|
875
913
|
console.log(cmd);
|
|
876
914
|
}
|
|
@@ -959,7 +997,7 @@ async function main() {
|
|
|
959
997
|
sectionTitle('Usage (global)'),
|
|
960
998
|
bullets([
|
|
961
999
|
`${dim('status:')} ${cmdFmt('happys auth status')} ${dim('[--json]')}`,
|
|
962
|
-
`${dim('login:')} ${cmdFmt('happys auth login')} ${dim('[--force] [--print] [--json]')}`,
|
|
1000
|
+
`${dim('login:')} ${cmdFmt('happys auth login')} ${dim('[--identity=<name>] [--no-open] [--force] [--print] [--json]')}`,
|
|
963
1001
|
`${dim('seed:')} ${cmdFmt('happys auth copy-from <sourceStack|legacy> --all')} ${dim('[--except=main,dev-auth] [--force] [--with-infra] [--link] [--json]')}`,
|
|
964
1002
|
`${dim('dev key:')} ${cmdFmt('happys auth dev-key')} ${dim('[--print] [--format=base64url|backup] [--set=<secret>] [--clear] [--json]')}`,
|
|
965
1003
|
]),
|
|
@@ -967,7 +1005,7 @@ async function main() {
|
|
|
967
1005
|
sectionTitle('Usage (stack-scoped)'),
|
|
968
1006
|
bullets([
|
|
969
1007
|
`${dim('status:')} ${cmdFmt('happys stack auth <name> status')} ${dim('[--json]')}`,
|
|
970
|
-
`${dim('login:')} ${cmdFmt('happys stack auth <name> login')} ${dim('[--force] [--print] [--json]')}`,
|
|
1008
|
+
`${dim('login:')} ${cmdFmt('happys stack auth <name> login')} ${dim('[--identity=<name>] [--no-open] [--force] [--print] [--json]')}`,
|
|
971
1009
|
`${dim('seed:')} ${cmdFmt('happys stack auth <name> copy-from <sourceStack|legacy>')} ${dim('[--force] [--with-infra] [--link] [--json]')}`,
|
|
972
1010
|
]),
|
|
973
1011
|
'',
|
package/scripts/daemon.mjs
CHANGED
|
@@ -193,28 +193,37 @@ function resolveHappyCliDistEntrypoint(cliBin) {
|
|
|
193
193
|
async function ensureHappyCliDistExists({ cliBin }) {
|
|
194
194
|
const distEntrypoint = resolveHappyCliDistEntrypoint(cliBin);
|
|
195
195
|
if (!distEntrypoint) return { ok: false, distEntrypoint: null, built: false, reason: 'unknown_cli_bin' };
|
|
196
|
-
|
|
196
|
+
const cliDir = join(dirname(cliBin), '..');
|
|
197
197
|
|
|
198
198
|
// Try to recover automatically: missing dist is a common first-run worktree issue.
|
|
199
199
|
// We build in-place using the cliDir that owns this cliBin (../ from bin/).
|
|
200
|
-
const cliDir = join(dirname(cliBin), '..');
|
|
201
200
|
const buildCli =
|
|
202
201
|
(process.env.HAPPY_STACKS_CLI_BUILD ?? process.env.HAPPY_LOCAL_CLI_BUILD ?? '1').toString().trim() !== '0';
|
|
203
202
|
if (!buildCli) {
|
|
204
203
|
return { ok: false, distEntrypoint, built: false, reason: 'build_disabled' };
|
|
205
204
|
}
|
|
206
205
|
|
|
206
|
+
let buildRes = null;
|
|
207
207
|
try {
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
// In auto mode, ensureCliBuilt() is a fast no-op when nothing changed.
|
|
209
|
+
buildRes = await ensureCliBuilt(cliDir, { buildCli: true });
|
|
210
|
+
if (buildRes?.built) {
|
|
211
|
+
// eslint-disable-next-line no-console
|
|
212
|
+
console.warn(`[local] happy-cli: rebuilt (${cliDir})`);
|
|
213
|
+
}
|
|
211
214
|
} catch (e) {
|
|
212
215
|
return { ok: false, distEntrypoint, built: false, reason: String(e?.message ?? e) };
|
|
213
216
|
}
|
|
214
217
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
if (existsSync(distEntrypoint)) {
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
distEntrypoint,
|
|
222
|
+
built: Boolean(buildRes?.built),
|
|
223
|
+
reason: buildRes?.built ? (buildRes.reason ?? 'rebuilt') : 'exists',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return { ok: false, distEntrypoint, built: Boolean(buildRes?.built), reason: buildRes?.built ? 'rebuilt_but_missing' : 'missing' };
|
|
218
227
|
}
|
|
219
228
|
|
|
220
229
|
function excerptIndicatesMissingAuth(excerpt) {
|
|
@@ -243,14 +252,17 @@ function allowDaemonWaitForAuthWithoutTty() {
|
|
|
243
252
|
return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'y';
|
|
244
253
|
}
|
|
245
254
|
|
|
246
|
-
function authLoginHint() {
|
|
247
|
-
const
|
|
248
|
-
|
|
255
|
+
function authLoginHint({ stackName, cliIdentity }) {
|
|
256
|
+
const id = (cliIdentity ?? '').toString().trim();
|
|
257
|
+
const suffix = id && id !== 'default' ? ` --identity=${id} --no-open` : '';
|
|
258
|
+
return stackName === 'main' ? `happys auth login${suffix}` : `happys stack auth ${stackName} login${suffix}`;
|
|
249
259
|
}
|
|
250
260
|
|
|
251
|
-
function authCopyFromSeedHint() {
|
|
252
|
-
const stackName = (process.env.HAPPY_STACKS_STACK ?? process.env.HAPPY_LOCAL_STACK ?? '').trim() || 'main';
|
|
261
|
+
function authCopyFromSeedHint({ stackName, cliIdentity }) {
|
|
253
262
|
if (stackName === 'main') return null;
|
|
263
|
+
// For multi-identity daemons, copying credentials defeats the purpose (multiple accounts).
|
|
264
|
+
const id = (cliIdentity ?? '').toString().trim();
|
|
265
|
+
if (id && id !== 'default') return null;
|
|
254
266
|
const seed = resolveAuthSeedFromEnv(process.env);
|
|
255
267
|
return `happys stack auth ${stackName} copy-from ${seed}`;
|
|
256
268
|
}
|
|
@@ -456,11 +468,16 @@ export async function startLocalDaemonWithAuth({
|
|
|
456
468
|
forceRestart = false,
|
|
457
469
|
env = process.env,
|
|
458
470
|
stackName = null,
|
|
471
|
+
cliIdentity = 'default',
|
|
459
472
|
}) {
|
|
460
473
|
const resolvedStackName =
|
|
461
474
|
(stackName ?? '').toString().trim() ||
|
|
462
475
|
(env.HAPPY_STACKS_STACK ?? env.HAPPY_LOCAL_STACK ?? '').toString().trim() ||
|
|
463
476
|
'main';
|
|
477
|
+
const resolvedCliIdentity =
|
|
478
|
+
(cliIdentity ?? '').toString().trim() ||
|
|
479
|
+
(env.HAPPY_STACKS_CLI_IDENTITY ?? env.HAPPY_LOCAL_CLI_IDENTITY ?? '').toString().trim() ||
|
|
480
|
+
'default';
|
|
464
481
|
const baseEnv = { ...env };
|
|
465
482
|
const daemonEnv = getDaemonEnv({ baseEnv, cliHomeDir, internalServerUrl, publicServerUrl });
|
|
466
483
|
|
|
@@ -497,7 +514,7 @@ export async function startLocalDaemonWithAuth({
|
|
|
497
514
|
return;
|
|
498
515
|
}
|
|
499
516
|
|
|
500
|
-
if (!forceRestart &&
|
|
517
|
+
if (!forceRestart && existing.status === 'running') {
|
|
501
518
|
const pid = existing.pid;
|
|
502
519
|
const matches = await daemonEnvMatches({ pid, cliHomeDir, internalServerUrl, publicServerUrl });
|
|
503
520
|
if (matches === true) {
|
|
@@ -518,6 +535,12 @@ export async function startLocalDaemonWithAuth({
|
|
|
518
535
|
return;
|
|
519
536
|
}
|
|
520
537
|
}
|
|
538
|
+
if (!forceRestart && existing.status === 'starting') {
|
|
539
|
+
// A lock file without a stable daemon.state.json usually means the daemon never finished starting
|
|
540
|
+
// (common when auth is required but daemon start is non-interactive). Attempt a safe restart.
|
|
541
|
+
// eslint-disable-next-line no-console
|
|
542
|
+
console.warn(`[local] daemon appears stuck starting for stack home (pid=${existing.pid}); restarting...`);
|
|
543
|
+
}
|
|
521
544
|
|
|
522
545
|
// Stop any existing daemon for THIS stack home dir.
|
|
523
546
|
try {
|
|
@@ -569,7 +592,7 @@ export async function startLocalDaemonWithAuth({
|
|
|
569
592
|
// daemon status separately.
|
|
570
593
|
await delay(500);
|
|
571
594
|
const stateAfter = checkDaemonState(cliHomeDir);
|
|
572
|
-
if (stateAfter.status === 'running'
|
|
595
|
+
if (stateAfter.status === 'running') {
|
|
573
596
|
return { ok: true, exitCode, excerpt: null, logPath: null };
|
|
574
597
|
}
|
|
575
598
|
|
|
@@ -590,11 +613,11 @@ export async function startLocalDaemonWithAuth({
|
|
|
590
613
|
|
|
591
614
|
if (excerptIndicatesMissingAuth(first.excerpt)) {
|
|
592
615
|
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY) || allowDaemonWaitForAuthWithoutTty();
|
|
593
|
-
const copyHint = authCopyFromSeedHint();
|
|
616
|
+
const copyHint = authCopyFromSeedHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity });
|
|
594
617
|
const hint =
|
|
595
618
|
`[local] daemon is not authenticated yet (expected on first run).\n` +
|
|
596
619
|
`[local] In another terminal, run:\n` +
|
|
597
|
-
`${authLoginHint()}\n` +
|
|
620
|
+
`${authLoginHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity })}\n` +
|
|
598
621
|
(copyHint ? `[local] Or (recommended if main is already logged in):\n${copyHint}\n` : '');
|
|
599
622
|
if (!isInteractive) {
|
|
600
623
|
throw new Error(`${hint}[local] Non-interactive mode: refusing to wait for credentials.`);
|
|
@@ -629,12 +652,12 @@ export async function startLocalDaemonWithAuth({
|
|
|
629
652
|
try {
|
|
630
653
|
await maybeAutoReseedInvalidAuth({ stackName });
|
|
631
654
|
} catch (e) {
|
|
632
|
-
const copyHint = authCopyFromSeedHint();
|
|
655
|
+
const copyHint = authCopyFromSeedHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity });
|
|
633
656
|
console.error(
|
|
634
657
|
`[local] daemon credentials were rejected by the server (401).\n` +
|
|
635
658
|
`[local] Fix:\n` +
|
|
636
659
|
(copyHint ? `- ${copyHint}\n` : '') +
|
|
637
|
-
`- ${authLoginHint()}`
|
|
660
|
+
`- ${authLoginHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity })}`
|
|
638
661
|
);
|
|
639
662
|
throw e;
|
|
640
663
|
}
|
|
@@ -648,13 +671,13 @@ export async function startLocalDaemonWithAuth({
|
|
|
648
671
|
throw new Error('Failed to start daemon (after auth re-seed)');
|
|
649
672
|
}
|
|
650
673
|
} else {
|
|
651
|
-
const copyHint = authCopyFromSeedHint();
|
|
674
|
+
const copyHint = authCopyFromSeedHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity });
|
|
652
675
|
console.error(
|
|
653
676
|
`[local] daemon failed to start (server returned an error).\n` +
|
|
654
677
|
`[local] Try:\n` +
|
|
655
678
|
`- happys doctor\n` +
|
|
656
679
|
(copyHint ? `- ${copyHint}\n` : '') +
|
|
657
|
-
`- ${authLoginHint()}`
|
|
680
|
+
`- ${authLoginHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity })}`
|
|
658
681
|
);
|
|
659
682
|
throw new Error('Failed to start daemon');
|
|
660
683
|
}
|
package/scripts/doctor.mjs
CHANGED
|
@@ -63,7 +63,7 @@ async function fetchHealth(url) {
|
|
|
63
63
|
|
|
64
64
|
async function main() {
|
|
65
65
|
const argv = process.argv.slice(2);
|
|
66
|
-
const { flags, kv } = parseArgs(argv);
|
|
66
|
+
const { flags, kv: argsKv } = parseArgs(argv);
|
|
67
67
|
const fix = flags.has('--fix');
|
|
68
68
|
const json = wantsJson(argv, { flags });
|
|
69
69
|
|
|
@@ -117,7 +117,7 @@ async function main() {
|
|
|
117
117
|
? process.env.HAPPY_LOCAL_UI_BUILD_DIR.trim()
|
|
118
118
|
: join(autostart.baseDir, 'ui');
|
|
119
119
|
|
|
120
|
-
const serverComponentName = getServerComponentName({ kv });
|
|
120
|
+
const serverComponentName = getServerComponentName({ kv: argsKv });
|
|
121
121
|
if (serverComponentName === 'both') {
|
|
122
122
|
throw new Error(`[doctor] --server=both is not supported (pick one: happy-server-light or happy-server)`);
|
|
123
123
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
function runNode(args, { cwd, env }) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const proc = spawn(process.execPath, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
12
|
+
let stdout = '';
|
|
13
|
+
let stderr = '';
|
|
14
|
+
proc.stdout.on('data', (d) => (stdout += String(d)));
|
|
15
|
+
proc.stderr.on('data', (d) => (stderr += String(d)));
|
|
16
|
+
proc.on('error', reject);
|
|
17
|
+
proc.on('exit', (code) => resolve({ code: code ?? 0, stdout, stderr }));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function writeStubHappyCli({ dir }) {
|
|
22
|
+
await mkdir(join(dir, 'bin'), { recursive: true });
|
|
23
|
+
await mkdir(join(dir, 'dist'), { recursive: true });
|
|
24
|
+
|
|
25
|
+
// `scripts/doctor.mjs` runs: node <cliBin> daemon status
|
|
26
|
+
await writeFile(
|
|
27
|
+
join(dir, 'bin', 'happy.mjs'),
|
|
28
|
+
[
|
|
29
|
+
`if (process.argv.includes('daemon') && process.argv.includes('status')) {`,
|
|
30
|
+
` console.log('Daemon is running');`,
|
|
31
|
+
` process.exit(0);`,
|
|
32
|
+
`}`,
|
|
33
|
+
`console.log('ok');`,
|
|
34
|
+
].join('\n'),
|
|
35
|
+
'utf-8'
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Keep dist present to avoid incidental MODULE_NOT_FOUND in other helper paths.
|
|
39
|
+
await writeFile(join(dir, 'dist', 'index.mjs'), `export {};`, 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('doctor does not crash in non-json mode (kv helper not shadowed)', async () => {
|
|
43
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
const rootDir = dirname(scriptsDir);
|
|
45
|
+
|
|
46
|
+
const tmp = await mkdtemp(join(tmpdir(), 'happy-stacks-doctor-'));
|
|
47
|
+
const stubServer = join(tmp, 'happy-server-light');
|
|
48
|
+
const stubCli = join(tmp, 'happy-cli');
|
|
49
|
+
await mkdir(stubServer, { recursive: true });
|
|
50
|
+
await writeStubHappyCli({ dir: stubCli });
|
|
51
|
+
|
|
52
|
+
const env = {
|
|
53
|
+
...process.env,
|
|
54
|
+
HAPPY_STACKS_COMPONENT_DIR_HAPPY_SERVER_LIGHT: stubServer,
|
|
55
|
+
HAPPY_STACKS_COMPONENT_DIR_HAPPY_CLI: stubCli,
|
|
56
|
+
// Avoid UI build dir checks (keeps this test hermetic).
|
|
57
|
+
HAPPY_LOCAL_SERVE_UI: '0',
|
|
58
|
+
// Avoid any side effects in temp runs.
|
|
59
|
+
HAPPY_STACKS_CLI_ROOT_DISABLE: '1',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const res = await runNode([join(rootDir, 'scripts', 'doctor.mjs')], { cwd: rootDir, env });
|
|
63
|
+
assert.equal(res.code, 0, `expected exit 0, got ${res.code}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}`);
|
|
64
|
+
assert.match(res.stdout, /happy-stacks doctor/i);
|
|
65
|
+
assert.match(res.stdout, /Details/);
|
|
66
|
+
});
|
|
67
|
+
|
package/scripts/happy.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import './utils/env/env.mjs';
|
|
2
|
-
import {
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
@@ -43,7 +43,9 @@ async function main() {
|
|
|
43
43
|
const cliDir = getComponentDir(rootDir, 'happy-cli');
|
|
44
44
|
const entrypoint = join(cliDir, 'dist', 'index.mjs');
|
|
45
45
|
if (!existsSync(entrypoint)) {
|
|
46
|
-
|
|
46
|
+
console.error(`[happy] missing happy-cli build at: ${entrypoint}`);
|
|
47
|
+
console.error('Run: happys bootstrap');
|
|
48
|
+
process.exit(1);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const env = { ...process.env };
|
|
@@ -51,14 +53,25 @@ async function main() {
|
|
|
51
53
|
env.HAPPY_SERVER_URL = env.HAPPY_SERVER_URL || internalServerUrl;
|
|
52
54
|
env.HAPPY_WEBAPP_URL = env.HAPPY_WEBAPP_URL || publicServerUrl;
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
const res = spawnSync(process.execPath, ['--no-warnings', '--no-deprecation', entrypoint, ...argv], {
|
|
55
57
|
stdio: 'inherit',
|
|
56
58
|
env,
|
|
57
59
|
});
|
|
60
|
+
|
|
61
|
+
if (res.error) {
|
|
62
|
+
const msg = res.error instanceof Error ? res.error.message : String(res.error);
|
|
63
|
+
console.error(`[happy] failed to run happy-cli: ${msg}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
process.exit(res.status ?? 1);
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
main().catch((err) => {
|
|
61
|
-
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
console.error('[happy] failed:', message);
|
|
73
|
+
if (process.env.DEBUG && err instanceof Error && err.stack) {
|
|
74
|
+
console.error(err.stack);
|
|
75
|
+
}
|
|
62
76
|
process.exit(1);
|
|
63
77
|
});
|
|
64
|
-
|
package/scripts/import.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { readEnvObjectFromFile } from './utils/env/read.mjs';
|
|
|
18
18
|
import { clipboardAvailable, copyTextToClipboard } from './utils/ui/clipboard.mjs';
|
|
19
19
|
import { detectInstalledLlmTools } from './utils/llm/tools.mjs';
|
|
20
20
|
import { launchLlmAssistant } from './utils/llm/assist.mjs';
|
|
21
|
+
import { buildHappyStacksRunnerShellSnippet } from './utils/llm/happys_runner.mjs';
|
|
21
22
|
|
|
22
23
|
function usage() {
|
|
23
24
|
return [
|
|
@@ -505,30 +506,32 @@ function readPinnedComponentDirFromEnvObject(envObj, component) {
|
|
|
505
506
|
}
|
|
506
507
|
|
|
507
508
|
function buildLlmPromptForImport() {
|
|
509
|
+
const hs = buildHappyStacksRunnerShellSnippet();
|
|
508
510
|
return [
|
|
509
511
|
'You are an assistant helping the user migrate legacy Happy split repos into Happy Stacks.',
|
|
510
512
|
'',
|
|
513
|
+
hs,
|
|
511
514
|
'Goals:',
|
|
512
515
|
'- Import legacy split repos (happy / happy-cli / happy-server) into a stack in Happy Stacks.',
|
|
513
516
|
'- Optionally migrate commits into the slopus/happy monorepo layout (expo-app/cli/server).',
|
|
514
517
|
'',
|
|
515
518
|
'How to proceed:',
|
|
516
519
|
'1) Run the guided import wizard:',
|
|
517
|
-
' - `
|
|
520
|
+
' - `hs import`',
|
|
518
521
|
'',
|
|
519
522
|
'Non-interactive (LLM-friendly) variant:',
|
|
520
523
|
'- Inspect candidate repos/worktrees/branches (JSON):',
|
|
521
|
-
' - `
|
|
524
|
+
' - `hs import inspect --happy=<path|url> --happy-cli=<path|url> --happy-server=<path|url> --yes --json`',
|
|
522
525
|
'- Apply pins to a stack (no prompts):',
|
|
523
|
-
' - `
|
|
526
|
+
' - `hs import apply --stack=<name> --server=happy-server-light --happy=<path|url> --happy-ref=<ref> --happy-cli=<path|url> --happy-cli-ref=<ref> --happy-server=<path|url> --happy-server-ref=<ref> --yes`',
|
|
524
527
|
'2) If you want to migrate an existing imported stack later:',
|
|
525
|
-
' - `
|
|
528
|
+
' - `hs import migrate --stack=<stack>`',
|
|
526
529
|
'',
|
|
527
530
|
'Conflict handling (monorepo port):',
|
|
528
|
-
'- Prefer guided mode: `
|
|
531
|
+
'- Prefer guided mode: `hs monorepo port guide --target=<monorepo-root>`',
|
|
529
532
|
'- For machine-readable state, use:',
|
|
530
|
-
' - `
|
|
531
|
-
' - `
|
|
533
|
+
' - `hs monorepo port status --target=<monorepo-root> --json`',
|
|
534
|
+
' - `hs monorepo port continue --target=<monorepo-root>`',
|
|
532
535
|
'',
|
|
533
536
|
'Important:',
|
|
534
537
|
'- A “stack” is an isolated runtime (ports + data + env) under ~/.happy/stacks/<name>.',
|
|
@@ -538,9 +541,11 @@ function buildLlmPromptForImport() {
|
|
|
538
541
|
}
|
|
539
542
|
|
|
540
543
|
function buildLlmPromptForMigrate({ stackName }) {
|
|
544
|
+
const hs = buildHappyStacksRunnerShellSnippet();
|
|
541
545
|
return [
|
|
542
546
|
'You are an assistant helping the user migrate an existing Happy Stacks stack to the monorepo.',
|
|
543
547
|
'',
|
|
548
|
+
hs,
|
|
544
549
|
`Target stack: ${stackName || '<stack>'}`,
|
|
545
550
|
'',
|
|
546
551
|
'Goal:',
|
|
@@ -548,17 +553,17 @@ function buildLlmPromptForMigrate({ stackName }) {
|
|
|
548
553
|
'- Create a new monorepo stack by default (keep the legacy stack intact).',
|
|
549
554
|
'',
|
|
550
555
|
'Command:',
|
|
551
|
-
`-
|
|
556
|
+
`- hs import migrate --stack=${stackName || '<stack>'}`,
|
|
552
557
|
'',
|
|
553
558
|
'Conflict handling:',
|
|
554
|
-
'- This uses `
|
|
555
|
-
'- To inspect machine-readably: `
|
|
559
|
+
'- This uses `hs monorepo port guide` which pauses on conflicts.',
|
|
560
|
+
'- To inspect machine-readably: `hs monorepo port status --target=<monorepo-root> --json`',
|
|
556
561
|
].join('\n');
|
|
557
562
|
}
|
|
558
563
|
|
|
559
564
|
function buildMonorepoMigrationPrompt({ targetMonorepoRoot, branch, sources }) {
|
|
560
565
|
const args = [
|
|
561
|
-
`
|
|
566
|
+
`hs monorepo port --target=${targetMonorepoRoot} --branch=${branch} --3way`,
|
|
562
567
|
sources.happy ? `--from-happy=${sources.happy}` : '',
|
|
563
568
|
sources['happy-cli'] ? `--from-happy-cli=${sources['happy-cli']}` : '',
|
|
564
569
|
sources['happy-server'] ? `--from-happy-server=${sources['happy-server']}` : '',
|
|
@@ -569,6 +574,7 @@ function buildMonorepoMigrationPrompt({ targetMonorepoRoot, branch, sources }) {
|
|
|
569
574
|
return [
|
|
570
575
|
'You are an assistant helping the user migrate split-repo commits into the Happy monorepo layout.',
|
|
571
576
|
'',
|
|
577
|
+
buildHappyStacksRunnerShellSnippet(),
|
|
572
578
|
`Target monorepo worktree: ${targetMonorepoRoot}`,
|
|
573
579
|
`Port branch: ${branch}`,
|
|
574
580
|
'',
|
|
@@ -580,10 +586,10 @@ function buildMonorepoMigrationPrompt({ targetMonorepoRoot, branch, sources }) {
|
|
|
580
586
|
args,
|
|
581
587
|
'',
|
|
582
588
|
'If it stops with conflicts:',
|
|
583
|
-
`- Inspect:
|
|
589
|
+
`- Inspect: hs monorepo port status --target=${targetMonorepoRoot} --json`,
|
|
584
590
|
`- Resolve conflicted files (keep changes scoped to expo-app/, cli/, server/)`,
|
|
585
591
|
`- Stage: git -C ${targetMonorepoRoot} add <files>`,
|
|
586
|
-
`- Continue:
|
|
592
|
+
`- Continue: hs monorepo port continue --target=${targetMonorepoRoot}`,
|
|
587
593
|
'',
|
|
588
594
|
'Repeat status/resolve/continue until ok.',
|
|
589
595
|
].join('\n');
|