happy-stacks 0.3.0 → 0.5.0
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/README.md +93 -40
- package/bin/happys.mjs +158 -16
- package/docs/codex-mcp-resume.md +130 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +17640 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +3845 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +102 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1452 -0
- package/docs/commit-audits/happy/leeroy-wip.manual-review-queue.md +116 -0
- package/docs/happy-development.md +3 -4
- package/docs/isolated-linux-vm.md +82 -0
- package/docs/mobile-ios.md +112 -54
- package/docs/monorepo-migration.md +286 -0
- package/docs/server-flavors.md +19 -3
- package/docs/stacks.md +35 -0
- package/package.json +5 -1
- package/scripts/auth.mjs +32 -10
- package/scripts/build.mjs +55 -8
- package/scripts/daemon.mjs +166 -10
- package/scripts/dev.mjs +198 -50
- package/scripts/doctor.mjs +0 -4
- package/scripts/edison.mjs +6 -4
- package/scripts/env.mjs +150 -0
- package/scripts/env_cmd.test.mjs +128 -0
- package/scripts/init.mjs +8 -3
- package/scripts/install.mjs +207 -69
- package/scripts/lint.mjs +24 -4
- package/scripts/migrate.mjs +3 -12
- package/scripts/mobile.mjs +88 -104
- package/scripts/mobile_dev_client.mjs +83 -0
- package/scripts/monorepo.mjs +1096 -0
- package/scripts/monorepo_port.test.mjs +1470 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
- package/scripts/review.mjs +908 -0
- package/scripts/review_pr.mjs +353 -0
- package/scripts/run.mjs +101 -21
- package/scripts/service.mjs +2 -2
- package/scripts/setup.mjs +189 -68
- package/scripts/setup_pr.mjs +586 -38
- package/scripts/stack.mjs +990 -196
- package/scripts/stack_archive_cmd.test.mjs +91 -0
- package/scripts/stack_editor_workspace_monorepo_root.test.mjs +65 -0
- package/scripts/stack_env_cmd.test.mjs +87 -0
- package/scripts/stack_happy_cmd.test.mjs +126 -0
- package/scripts/stack_interactive_monorepo_group.test.mjs +71 -0
- package/scripts/stack_monorepo_defaults.test.mjs +62 -0
- package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +66 -0
- package/scripts/stack_server_flavors_defaults.test.mjs +55 -0
- package/scripts/stack_shorthand_cmd.test.mjs +55 -0
- package/scripts/stack_wt_list.test.mjs +128 -0
- package/scripts/tailscale.mjs +37 -1
- package/scripts/test.mjs +45 -8
- package/scripts/tui.mjs +395 -39
- package/scripts/typecheck.mjs +24 -4
- package/scripts/utils/auth/daemon_gate.mjs +55 -0
- package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
- package/scripts/utils/auth/guided_pr_auth.mjs +79 -0
- package/scripts/utils/auth/guided_stack_web_login.mjs +75 -0
- package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
- package/scripts/utils/auth/login_ux.mjs +32 -13
- package/scripts/utils/auth/sources.mjs +26 -0
- package/scripts/utils/auth/stack_guided_login.mjs +353 -0
- package/scripts/utils/cli/cli_registry.mjs +43 -4
- package/scripts/utils/cli/cwd_scope.mjs +136 -0
- package/scripts/utils/cli/cwd_scope.test.mjs +110 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/prereqs.mjs +75 -0
- package/scripts/utils/cli/prereqs.test.mjs +34 -0
- package/scripts/utils/cli/progress.mjs +126 -0
- package/scripts/utils/cli/verbosity.mjs +12 -0
- package/scripts/utils/cli/wizard.mjs +17 -9
- package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +60 -0
- package/scripts/utils/dev/daemon.mjs +61 -4
- package/scripts/utils/dev/expo_dev.mjs +430 -0
- package/scripts/utils/dev/expo_dev.test.mjs +76 -0
- package/scripts/utils/dev/server.mjs +36 -42
- package/scripts/utils/dev_auth_key.mjs +169 -0
- package/scripts/utils/edison/git_roots.mjs +29 -0
- package/scripts/utils/edison/git_roots.test.mjs +36 -0
- package/scripts/utils/env/env.mjs +7 -3
- package/scripts/utils/env/env_file.mjs +4 -2
- package/scripts/utils/env/env_file.test.mjs +44 -0
- package/scripts/utils/expo/command.mjs +52 -0
- package/scripts/utils/expo/expo.mjs +20 -1
- package/scripts/utils/expo/metro_ports.mjs +114 -0
- package/scripts/utils/git/git.mjs +67 -0
- package/scripts/utils/git/worktrees.mjs +80 -25
- package/scripts/utils/git/worktrees_monorepo.test.mjs +54 -0
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/mobile/config.mjs +31 -0
- package/scripts/utils/mobile/dev_client_links.mjs +60 -0
- package/scripts/utils/mobile/identifiers.mjs +47 -0
- package/scripts/utils/mobile/identifiers.test.mjs +42 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +98 -0
- package/scripts/utils/net/lan_ip.mjs +24 -0
- package/scripts/utils/net/ports.mjs +9 -1
- package/scripts/utils/net/tcp_forward.mjs +162 -0
- package/scripts/utils/net/url.mjs +30 -0
- package/scripts/utils/net/url.test.mjs +20 -0
- package/scripts/utils/paths/localhost_host.mjs +50 -3
- package/scripts/utils/paths/paths.mjs +159 -40
- package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
- package/scripts/utils/paths/paths_server_flavors.test.mjs +45 -0
- package/scripts/utils/proc/commands.mjs +2 -3
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pm.mjs +176 -22
- package/scripts/utils/proc/pm_spawn.test.mjs +76 -0
- package/scripts/utils/proc/pm_stack_cache_env.test.mjs +142 -0
- package/scripts/utils/proc/proc.mjs +136 -4
- package/scripts/utils/proc/proc.test.mjs +77 -0
- package/scripts/utils/review/base_ref.mjs +74 -0
- package/scripts/utils/review/base_ref.test.mjs +54 -0
- package/scripts/utils/review/chunks.mjs +55 -0
- package/scripts/utils/review/chunks.test.mjs +51 -0
- package/scripts/utils/review/findings.mjs +165 -0
- package/scripts/utils/review/findings.test.mjs +85 -0
- package/scripts/utils/review/head_slice.mjs +153 -0
- package/scripts/utils/review/head_slice.test.mjs +91 -0
- package/scripts/utils/review/instructions/deep.md +20 -0
- package/scripts/utils/review/runners/coderabbit.mjs +61 -0
- package/scripts/utils/review/runners/coderabbit.test.mjs +59 -0
- package/scripts/utils/review/runners/codex.mjs +61 -0
- package/scripts/utils/review/runners/codex.test.mjs +35 -0
- package/scripts/utils/review/slices.mjs +140 -0
- package/scripts/utils/review/slices.test.mjs +32 -0
- package/scripts/utils/review/targets.mjs +24 -0
- package/scripts/utils/review/targets.test.mjs +36 -0
- package/scripts/utils/sandbox/review_pr_sandbox.mjs +106 -0
- package/scripts/utils/server/flavor_scripts.mjs +98 -0
- package/scripts/utils/server/flavor_scripts.test.mjs +146 -0
- package/scripts/utils/server/mobile_api_url.mjs +61 -0
- package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
- package/scripts/utils/server/prisma_import.mjs +37 -0
- package/scripts/utils/server/prisma_import.test.mjs +70 -0
- package/scripts/utils/server/ui_env.mjs +14 -0
- package/scripts/utils/server/ui_env.test.mjs +46 -0
- package/scripts/utils/server/urls.mjs +14 -4
- package/scripts/utils/server/validate.mjs +53 -16
- package/scripts/utils/server/validate.test.mjs +89 -0
- package/scripts/utils/service/autostart_darwin.mjs +42 -2
- package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
- package/scripts/utils/stack/context.mjs +2 -2
- package/scripts/utils/stack/editor_workspace.mjs +6 -6
- package/scripts/utils/stack/interactive_stack_config.mjs +185 -0
- package/scripts/utils/stack/pr_stack_name.mjs +16 -0
- package/scripts/utils/stack/runtime_state.mjs +2 -1
- package/scripts/utils/stack/startup.mjs +120 -13
- package/scripts/utils/stack/startup_server_light_dirs.test.mjs +64 -0
- package/scripts/utils/stack/startup_server_light_generate.test.mjs +70 -0
- package/scripts/utils/stack/startup_server_light_legacy.test.mjs +88 -0
- package/scripts/utils/stack/stop.mjs +15 -4
- package/scripts/utils/stack_context.mjs +23 -0
- package/scripts/utils/stack_runtime_state.mjs +104 -0
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/tailscale/ip.mjs +116 -0
- package/scripts/utils/ui/ansi.mjs +39 -0
- package/scripts/utils/ui/qr.mjs +17 -0
- package/scripts/utils/validate.mjs +88 -0
- package/scripts/where.mjs +2 -2
- package/scripts/worktrees.mjs +755 -179
- package/scripts/worktrees_archive_cmd.test.mjs +245 -0
- package/scripts/worktrees_cursor_monorepo_root.test.mjs +63 -0
- package/scripts/worktrees_list_specs_no_recurse.test.mjs +33 -0
- package/scripts/worktrees_monorepo_use_group.test.mjs +67 -0
- package/scripts/utils/dev/expo_web.mjs +0 -112
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared policy for when the stack runner should start the Happy daemon.
|
|
6
|
+
*
|
|
7
|
+
* In `setup-pr` / `review-pr` guided login flows we intentionally start server+UI first,
|
|
8
|
+
* then guide authentication, then start daemon post-auth. Starting the daemon before
|
|
9
|
+
* credentials exist can strand it in its own auth flow (lock held, no machine registration),
|
|
10
|
+
* which leads to "no machines" in the UI.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export function credentialsPathForCliHomeDir(cliHomeDir) {
|
|
14
|
+
return join(String(cliHomeDir ?? ''), 'access.key');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function hasStackCredentials({ cliHomeDir }) {
|
|
18
|
+
if (!cliHomeDir) return false;
|
|
19
|
+
return existsSync(credentialsPathForCliHomeDir(cliHomeDir));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isAuthFlowEnabled(env) {
|
|
23
|
+
const v = (env?.HAPPY_STACKS_AUTH_FLOW ?? env?.HAPPY_LOCAL_AUTH_FLOW ?? '').toString().trim();
|
|
24
|
+
return v === '1' || v.toLowerCase() === 'true';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns { ok: boolean, reason: string } where ok=true means it's safe to start the daemon now.
|
|
29
|
+
* When ok=false, callers should either:
|
|
30
|
+
* - run interactive auth first (TTY), or
|
|
31
|
+
* - skip daemon start without error in orchestrated auth flows, or
|
|
32
|
+
* - fail closed in non-interactive contexts.
|
|
33
|
+
*/
|
|
34
|
+
export function daemonStartGate({ env, cliHomeDir }) {
|
|
35
|
+
if (hasStackCredentials({ cliHomeDir })) {
|
|
36
|
+
return { ok: true, reason: 'credentials_present' };
|
|
37
|
+
}
|
|
38
|
+
if (isAuthFlowEnabled(env)) {
|
|
39
|
+
// Orchestrated auth flow (setup-pr/review-pr): keep server/UI up and let the orchestrator
|
|
40
|
+
// run guided login; starting the daemon now is counterproductive.
|
|
41
|
+
return { ok: false, reason: 'auth_flow_missing_credentials' };
|
|
42
|
+
}
|
|
43
|
+
return { ok: false, reason: 'missing_credentials' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function formatDaemonAuthRequiredError({ stackName, cliHomeDir }) {
|
|
47
|
+
const name = (stackName ?? '').toString().trim() || 'main';
|
|
48
|
+
const path = credentialsPathForCliHomeDir(cliHomeDir);
|
|
49
|
+
return (
|
|
50
|
+
`[local] daemon auth required: credentials not found for stack "${name}".\n` +
|
|
51
|
+
`[local] expected: ${path}\n` +
|
|
52
|
+
`[local] fix: run \`happy auth login\` (stack-scoped), or re-run with UI enabled to complete guided login.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { mkdtemp, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
|
|
7
|
+
import { daemonStartGate, hasStackCredentials } from './daemon_gate.mjs';
|
|
8
|
+
|
|
9
|
+
test('hasStackCredentials detects access.key', async () => {
|
|
10
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-daemon-gate-'));
|
|
11
|
+
assert.equal(hasStackCredentials({ cliHomeDir: dir }), false);
|
|
12
|
+
await writeFile(join(dir, 'access.key'), 'dummy', 'utf-8');
|
|
13
|
+
assert.equal(hasStackCredentials({ cliHomeDir: dir }), true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('daemonStartGate blocks daemon start in auth flow when missing credentials', async () => {
|
|
17
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-daemon-gate-'));
|
|
18
|
+
const gate = daemonStartGate({ env: { HAPPY_STACKS_AUTH_FLOW: '1' }, cliHomeDir: dir });
|
|
19
|
+
assert.equal(gate.ok, false);
|
|
20
|
+
assert.equal(gate.reason, 'auth_flow_missing_credentials');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('daemonStartGate blocks daemon start when missing credentials (non-auth flow)', async () => {
|
|
24
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-daemon-gate-'));
|
|
25
|
+
const gate = daemonStartGate({ env: {}, cliHomeDir: dir });
|
|
26
|
+
assert.equal(gate.ok, false);
|
|
27
|
+
assert.equal(gate.reason, 'missing_credentials');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('daemonStartGate allows daemon start when credentials exist', async () => {
|
|
31
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-daemon-gate-'));
|
|
32
|
+
await writeFile(join(dir, 'access.key'), 'dummy', 'utf-8');
|
|
33
|
+
const gate = daemonStartGate({ env: {}, cliHomeDir: dir });
|
|
34
|
+
assert.equal(gate.ok, true);
|
|
35
|
+
assert.equal(gate.reason, 'credentials_present');
|
|
36
|
+
});
|
|
37
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isTty, promptSelect, withRl } from '../cli/wizard.mjs';
|
|
2
|
+
import { detectSeedableAuthSources } from './sources.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Decide how a PR review stack should authenticate.
|
|
6
|
+
*
|
|
7
|
+
* This deliberately does NOT offer "legacy ~/.happy" sources:
|
|
8
|
+
* for production/remote Happy installs we cannot reliably seed local DB Account rows, so it leads to broken stacks.
|
|
9
|
+
*/
|
|
10
|
+
export async function decidePrAuthPlan({
|
|
11
|
+
interactive = isTty(),
|
|
12
|
+
seedAuthFlag = null,
|
|
13
|
+
explicitFrom = '',
|
|
14
|
+
defaultLoginNow = true,
|
|
15
|
+
} = {}) {
|
|
16
|
+
if (seedAuthFlag === false) return { mode: 'login', loginNow: defaultLoginNow };
|
|
17
|
+
if (seedAuthFlag === true) {
|
|
18
|
+
// Caller must supply from; if not, pick best available.
|
|
19
|
+
const sources = detectSeedableAuthSources();
|
|
20
|
+
const from = explicitFrom || sources[0] || 'main';
|
|
21
|
+
return { mode: 'seed', from, link: true };
|
|
22
|
+
}
|
|
23
|
+
if (explicitFrom) {
|
|
24
|
+
return { mode: 'seed', from: explicitFrom, link: true };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const sources = detectSeedableAuthSources();
|
|
28
|
+
if (!interactive) {
|
|
29
|
+
// Non-interactive default: prefer seeding only if explicitly configured elsewhere.
|
|
30
|
+
// setup-pr will handle its own defaults.
|
|
31
|
+
return { mode: 'auto', sources };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If there's nothing to reuse, don't ask a pointless question.
|
|
35
|
+
if (!sources.length) {
|
|
36
|
+
return { mode: 'login', loginNow: defaultLoginNow, reason: 'no_seed_sources' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Interactive prompt: keep it simple for reviewers.
|
|
40
|
+
const choice = await withRl(async (rl) => {
|
|
41
|
+
const opts = [];
|
|
42
|
+
if (sources.length) {
|
|
43
|
+
opts.push({ label: `reuse existing Happy Stacks auth (${sources.join(' / ')})`, value: 'seed' });
|
|
44
|
+
}
|
|
45
|
+
opts.push({ label: defaultLoginNow ? 'login now (recommended)' : 'login later', value: 'login' });
|
|
46
|
+
return await promptSelect(rl, {
|
|
47
|
+
title: 'Authentication for this PR stack:',
|
|
48
|
+
options: opts,
|
|
49
|
+
defaultIndex: 0,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (choice === 'seed' && sources.length) {
|
|
54
|
+
let from = sources[0];
|
|
55
|
+
if (sources.length > 1) {
|
|
56
|
+
from = await withRl(async (rl) => {
|
|
57
|
+
return await promptSelect(rl, {
|
|
58
|
+
title: 'Which existing auth should we reuse?',
|
|
59
|
+
options: sources.map((s) => ({ label: s, value: s })),
|
|
60
|
+
defaultIndex: 0,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const link = await withRl(async (rl) => {
|
|
65
|
+
return await promptSelect(rl, {
|
|
66
|
+
title: 'When reusing, symlink or copy credentials?',
|
|
67
|
+
options: [
|
|
68
|
+
{ label: 'symlink (recommended) — stays up to date', value: true },
|
|
69
|
+
{ label: 'copy — more isolated per stack', value: false },
|
|
70
|
+
],
|
|
71
|
+
defaultIndex: 0,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
return { mode: 'seed', from: String(from), link: Boolean(link) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { mode: 'login', loginNow: defaultLoginNow };
|
|
78
|
+
}
|
|
79
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { prompt, withRl } from '../cli/wizard.mjs';
|
|
2
|
+
import { openUrlInBrowser } from '../ui/browser.mjs';
|
|
3
|
+
import { preferStackLocalhostUrl } from '../paths/localhost_host.mjs';
|
|
4
|
+
|
|
5
|
+
function supportsAnsi() {
|
|
6
|
+
if (!process.stdout.isTTY) return false;
|
|
7
|
+
if (process.env.NO_COLOR) return false;
|
|
8
|
+
if ((process.env.TERM ?? '').toLowerCase() === 'dumb') return false;
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function bold(s) {
|
|
13
|
+
return supportsAnsi() ? `\x1b[1m${s}\x1b[0m` : String(s);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function dim(s) {
|
|
17
|
+
return supportsAnsi() ? `\x1b[2m${s}\x1b[0m` : String(s);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function cyan(s) {
|
|
21
|
+
return supportsAnsi() ? `\x1b[36m${s}\x1b[0m` : String(s);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function green(s) {
|
|
25
|
+
return supportsAnsi() ? `\x1b[32m${s}\x1b[0m` : String(s);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function guidedStackWebSignupThenLogin({ webappUrl, stackName }) {
|
|
29
|
+
const url = await preferStackLocalhostUrl(webappUrl, { stackName });
|
|
30
|
+
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.log('');
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.log(bold('Happy login'));
|
|
35
|
+
|
|
36
|
+
// Step 1/2
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
console.log(dim('Step 1/2 — open the web app'));
|
|
39
|
+
// eslint-disable-next-line no-console
|
|
40
|
+
console.log(`We’ll open the Happy web app so you can ${bold('create an account')} (or ${bold('log in')}).`);
|
|
41
|
+
if (url) {
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
console.log(`${dim('URL:')} ${cyan(url)}`);
|
|
44
|
+
}
|
|
45
|
+
// eslint-disable-next-line no-console
|
|
46
|
+
console.log('');
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.log(`${bold('Press Enter')} to open it in your browser.`);
|
|
49
|
+
await withRl(async (rl) => {
|
|
50
|
+
await prompt(rl, '', { defaultValue: '' });
|
|
51
|
+
});
|
|
52
|
+
if (url) {
|
|
53
|
+
await openUrlInBrowser(url);
|
|
54
|
+
}
|
|
55
|
+
// eslint-disable-next-line no-console
|
|
56
|
+
console.log(green('✓ Browser opened'));
|
|
57
|
+
|
|
58
|
+
// Step 2/2
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.log('');
|
|
61
|
+
// eslint-disable-next-line no-console
|
|
62
|
+
console.log(dim('Step 2/2 — connect this terminal'));
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
console.log(`Next, we’ll connect ${bold('this terminal')} to your Happy account.`);
|
|
65
|
+
// eslint-disable-next-line no-console
|
|
66
|
+
console.log(`When prompted, choose: ${bold('Web Browser')} ${dim('(press 2)')}.`);
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.log('');
|
|
69
|
+
// eslint-disable-next-line no-console
|
|
70
|
+
console.log(`After you’ve created/logged in in the browser, ${bold('press Enter')} to continue.`);
|
|
71
|
+
await withRl(async (rl) => {
|
|
72
|
+
await prompt(rl, '', { defaultValue: '' });
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { isTty, prompt, withRl } from '../cli/wizard.mjs';
|
|
5
|
+
import { detectSeedableAuthSources } from './sources.mjs';
|
|
6
|
+
import { guidedStackAuthLoginNow, stackAuthCopyFrom } from './stack_guided_login.mjs';
|
|
7
|
+
|
|
8
|
+
export function needsAuthSeed({ cliHomeDir, accountCount }) {
|
|
9
|
+
const accessKeyPath = join(cliHomeDir, 'access.key');
|
|
10
|
+
const hasAccessKey = existsSync(accessKeyPath);
|
|
11
|
+
const hasAccounts = typeof accountCount === 'number' ? accountCount > 0 : null;
|
|
12
|
+
return !hasAccessKey || hasAccounts === false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeChoice(raw) {
|
|
16
|
+
const s = String(raw ?? '').trim().toLowerCase();
|
|
17
|
+
if (!s) return '';
|
|
18
|
+
return s[0];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function maybeRunInteractiveStackAuthSetup({
|
|
22
|
+
rootDir,
|
|
23
|
+
env = process.env,
|
|
24
|
+
stackName,
|
|
25
|
+
cliHomeDir,
|
|
26
|
+
accountCount,
|
|
27
|
+
isInteractive = isTty(),
|
|
28
|
+
autoSeedEnabled = false,
|
|
29
|
+
beforeLogin = null,
|
|
30
|
+
} = {}) {
|
|
31
|
+
if (!isInteractive) return { ok: true, skipped: true, reason: 'non_interactive' };
|
|
32
|
+
if (autoSeedEnabled) return { ok: true, skipped: true, reason: 'auto_seed_enabled' };
|
|
33
|
+
if (!needsAuthSeed({ cliHomeDir, accountCount })) return { ok: true, skipped: true, reason: 'already_initialized' };
|
|
34
|
+
|
|
35
|
+
const sources = detectSeedableAuthSources().filter((s) => s && s !== stackName);
|
|
36
|
+
const hasDevAuth = sources.includes('dev-auth');
|
|
37
|
+
const hasMain = sources.includes('main');
|
|
38
|
+
|
|
39
|
+
let choice = 'login';
|
|
40
|
+
if (hasDevAuth || hasMain) {
|
|
41
|
+
const defaultLetter = hasDevAuth ? 'Y' : hasMain ? 'M' : 'N';
|
|
42
|
+
const promptLine =
|
|
43
|
+
`[local] auth: stack "${stackName}" needs authentication.\n` +
|
|
44
|
+
`[local] Choose one:\n` +
|
|
45
|
+
(hasDevAuth ? ` - Y: copy from dev-auth\n` : '') +
|
|
46
|
+
(hasMain ? ` - M: copy from main\n` : '') +
|
|
47
|
+
` - N: login now\n`;
|
|
48
|
+
|
|
49
|
+
// eslint-disable-next-line no-console
|
|
50
|
+
console.log(promptLine);
|
|
51
|
+
const answer = await withRl(async (rl) => {
|
|
52
|
+
return await prompt(rl, `Pick [Y/M/N] (default: ${defaultLetter}): `, { defaultValue: defaultLetter });
|
|
53
|
+
});
|
|
54
|
+
const c = normalizeChoice(answer);
|
|
55
|
+
if (c === 'y' && hasDevAuth) choice = 'dev-auth';
|
|
56
|
+
else if (c === 'm' && hasMain) choice = 'main';
|
|
57
|
+
else if (c === 'n') choice = 'login';
|
|
58
|
+
else choice = hasDevAuth ? 'dev-auth' : hasMain ? 'main' : 'login';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (choice === 'login') {
|
|
62
|
+
if (beforeLogin && typeof beforeLogin === 'function') {
|
|
63
|
+
await beforeLogin();
|
|
64
|
+
}
|
|
65
|
+
await guidedStackAuthLoginNow({ rootDir, stackName, env });
|
|
66
|
+
return { ok: true, skipped: false, mode: 'login' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await stackAuthCopyFrom({ rootDir, stackName, fromStackName: String(choice), env, link: true });
|
|
70
|
+
return { ok: true, skipped: false, mode: 'seed', from: String(choice), link: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
@@ -8,6 +8,21 @@ export function normalizeAuthLoginContext(raw) {
|
|
|
8
8
|
return 'generic';
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function supportsAnsi() {
|
|
12
|
+
if (!process.stdout.isTTY) return false;
|
|
13
|
+
if (process.env.NO_COLOR) return false;
|
|
14
|
+
if ((process.env.TERM ?? '').toLowerCase() === 'dumb') return false;
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function bold(s) {
|
|
19
|
+
return supportsAnsi() ? `\x1b[1m${s}\x1b[0m` : String(s);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function dim(s) {
|
|
23
|
+
return supportsAnsi() ? `\x1b[2m${s}\x1b[0m` : String(s);
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
export function printAuthLoginInstructions({
|
|
12
27
|
stackName,
|
|
13
28
|
context = 'generic',
|
|
@@ -18,23 +33,27 @@ export function printAuthLoginInstructions({
|
|
|
18
33
|
rerunCmd,
|
|
19
34
|
}) {
|
|
20
35
|
const ctx = normalizeAuthLoginContext(context);
|
|
21
|
-
const
|
|
36
|
+
const subtitle =
|
|
22
37
|
ctx === 'selfhost'
|
|
23
|
-
? '
|
|
38
|
+
? 'Self-host'
|
|
24
39
|
: ctx === 'dev'
|
|
25
|
-
? '
|
|
40
|
+
? 'Dev'
|
|
26
41
|
: ctx === 'stack'
|
|
27
|
-
? `
|
|
28
|
-
: '
|
|
42
|
+
? `Stack: ${stackName || 'unknown'}`
|
|
43
|
+
: '';
|
|
29
44
|
|
|
30
45
|
// eslint-disable-next-line no-console
|
|
31
46
|
console.log('');
|
|
32
47
|
// eslint-disable-next-line no-console
|
|
33
|
-
console.log(
|
|
48
|
+
console.log(bold('Happy login'));
|
|
49
|
+
if (subtitle) {
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
console.log(dim(subtitle));
|
|
52
|
+
}
|
|
34
53
|
// eslint-disable-next-line no-console
|
|
35
|
-
console.log('
|
|
54
|
+
console.log('Steps:');
|
|
36
55
|
// eslint-disable-next-line no-console
|
|
37
|
-
console.log(' 1) A browser window will open
|
|
56
|
+
console.log(' 1) A browser window will open');
|
|
38
57
|
// eslint-disable-next-line no-console
|
|
39
58
|
console.log(' 2) Sign in (or create an account if this is your first time)');
|
|
40
59
|
// eslint-disable-next-line no-console
|
|
@@ -46,28 +65,28 @@ export function printAuthLoginInstructions({
|
|
|
46
65
|
// eslint-disable-next-line no-console
|
|
47
66
|
console.log('');
|
|
48
67
|
// eslint-disable-next-line no-console
|
|
49
|
-
console.log(`
|
|
68
|
+
console.log(`Web app: ${webappUrl}${webappUrlSource ? ` (${webappUrlSource})` : ''}`);
|
|
50
69
|
}
|
|
51
70
|
if (internalServerUrl) {
|
|
52
71
|
// eslint-disable-next-line no-console
|
|
53
|
-
console.log(`
|
|
72
|
+
console.log(`Internal: ${internalServerUrl}`);
|
|
54
73
|
}
|
|
55
74
|
if (publicServerUrl) {
|
|
56
75
|
// eslint-disable-next-line no-console
|
|
57
|
-
console.log(`
|
|
76
|
+
console.log(`Public: ${publicServerUrl}`);
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
if (ctx === 'selfhost') {
|
|
61
80
|
// eslint-disable-next-line no-console
|
|
62
81
|
console.log('');
|
|
63
82
|
// eslint-disable-next-line no-console
|
|
64
|
-
console.log('
|
|
83
|
+
console.log(dim('Note: this is required so the daemon can register this machine and sync sessions across devices.'));
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
// eslint-disable-next-line no-console
|
|
68
87
|
console.log('');
|
|
69
88
|
// eslint-disable-next-line no-console
|
|
70
|
-
console.log('
|
|
89
|
+
console.log('Tips:');
|
|
71
90
|
// eslint-disable-next-line no-console
|
|
72
91
|
console.log('- If the browser page does not load, make sure Happy is running and reachable.');
|
|
73
92
|
// eslint-disable-next-line no-console
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
import { resolveStackEnvPath } from '../paths/paths.mjs';
|
|
3
6
|
|
|
4
7
|
export function isLegacyAuthSourceName(name) {
|
|
5
8
|
const s = String(name ?? '').trim().toLowerCase();
|
|
@@ -10,3 +13,26 @@ export function getLegacyHappyBaseDir() {
|
|
|
10
13
|
return join(homedir(), '.happy');
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
export function stackHasAccessKey(stackName) {
|
|
17
|
+
try {
|
|
18
|
+
const { baseDir, envPath } = resolveStackEnvPath(stackName);
|
|
19
|
+
if (!existsSync(envPath)) return false;
|
|
20
|
+
return existsSync(join(baseDir, 'cli', 'access.key'));
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Seed sources that are safe to reuse locally.
|
|
28
|
+
*
|
|
29
|
+
* Note: deliberately does NOT include legacy ~/.happy sources; in many contexts we cannot reliably
|
|
30
|
+
* seed DB Account rows, which leads to broken stacks.
|
|
31
|
+
*/
|
|
32
|
+
export function detectSeedableAuthSources() {
|
|
33
|
+
const out = [];
|
|
34
|
+
if (stackHasAccessKey('dev-auth')) out.push('dev-auth');
|
|
35
|
+
if (stackHasAccessKey('main')) out.push('main');
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|