happy-stacks 0.2.0 → 0.4.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 +84 -25
- package/bin/happys.mjs +116 -17
- package/docs/happy-development.md +2 -2
- package/docs/isolated-linux-vm.md +82 -0
- package/docs/mobile-ios.md +112 -54
- package/package.json +5 -1
- package/scripts/auth.mjs +59 -208
- package/scripts/build.mjs +58 -12
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +5 -5
- package/scripts/daemon.mjs +168 -20
- package/scripts/dev.mjs +196 -70
- package/scripts/doctor.mjs +20 -36
- package/scripts/edison.mjs +105 -78
- package/scripts/happy.mjs +8 -19
- package/scripts/init.mjs +8 -14
- package/scripts/install.mjs +119 -23
- package/scripts/lint.mjs +31 -32
- package/scripts/menubar.mjs +6 -13
- package/scripts/migrate.mjs +11 -21
- package/scripts/mobile.mjs +93 -108
- package/scripts/mobile_dev_client.mjs +83 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
- package/scripts/review.mjs +217 -0
- package/scripts/review_pr.mjs +368 -0
- package/scripts/run.mjs +95 -21
- package/scripts/self.mjs +11 -29
- package/scripts/server_flavor.mjs +4 -4
- package/scripts/service.mjs +19 -29
- package/scripts/setup.mjs +63 -160
- package/scripts/setup_pr.mjs +592 -52
- package/scripts/stack.mjs +608 -200
- package/scripts/stop.mjs +3 -3
- package/scripts/tailscale.mjs +44 -11
- package/scripts/test.mjs +52 -36
- package/scripts/tui.mjs +314 -74
- package/scripts/typecheck.mjs +31 -32
- package/scripts/ui_gateway.mjs +1 -1
- package/scripts/uninstall.mjs +6 -6
- package/scripts/utils/auth/daemon_gate.mjs +55 -0
- package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
- package/scripts/utils/auth/dev_key.mjs +163 -0
- package/scripts/utils/{auth_files.mjs → auth/files.mjs} +2 -4
- 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/handy_master_secret.mjs +68 -0
- package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
- package/scripts/utils/{auth_login_ux.mjs → auth/login_ux.mjs} +32 -13
- package/scripts/utils/auth/sources.mjs +38 -0
- package/scripts/utils/auth/stack_guided_login.mjs +353 -0
- package/scripts/utils/cli/cli_registry.mjs +24 -0
- package/scripts/utils/cli/cwd_scope.mjs +82 -0
- package/scripts/utils/cli/cwd_scope.test.mjs +77 -0
- package/scripts/utils/cli/flags.mjs +17 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/normalize.mjs +16 -0
- package/scripts/utils/cli/prereqs.mjs +72 -0
- package/scripts/utils/cli/progress.mjs +126 -0
- package/scripts/utils/cli/smoke_help.mjs +2 -2
- package/scripts/utils/cli/verbosity.mjs +12 -0
- package/scripts/utils/cli/wizard.mjs +1 -1
- package/scripts/utils/crypto/tokens.mjs +14 -0
- package/scripts/utils/{dev_daemon.mjs → dev/daemon.mjs} +51 -7
- package/scripts/utils/dev/expo_dev.mjs +246 -0
- package/scripts/utils/dev/expo_dev.test.mjs +76 -0
- package/scripts/utils/{dev_server.mjs → dev/server.mjs} +22 -32
- package/scripts/utils/dev_auth_key.mjs +1 -1
- package/scripts/utils/{config.mjs → env/config.mjs} +3 -2
- package/scripts/utils/{dotenv.mjs → env/dotenv.mjs} +3 -0
- package/scripts/utils/{env.mjs → env/env.mjs} +5 -3
- package/scripts/utils/{env_file.mjs → env/env_file.mjs} +2 -1
- package/scripts/utils/{env_local.mjs → env/env_local.mjs} +1 -0
- package/scripts/utils/env/read.mjs +30 -0
- package/scripts/utils/env/values.mjs +13 -0
- package/scripts/utils/expo/command.mjs +52 -0
- package/scripts/utils/{expo.mjs → expo/expo.mjs} +23 -10
- package/scripts/utils/expo/metro_ports.mjs +114 -0
- package/scripts/utils/fs/json.mjs +25 -0
- package/scripts/utils/fs/ops.mjs +29 -0
- package/scripts/utils/fs/package_json.mjs +8 -0
- package/scripts/utils/fs/tail.mjs +12 -0
- package/scripts/utils/git/git.mjs +67 -0
- package/scripts/utils/git/refs.mjs +26 -0
- package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +27 -23
- package/scripts/utils/handy_master_secret.mjs +2 -2
- 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/dns.mjs +10 -0
- package/scripts/utils/net/lan_ip.mjs +24 -0
- package/scripts/utils/{ports.mjs → net/ports.mjs} +12 -6
- package/scripts/utils/net/url.mjs +30 -0
- package/scripts/utils/net/url.test.mjs +20 -0
- package/scripts/utils/paths/localhost_host.mjs +56 -0
- package/scripts/utils/{paths.mjs → paths/paths.mjs} +52 -45
- package/scripts/utils/{runtime.mjs → paths/runtime.mjs} +3 -1
- package/scripts/utils/proc/commands.mjs +34 -0
- package/scripts/utils/{ownership.mjs → proc/ownership.mjs} +1 -1
- package/scripts/utils/proc/package_scripts.mjs +31 -0
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pids.mjs +11 -0
- package/scripts/utils/{pm.mjs → proc/pm.mjs} +128 -158
- package/scripts/utils/{proc.mjs → proc/proc.mjs} +77 -2
- package/scripts/utils/review/base_ref.mjs +74 -0
- package/scripts/utils/review/base_ref.test.mjs +54 -0
- package/scripts/utils/review/runners/coderabbit.mjs +19 -0
- package/scripts/utils/review/runners/codex.mjs +51 -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/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +10 -49
- 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/port.mjs +68 -0
- package/scripts/utils/{server.mjs → server/server.mjs} +12 -0
- package/scripts/utils/server/urls.mjs +101 -0
- package/scripts/utils/server/validate.mjs +88 -0
- package/scripts/utils/service/autostart_darwin.mjs +182 -0
- package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
- package/scripts/utils/stack/context.mjs +23 -0
- package/scripts/utils/stack/dirs.mjs +27 -0
- package/scripts/utils/stack/editor_workspace.mjs +152 -0
- package/scripts/utils/stack/names.mjs +12 -0
- package/scripts/utils/stack/pr_stack_name.mjs +16 -0
- package/scripts/utils/stack/runtime_state.mjs +88 -0
- package/scripts/utils/stack/stacks.mjs +45 -0
- package/scripts/utils/{stack_startup.mjs → stack/startup.mjs} +9 -2
- package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +24 -19
- package/scripts/utils/stack_context.mjs +3 -3
- package/scripts/utils/stack_runtime_state.mjs +1 -1
- package/scripts/utils/stacks.mjs +2 -2
- package/scripts/utils/{browser.mjs → ui/browser.mjs} +1 -1
- package/scripts/utils/ui/qr.mjs +17 -0
- package/scripts/utils/ui/text.mjs +16 -0
- package/scripts/utils/validate.mjs +1 -1
- package/scripts/where.mjs +6 -6
- package/scripts/worktrees.mjs +171 -113
- package/scripts/utils/auth_sources.mjs +0 -12
- package/scripts/utils/dev_expo_web.mjs +0 -112
- package/scripts/utils/localhost_host.mjs +0 -17
- package/scripts/utils/server_port.mjs +0 -9
- package/scripts/utils/server_urls.mjs +0 -54
- /package/scripts/utils/{sandbox.mjs → env/sandbox.mjs} +0 -0
- /package/scripts/utils/{fs.mjs → fs/fs.mjs} +0 -0
- /package/scripts/utils/{canonical_home.mjs → paths/canonical_home.mjs} +0 -0
- /package/scripts/utils/{watch.mjs → proc/watch.mjs} +0 -0
package/scripts/uninstall.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
|
|
3
3
|
import { rm } from 'node:fs/promises';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
@@ -7,11 +7,11 @@ import { spawnSync } from 'node:child_process';
|
|
|
7
7
|
|
|
8
8
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
9
9
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
10
|
-
import { expandHome } from './utils/canonical_home.mjs';
|
|
11
|
-
import { getHappyStacksHomeDir, getRootDir, getStacksStorageRoot } from './utils/paths.mjs';
|
|
12
|
-
import { getRuntimeDir } from './utils/runtime.mjs';
|
|
13
|
-
import { getCanonicalHomeEnvPath } from './utils/config.mjs';
|
|
14
|
-
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
10
|
+
import { expandHome } from './utils/paths/canonical_home.mjs';
|
|
11
|
+
import { getHappyStacksHomeDir, getRootDir, getStacksStorageRoot } from './utils/paths/paths.mjs';
|
|
12
|
+
import { getRuntimeDir } from './utils/paths/runtime.mjs';
|
|
13
|
+
import { getCanonicalHomeEnvPath } from './utils/env/config.mjs';
|
|
14
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
15
15
|
|
|
16
16
|
function resolveWorkspaceDir({ rootDir, homeDir }) {
|
|
17
17
|
// Uninstall should never default to deleting the repo root (getWorkspaceDir() can fall back to cliRootDir).
|
|
@@ -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,163 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { getHappyStacksHomeDir } from '../paths/paths.mjs';
|
|
6
|
+
|
|
7
|
+
export function getDevAuthKeyPath(env = process.env) {
|
|
8
|
+
return join(getHappyStacksHomeDir(env), 'keys', 'dev-auth.json');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function base64UrlToBytes(s) {
|
|
12
|
+
try {
|
|
13
|
+
const raw = String(s ?? '').trim();
|
|
14
|
+
if (!raw) return null;
|
|
15
|
+
const b64 = raw.replace(/-/g, '+').replace(/_/g, '/');
|
|
16
|
+
const pad = b64.length % 4 === 0 ? '' : '='.repeat(4 - (b64.length % 4));
|
|
17
|
+
return new Uint8Array(Buffer.from(b64 + pad, 'base64'));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function bytesToBase64Url(bytes) {
|
|
24
|
+
const b64 = Buffer.from(bytes).toString('base64');
|
|
25
|
+
return b64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Base32 alphabet (RFC 4648)
|
|
29
|
+
const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
30
|
+
|
|
31
|
+
function bytesToBase32(bytes) {
|
|
32
|
+
let result = '';
|
|
33
|
+
let buffer = 0;
|
|
34
|
+
let bufferLength = 0;
|
|
35
|
+
|
|
36
|
+
for (const byte of bytes) {
|
|
37
|
+
buffer = (buffer << 8) | byte;
|
|
38
|
+
bufferLength += 8;
|
|
39
|
+
while (bufferLength >= 5) {
|
|
40
|
+
bufferLength -= 5;
|
|
41
|
+
result += BASE32_ALPHABET[(buffer >> bufferLength) & 0x1f];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (bufferLength > 0) {
|
|
45
|
+
result += BASE32_ALPHABET[(buffer << (5 - bufferLength)) & 0x1f];
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function base32ToBytes(base32) {
|
|
51
|
+
let normalized = String(base32 ?? '')
|
|
52
|
+
.toUpperCase()
|
|
53
|
+
.replace(/0/g, 'O')
|
|
54
|
+
.replace(/1/g, 'I')
|
|
55
|
+
.replace(/8/g, 'B')
|
|
56
|
+
.replace(/9/g, 'G');
|
|
57
|
+
const cleaned = normalized.replace(/[^A-Z2-7]/g, '');
|
|
58
|
+
if (!cleaned) throw new Error('no valid base32 characters');
|
|
59
|
+
|
|
60
|
+
const bytes = [];
|
|
61
|
+
let buffer = 0;
|
|
62
|
+
let bufferLength = 0;
|
|
63
|
+
for (const char of cleaned) {
|
|
64
|
+
const value = BASE32_ALPHABET.indexOf(char);
|
|
65
|
+
if (value === -1) throw new Error('invalid base32 character');
|
|
66
|
+
buffer = (buffer << 5) | value;
|
|
67
|
+
bufferLength += 5;
|
|
68
|
+
if (bufferLength >= 8) {
|
|
69
|
+
bufferLength -= 8;
|
|
70
|
+
bytes.push((buffer >> bufferLength) & 0xff);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return new Uint8Array(bytes);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function normalizeDevAuthKeyInputToBytes(input) {
|
|
77
|
+
const raw = String(input ?? '').trim();
|
|
78
|
+
if (!raw) return null;
|
|
79
|
+
|
|
80
|
+
// Match Happy UI behavior:
|
|
81
|
+
// - backup format is base32 and is long (usually grouped with '-' / spaces)
|
|
82
|
+
// - base64url is short (~43 chars) and may contain '-' / '_' legitimately
|
|
83
|
+
//
|
|
84
|
+
// Key point: avoid mis-parsing backup base32 as base64.
|
|
85
|
+
if (raw.length > 50) {
|
|
86
|
+
try {
|
|
87
|
+
const b32 = base32ToBytes(raw);
|
|
88
|
+
return b32.length === 32 ? b32 : null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const b64 = base64UrlToBytes(raw);
|
|
95
|
+
if (b64 && b64.length === 32) return b64;
|
|
96
|
+
try {
|
|
97
|
+
const b32 = base32ToBytes(raw);
|
|
98
|
+
return b32.length === 32 ? b32 : null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function formatDevAuthKeyBackup(secretKeyBase64Url) {
|
|
105
|
+
const bytes = base64UrlToBytes(secretKeyBase64Url);
|
|
106
|
+
if (!bytes || bytes.length !== 32) throw new Error('invalid secret key (expected base64url 32 bytes)');
|
|
107
|
+
const base32 = bytesToBase32(bytes);
|
|
108
|
+
const groups = [];
|
|
109
|
+
for (let i = 0; i < base32.length; i += 5) groups.push(base32.slice(i, i + 5));
|
|
110
|
+
return groups.join('-');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function readDevAuthKey({ env = process.env } = {}) {
|
|
114
|
+
if ((env.HAPPY_STACKS_DEV_AUTH_SECRET_KEY ?? '').toString().trim()) {
|
|
115
|
+
const bytes = normalizeDevAuthKeyInputToBytes(env.HAPPY_STACKS_DEV_AUTH_SECRET_KEY);
|
|
116
|
+
if (!bytes) return { ok: false, error: 'invalid_env_key', source: 'env', secretKeyBase64Url: null, backup: null };
|
|
117
|
+
const base64url = bytesToBase64Url(bytes);
|
|
118
|
+
return { ok: true, source: 'env:HAPPY_STACKS_DEV_AUTH_SECRET_KEY', secretKeyBase64Url: base64url, backup: formatDevAuthKeyBackup(base64url) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const path = getDevAuthKeyPath(env);
|
|
122
|
+
try {
|
|
123
|
+
if (!existsSync(path)) return { ok: true, source: `file:${path}`, secretKeyBase64Url: null, backup: null, path };
|
|
124
|
+
const raw = await readFile(path, 'utf-8');
|
|
125
|
+
const parsed = JSON.parse(raw);
|
|
126
|
+
const input = parsed?.secretKeyBase64Url ?? parsed?.secretKey ?? parsed?.key ?? null;
|
|
127
|
+
const bytes = normalizeDevAuthKeyInputToBytes(input);
|
|
128
|
+
if (!bytes) return { ok: false, error: 'invalid_file_key', source: `file:${path}`, secretKeyBase64Url: null, backup: null, path };
|
|
129
|
+
const base64url = bytesToBase64Url(bytes);
|
|
130
|
+
return { ok: true, source: `file:${path}`, secretKeyBase64Url: base64url, backup: formatDevAuthKeyBackup(base64url), path };
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return { ok: false, error: 'failed_to_read', source: `file:${path}`, secretKeyBase64Url: null, backup: null, path, details: e instanceof Error ? e.message : String(e) };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function writeDevAuthKey({ env = process.env, input } = {}) {
|
|
137
|
+
const bytes = normalizeDevAuthKeyInputToBytes(input);
|
|
138
|
+
if (!bytes || bytes.length !== 32) {
|
|
139
|
+
throw new Error('invalid secret key (expected 32 bytes; accept base64url or backup format)');
|
|
140
|
+
}
|
|
141
|
+
const secretKeyBase64Url = bytesToBase64Url(bytes);
|
|
142
|
+
const path = getDevAuthKeyPath(env);
|
|
143
|
+
await mkdir(dirname(path), { recursive: true });
|
|
144
|
+
const payload = {
|
|
145
|
+
v: 1,
|
|
146
|
+
createdAt: new Date().toISOString(),
|
|
147
|
+
secretKeyBase64Url,
|
|
148
|
+
};
|
|
149
|
+
await writeFile(path, JSON.stringify(payload, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
150
|
+
await chmod(path, 0o600).catch(() => {});
|
|
151
|
+
return { ok: true, path, secretKeyBase64Url, backup: formatDevAuthKeyBackup(secretKeyBase64Url) };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function clearDevAuthKey({ env = process.env } = {}) {
|
|
155
|
+
const path = getDevAuthKeyPath(env);
|
|
156
|
+
try {
|
|
157
|
+
if (!existsSync(path)) return { ok: true, deleted: false, path };
|
|
158
|
+
await unlink(path);
|
|
159
|
+
return { ok: true, deleted: true, path };
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return { ok: false, deleted: false, path, error: e instanceof Error ? e.message : String(e) };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { chmod, copyFile, lstat,
|
|
1
|
+
import { chmod, copyFile, lstat, symlink, unlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
await mkdir(p, { recursive: true });
|
|
7
|
-
}
|
|
5
|
+
import { ensureDir } from '../fs/ops.mjs';
|
|
8
6
|
|
|
9
7
|
export async function removeFileOrSymlinkIfExists(path) {
|
|
10
8
|
try {
|
|
@@ -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,68 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { parseEnvToObject } from '../env/dotenv.mjs';
|
|
5
|
+
import { resolveStackEnvPath } from '../paths/paths.mjs';
|
|
6
|
+
import { getLegacyHappyBaseDir, isLegacyAuthSourceName } from './sources.mjs';
|
|
7
|
+
import { getEnvValue } from '../env/values.mjs';
|
|
8
|
+
import { readTextIfExists } from '../fs/ops.mjs';
|
|
9
|
+
import { stackExistsSync } from '../stack/stacks.mjs';
|
|
10
|
+
|
|
11
|
+
export async function resolveHandyMasterSecretFromStack({
|
|
12
|
+
stackName,
|
|
13
|
+
requireStackExists = false,
|
|
14
|
+
allowLegacyAuthSource = true,
|
|
15
|
+
allowLegacyMainFallback = true,
|
|
16
|
+
} = {}) {
|
|
17
|
+
const name = String(stackName ?? '').trim() || 'main';
|
|
18
|
+
|
|
19
|
+
if (isLegacyAuthSourceName(name)) {
|
|
20
|
+
if (!allowLegacyAuthSource) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'[auth] legacy auth source is disabled in sandbox mode.\n' +
|
|
23
|
+
'Reason: it reads from ~/.happy (global user state).\n' +
|
|
24
|
+
'If you really want this, set: HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const baseDir = getLegacyHappyBaseDir();
|
|
28
|
+
const legacySecretPath = join(baseDir, 'server-light', 'handy-master-secret.txt');
|
|
29
|
+
const secret = await readTextIfExists(legacySecretPath);
|
|
30
|
+
return secret ? { secret, source: legacySecretPath } : { secret: null, source: null };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (requireStackExists && !stackExistsSync(name)) {
|
|
34
|
+
throw new Error(`[auth] cannot copy auth: source stack "${name}" does not exist`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const resolved = resolveStackEnvPath(name);
|
|
38
|
+
const sourceBaseDir = resolved.baseDir;
|
|
39
|
+
const sourceEnvPath = resolved.envPath;
|
|
40
|
+
const raw = await readTextIfExists(sourceEnvPath);
|
|
41
|
+
const env = raw ? parseEnvToObject(raw) : {};
|
|
42
|
+
|
|
43
|
+
const inline = getEnvValue(env, 'HANDY_MASTER_SECRET');
|
|
44
|
+
if (inline) {
|
|
45
|
+
return { secret: inline, source: `${sourceEnvPath} (HANDY_MASTER_SECRET)` };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const secretFile = getEnvValue(env, 'HAPPY_STACKS_HANDY_MASTER_SECRET_FILE');
|
|
49
|
+
if (secretFile) {
|
|
50
|
+
const secret = await readTextIfExists(secretFile);
|
|
51
|
+
if (secret) return { secret, source: secretFile };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const dataDir = getEnvValue(env, 'HAPPY_SERVER_LIGHT_DATA_DIR') || join(sourceBaseDir, 'server-light');
|
|
55
|
+
const secretPath = join(dataDir, 'handy-master-secret.txt');
|
|
56
|
+
const secret = await readTextIfExists(secretPath);
|
|
57
|
+
if (secret) return { secret, source: secretPath };
|
|
58
|
+
|
|
59
|
+
// Last-resort legacy: if main has never been migrated to stack dirs.
|
|
60
|
+
if (name === 'main' && allowLegacyMainFallback) {
|
|
61
|
+
const legacy = join(homedir(), '.happy', 'server-light', 'handy-master-secret.txt');
|
|
62
|
+
const legacySecret = await readTextIfExists(legacy);
|
|
63
|
+
if (legacySecret) return { secret: legacySecret, source: legacy };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { secret: null, source: null };
|
|
67
|
+
}
|
|
68
|
+
|
|
@@ -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
|