happy-stacks 0.2.0 → 0.3.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 +59 -22
- package/bin/happys.mjs +2 -2
- package/package.json +1 -1
- package/scripts/auth.mjs +49 -202
- package/scripts/build.mjs +5 -6
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +5 -5
- package/scripts/daemon.mjs +9 -17
- package/scripts/dev.mjs +18 -27
- package/scripts/doctor.mjs +20 -36
- package/scripts/edison.mjs +102 -77
- package/scripts/happy.mjs +8 -19
- package/scripts/init.mjs +5 -13
- package/scripts/install.mjs +8 -8
- package/scripts/lint.mjs +8 -29
- package/scripts/menubar.mjs +6 -13
- package/scripts/migrate.mjs +11 -21
- package/scripts/mobile.mjs +13 -12
- package/scripts/run.mjs +15 -15
- package/scripts/self.mjs +11 -29
- package/scripts/server_flavor.mjs +4 -4
- package/scripts/service.mjs +18 -28
- package/scripts/setup.mjs +26 -122
- package/scripts/setup_pr.mjs +11 -28
- package/scripts/stack.mjs +111 -161
- package/scripts/stop.mjs +3 -3
- package/scripts/tailscale.mjs +7 -10
- package/scripts/test.mjs +8 -29
- package/scripts/tui.mjs +8 -38
- package/scripts/typecheck.mjs +8 -29
- package/scripts/ui_gateway.mjs +1 -1
- package/scripts/uninstall.mjs +6 -6
- package/scripts/utils/{dev_auth_key.mjs → auth/dev_key.mjs} +2 -8
- package/scripts/utils/{auth_files.mjs → auth/files.mjs} +2 -4
- package/scripts/utils/{handy_master_secret.mjs → auth/handy_master_secret.mjs} +6 -32
- package/scripts/utils/cli/flags.mjs +17 -0
- package/scripts/utils/cli/normalize.mjs +16 -0
- package/scripts/utils/cli/smoke_help.mjs +2 -2
- 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} +4 -4
- package/scripts/utils/{dev_expo_web.mjs → dev/expo_web.mjs} +5 -5
- package/scripts/utils/{dev_server.mjs → dev/server.mjs} +7 -7
- 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.mjs → expo/expo.mjs} +3 -9
- 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/refs.mjs +26 -0
- package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +3 -3
- package/scripts/utils/net/dns.mjs +10 -0
- package/scripts/utils/{ports.mjs → net/ports.mjs} +3 -5
- package/scripts/utils/{localhost_host.mjs → paths/localhost_host.mjs} +2 -10
- package/scripts/utils/{paths.mjs → paths/paths.mjs} +10 -7
- 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/pids.mjs +11 -0
- package/scripts/utils/{pm.mjs → proc/pm.mjs} +65 -152
- package/scripts/utils/{proc.mjs → proc/proc.mjs} +1 -0
- package/scripts/utils/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +10 -49
- package/scripts/utils/server/port.mjs +68 -0
- package/scripts/utils/{server.mjs → server/server.mjs} +12 -0
- package/scripts/utils/server/urls.mjs +91 -0
- package/scripts/utils/{validate.mjs → server/validate.mjs} +1 -1
- package/scripts/utils/service/autostart_darwin.mjs +142 -0
- package/scripts/utils/{stack_context.mjs → stack/context.mjs} +2 -2
- 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_runtime_state.mjs → stack/runtime_state.mjs} +10 -27
- package/scripts/utils/{stacks.mjs → stack/stacks.mjs} +9 -2
- package/scripts/utils/{stack_startup.mjs → stack/startup.mjs} +2 -2
- package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +9 -15
- package/scripts/utils/{browser.mjs → ui/browser.mjs} +1 -1
- package/scripts/utils/ui/text.mjs +16 -0
- package/scripts/where.mjs +6 -6
- package/scripts/worktrees.mjs +30 -58
- package/scripts/utils/server_port.mjs +0 -9
- package/scripts/utils/server_urls.mjs +0 -54
- /package/scripts/utils/{auth_login_ux.mjs → auth/login_ux.mjs} +0 -0
- /package/scripts/utils/{auth_sources.mjs → auth/sources.mjs} +0 -0
- /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/README.md
CHANGED
|
@@ -23,13 +23,6 @@ Recommended:
|
|
|
23
23
|
npx happy-stacks setup --profile=selfhost
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Alternative (global install):
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npm install -g happy-stacks
|
|
30
|
-
happys setup --profile=selfhost
|
|
31
|
-
```
|
|
32
|
-
|
|
33
26
|
`setup` can optionally start Happy and guide you through authentication.
|
|
34
27
|
|
|
35
28
|
### Step 2: Start Happy
|
|
@@ -192,7 +185,10 @@ happys wt pr happy https://github.com/slopus/happy/pull/123 --use
|
|
|
192
185
|
happys wt pr happy 123 --update --stash
|
|
193
186
|
```
|
|
194
187
|
|
|
195
|
-
|
|
188
|
+
##### Developer quickstart: create a PR stack (isolated ports/dirs; idempotent updates)
|
|
189
|
+
|
|
190
|
+
This creates (or reuses) a named stack, checks out PR worktrees for the selected components, optionally seeds auth, and starts the stack.
|
|
191
|
+
Re-run with `--reuse` to update the existing worktrees when the PR changes.
|
|
196
192
|
|
|
197
193
|
```bash
|
|
198
194
|
happys stack pr pr123 \
|
|
@@ -202,15 +198,27 @@ happys stack pr pr123 \
|
|
|
202
198
|
--dev
|
|
203
199
|
```
|
|
204
200
|
|
|
205
|
-
|
|
201
|
+
Optional: run it in a self-contained sandbox folder (delete it to uninstall completely):
|
|
206
202
|
|
|
207
203
|
```bash
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
204
|
+
SANDBOX="$(mktemp -d /tmp/happy-stacks-sandbox.XXXXXX)"
|
|
205
|
+
happys --sandbox-dir "$SANDBOX" stack pr pr123 --happy=123 --happy-cli=456 --dev
|
|
206
|
+
rm -rf "$SANDBOX"
|
|
211
207
|
```
|
|
212
208
|
|
|
213
|
-
|
|
209
|
+
Update when the PR changes:
|
|
210
|
+
|
|
211
|
+
- Re-run with `--reuse` to fast-forward worktrees when possible.
|
|
212
|
+
- If the PR was force-pushed, add `--force`.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
happys stack pr pr123 --happy=123 --happy-cli=456 --reuse
|
|
216
|
+
happys stack pr pr123 --happy=123 --happy-cli=456 --reuse --force
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
##### Maintainer quickstart: one-shot “install + run PR stack” (idempotent)
|
|
220
|
+
|
|
221
|
+
This is the maintainer-friendly entrypoint. It is safe to re-run and will keep the PR stack wiring intact.
|
|
214
222
|
|
|
215
223
|
```bash
|
|
216
224
|
npx happy-stacks setup-pr \
|
|
@@ -218,11 +226,36 @@ npx happy-stacks setup-pr \
|
|
|
218
226
|
--happy-cli=https://github.com/slopus/happy-cli/pull/456
|
|
219
227
|
```
|
|
220
228
|
|
|
221
|
-
|
|
229
|
+
Optional: run it in a self-contained sandbox folder (delete it to uninstall completely):
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
SANDBOX="$(mktemp -d /tmp/happy-stacks-sandbox.XXXXXX)"
|
|
233
|
+
npx happy-stacks --sandbox-dir "$SANDBOX" setup-pr --happy=123 --happy-cli=456
|
|
234
|
+
rm -rf "$SANDBOX"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Short form (PR numbers):
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
npx happy-stacks setup-pr --happy=123 --happy-cli=456
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Override stack name (optional):
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
npx happy-stacks setup-pr --name=pr123 --happy=123 --happy-cli=456
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Update when the PR changes:
|
|
222
250
|
|
|
223
251
|
- Re-run the same command to fast-forward the PR worktrees.
|
|
224
252
|
- If the PR was force-pushed, add `--force`.
|
|
225
253
|
|
|
254
|
+
```bash
|
|
255
|
+
npx happy-stacks setup-pr --happy=123 --happy-cli=456
|
|
256
|
+
npx happy-stacks setup-pr --happy=123 --happy-cli=456 --force
|
|
257
|
+
```
|
|
258
|
+
|
|
226
259
|
Details: `[docs/worktrees-and-forks.md](docs/worktrees-and-forks.md)`.
|
|
227
260
|
|
|
228
261
|
#### Server flavor (server-light vs full server)
|
|
@@ -352,19 +385,23 @@ Notes:
|
|
|
352
385
|
|
|
353
386
|
### Sandbox / test installs (fully isolated)
|
|
354
387
|
|
|
355
|
-
If you want to test the full setup flow (including PR stacks) without impacting your “real” install, run with
|
|
388
|
+
If you want to test the full setup flow (including PR stacks) without impacting your “real” install, run everything with `--sandbox-dir`.
|
|
389
|
+
To fully uninstall the test run, stop the sandbox stacks and delete the sandbox folder.
|
|
356
390
|
|
|
357
391
|
```bash
|
|
358
|
-
|
|
359
|
-
```
|
|
392
|
+
SANDBOX="$(mktemp -d /tmp/happy-stacks-sandbox.XXXXXX)"
|
|
360
393
|
|
|
361
|
-
|
|
394
|
+
# Run a PR stack (fully isolated install)
|
|
395
|
+
npx happy-stacks --sandbox-dir "$SANDBOX" setup-pr --happy=123 --happy-cli=456
|
|
362
396
|
|
|
363
|
-
|
|
364
|
-
|
|
397
|
+
# Tear down + uninstall
|
|
398
|
+
npx happy-stacks --sandbox-dir "$SANDBOX" stop --yes --no-service
|
|
399
|
+
rm -rf "$SANDBOX"
|
|
365
400
|
```
|
|
366
401
|
|
|
367
|
-
|
|
368
|
-
|
|
402
|
+
Notes:
|
|
403
|
+
|
|
404
|
+
- Sandbox mode disables global OS side effects (**PATH edits**, **SwiftBar plugin install**, **LaunchAgents/systemd services**, **Tailscale Serve enable/disable**) by default.
|
|
405
|
+
- To explicitly allow those for testing, set `HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1` (still recommended to clean up after).
|
|
369
406
|
|
|
370
407
|
For contributor/LLM workflow expectations: `[AGENTS.md](AGENTS.md)`.
|
package/bin/happys.mjs
CHANGED
|
@@ -8,13 +8,13 @@ import { homedir } from 'node:os';
|
|
|
8
8
|
import { dirname, join } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { commandHelpArgs, renderHappysRootHelp, resolveHappysCommand } from '../scripts/utils/cli/cli_registry.mjs';
|
|
11
|
-
import { expandHome, getCanonicalHomeEnvPathFromEnv } from '../scripts/utils/canonical_home.mjs';
|
|
11
|
+
import { expandHome, getCanonicalHomeEnvPathFromEnv } from '../scripts/utils/paths/canonical_home.mjs';
|
|
12
12
|
|
|
13
13
|
function getCliRootDir() {
|
|
14
14
|
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
// expandHome is imported from scripts/utils/canonical_home.mjs
|
|
17
|
+
// expandHome is imported from scripts/utils/paths/canonical_home.mjs
|
|
18
18
|
|
|
19
19
|
function dotenvGetQuick(envPath, key) {
|
|
20
20
|
try {
|
package/package.json
CHANGED
package/scripts/auth.mjs
CHANGED
|
@@ -1,99 +1,47 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
3
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
4
|
-
import { getComponentDir, getDefaultAutostartPaths, getRootDir, getStackName, resolveStackEnvPath } from './utils/paths.mjs';
|
|
5
|
-
import { listAllStackNames } from './utils/stacks.mjs';
|
|
4
|
+
import { getComponentDir, getDefaultAutostartPaths, getRootDir, getStackName, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
5
|
+
import { listAllStackNames } from './utils/stack/stacks.mjs';
|
|
6
6
|
import { resolvePublicServerUrl } from './tailscale.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { getInternalServerUrl, getPublicServerUrlEnvOverride, getWebappUrlEnvOverride } from './utils/server/urls.mjs';
|
|
8
|
+
import { fetchHappyHealth } from './utils/server/server.mjs';
|
|
8
9
|
import { existsSync, readFileSync } from 'node:fs';
|
|
9
10
|
import { join } from 'node:path';
|
|
10
11
|
import { homedir } from 'node:os';
|
|
11
12
|
import { spawn } from 'node:child_process';
|
|
12
|
-
import { mkdir,
|
|
13
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
13
14
|
import { dirname } from 'node:path';
|
|
14
15
|
|
|
15
|
-
import {
|
|
16
|
-
import { ensureDepsInstalled, pmExecBin } from './utils/pm.mjs';
|
|
17
|
-
import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/happy_server_infra.mjs';
|
|
18
|
-
import { clearDevAuthKey, readDevAuthKey, writeDevAuthKey } from './utils/
|
|
19
|
-
import { getExpoStatePaths, isStateProcessRunning } from './utils/expo.mjs';
|
|
20
|
-
import { resolveAuthSeedFromEnv } from './utils/
|
|
21
|
-
import { printAuthLoginInstructions } from './utils/
|
|
22
|
-
import { copyFileIfMissing, linkFileIfMissing, removeFileOrSymlinkIfExists, writeSecretFileIfMissing } from './utils/
|
|
23
|
-
import { getLegacyHappyBaseDir, isLegacyAuthSourceName } from './utils/
|
|
24
|
-
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
25
|
-
import { resolveHandyMasterSecretFromStack } from './utils/handy_master_secret.mjs';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// For non-main stacks, do NOT inherit a global/server URL override from ~/.happy-stacks/env.local
|
|
41
|
-
// (which often points at main). Only use a public URL override if it is explicitly present in the
|
|
42
|
-
// stack env file itself.
|
|
43
|
-
const envPath =
|
|
44
|
-
(process.env.HAPPY_STACKS_ENV_FILE ?? '').trim() ||
|
|
45
|
-
(process.env.HAPPY_LOCAL_ENV_FILE ?? '').trim() ||
|
|
46
|
-
getStackEnvPath(stackName);
|
|
47
|
-
try {
|
|
48
|
-
if (!envPath || !existsSync(envPath)) return '';
|
|
49
|
-
const raw = readFileSync(envPath, 'utf-8');
|
|
50
|
-
const env = raw ? parseEnvToObject(raw) : {};
|
|
51
|
-
return (env.HAPPY_LOCAL_SERVER_URL ?? env.HAPPY_STACKS_SERVER_URL ?? '').toString().trim();
|
|
52
|
-
} catch {
|
|
53
|
-
return '';
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function expandTilde(p) {
|
|
58
|
-
return p.replace(/^~(?=\/)/, homedir());
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function resolveEnvWebappUrlForStack({ stackName }) {
|
|
62
|
-
const candidate = (process.env.HAPPY_WEBAPP_URL ?? '').trim();
|
|
63
|
-
|
|
64
|
-
// For main, allow the user's global override.
|
|
65
|
-
if (stackName === 'main') {
|
|
66
|
-
return candidate;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// For non-main stacks, only respect HAPPY_WEBAPP_URL if it is explicitly present in the stack env file.
|
|
70
|
-
const envPath =
|
|
71
|
-
(process.env.HAPPY_STACKS_ENV_FILE ?? '').trim() ||
|
|
72
|
-
(process.env.HAPPY_LOCAL_ENV_FILE ?? '').trim() ||
|
|
73
|
-
getStackEnvPath(stackName);
|
|
74
|
-
try {
|
|
75
|
-
if (!envPath || !existsSync(envPath)) return '';
|
|
76
|
-
const raw = readFileSync(envPath, 'utf-8');
|
|
77
|
-
const env = raw ? parseEnvToObject(raw) : {};
|
|
78
|
-
return (env.HAPPY_WEBAPP_URL ?? '').toString().trim();
|
|
79
|
-
} catch {
|
|
80
|
-
return '';
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function sanitizeDnsLabel(raw, { fallback = 'stack' } = {}) {
|
|
85
|
-
const s = String(raw ?? '')
|
|
86
|
-
.toLowerCase()
|
|
87
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
88
|
-
.replace(/-+/g, '-')
|
|
89
|
-
.replace(/^-+/, '')
|
|
90
|
-
.replace(/-+$/, '');
|
|
91
|
-
return s || fallback;
|
|
16
|
+
import { parseEnvToObject } from './utils/env/dotenv.mjs';
|
|
17
|
+
import { ensureDepsInstalled, pmExecBin } from './utils/proc/pm.mjs';
|
|
18
|
+
import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/server/infra/happy_server_infra.mjs';
|
|
19
|
+
import { clearDevAuthKey, readDevAuthKey, writeDevAuthKey } from './utils/auth/dev_key.mjs';
|
|
20
|
+
import { getExpoStatePaths, isStateProcessRunning } from './utils/expo/expo.mjs';
|
|
21
|
+
import { resolveAuthSeedFromEnv } from './utils/stack/startup.mjs';
|
|
22
|
+
import { printAuthLoginInstructions } from './utils/auth/login_ux.mjs';
|
|
23
|
+
import { copyFileIfMissing, linkFileIfMissing, removeFileOrSymlinkIfExists, writeSecretFileIfMissing } from './utils/auth/files.mjs';
|
|
24
|
+
import { getLegacyHappyBaseDir, isLegacyAuthSourceName } from './utils/auth/sources.mjs';
|
|
25
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
26
|
+
import { resolveHandyMasterSecretFromStack } from './utils/auth/handy_master_secret.mjs';
|
|
27
|
+
import { ensureDir, readTextIfExists } from './utils/fs/ops.mjs';
|
|
28
|
+
import { stackExistsSync } from './utils/stack/stacks.mjs';
|
|
29
|
+
import { checkDaemonState } from './daemon.mjs';
|
|
30
|
+
import {
|
|
31
|
+
getCliHomeDirFromEnvOrDefault,
|
|
32
|
+
getServerLightDataDirFromEnvOrDefault,
|
|
33
|
+
resolveCliHomeDir,
|
|
34
|
+
} from './utils/stack/dirs.mjs';
|
|
35
|
+
import { resolveLocalhostHost } from './utils/paths/localhost_host.mjs';
|
|
36
|
+
|
|
37
|
+
function getInternalServerUrlCompat() {
|
|
38
|
+
const { port, internalServerUrl } = getInternalServerUrl({ env: process.env, defaultPort: 3005 });
|
|
39
|
+
return { port, url: internalServerUrl };
|
|
92
40
|
}
|
|
93
41
|
|
|
94
42
|
async function resolveWebappUrlFromRunningExpo({ rootDir, stackName }) {
|
|
95
43
|
try {
|
|
96
|
-
const baseDir =
|
|
44
|
+
const baseDir = resolveStackEnvPath(stackName).baseDir;
|
|
97
45
|
const uiDir = getComponentDir(rootDir, 'happy');
|
|
98
46
|
const uiPaths = getExpoStatePaths({
|
|
99
47
|
baseDir,
|
|
@@ -105,66 +53,16 @@ async function resolveWebappUrlFromRunningExpo({ rootDir, stackName }) {
|
|
|
105
53
|
if (!uiRunning.running) return null;
|
|
106
54
|
const port = Number(uiRunning.state?.port);
|
|
107
55
|
if (!Number.isFinite(port) || port <= 0) return null;
|
|
108
|
-
const host =
|
|
56
|
+
const host = resolveLocalhostHost({ stackMode: stackName !== 'main', stackName });
|
|
109
57
|
return `http://${host}:${port}`;
|
|
110
58
|
} catch {
|
|
111
59
|
return null;
|
|
112
60
|
}
|
|
113
61
|
}
|
|
114
62
|
|
|
115
|
-
|
|
116
|
-
await mkdir(p, { recursive: true });
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function readTextIfExists(path) {
|
|
120
|
-
try {
|
|
121
|
-
if (!existsSync(path)) return null;
|
|
122
|
-
const raw = await readFile(path, 'utf-8');
|
|
123
|
-
const t = raw.trim();
|
|
124
|
-
return t ? t : null;
|
|
125
|
-
} catch {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
63
|
+
// NOTE: common fs helpers live in scripts/utils/fs/ops.mjs
|
|
129
64
|
|
|
130
|
-
// (auth file copy/link helpers live in scripts/utils/
|
|
131
|
-
|
|
132
|
-
function parseEnvToObject(raw) {
|
|
133
|
-
const parsed = parseDotenv(raw);
|
|
134
|
-
return Object.fromEntries(parsed.entries());
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function getStackDir(stackName) {
|
|
138
|
-
return resolveStackEnvPath(stackName).baseDir;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function getStackEnvPath(stackName) {
|
|
142
|
-
return resolveStackEnvPath(stackName).envPath;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function stackExistsSync(stackName) {
|
|
146
|
-
if (stackName === 'main') return true;
|
|
147
|
-
const envPath = getStackEnvPath(stackName);
|
|
148
|
-
return existsSync(envPath);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function getCliHomeDirFromEnvOrDefault({ stackBaseDir, env }) {
|
|
152
|
-
const fromEnv = (env.HAPPY_STACKS_CLI_HOME_DIR ?? env.HAPPY_LOCAL_CLI_HOME_DIR ?? '').trim();
|
|
153
|
-
return fromEnv || join(stackBaseDir, 'cli');
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function getServerLightDataDirFromEnvOrDefault({ stackBaseDir, env }) {
|
|
157
|
-
const fromEnv = (env.HAPPY_SERVER_LIGHT_DATA_DIR ?? '').trim();
|
|
158
|
-
return fromEnv || join(stackBaseDir, 'server-light');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function resolveCliHomeDir() {
|
|
162
|
-
const fromEnv = (process.env.HAPPY_LOCAL_CLI_HOME_DIR ?? process.env.HAPPY_STACKS_CLI_HOME_DIR ?? '').trim();
|
|
163
|
-
if (fromEnv) {
|
|
164
|
-
return expandTilde(fromEnv);
|
|
165
|
-
}
|
|
166
|
-
return join(getDefaultAutostartPaths().baseDir, 'cli');
|
|
167
|
-
}
|
|
65
|
+
// (auth file copy/link helpers live in scripts/utils/auth/files.mjs)
|
|
168
66
|
|
|
169
67
|
function fileHasContent(path) {
|
|
170
68
|
try {
|
|
@@ -175,60 +73,6 @@ function fileHasContent(path) {
|
|
|
175
73
|
}
|
|
176
74
|
}
|
|
177
75
|
|
|
178
|
-
function checkDaemonState(cliHomeDir) {
|
|
179
|
-
const statePath = join(cliHomeDir, 'daemon.state.json');
|
|
180
|
-
const lockPath = join(cliHomeDir, 'daemon.state.json.lock');
|
|
181
|
-
|
|
182
|
-
const alive = (pid) => {
|
|
183
|
-
try {
|
|
184
|
-
process.kill(pid, 0);
|
|
185
|
-
return true;
|
|
186
|
-
} catch {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
if (existsSync(statePath)) {
|
|
192
|
-
try {
|
|
193
|
-
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
194
|
-
const pid = Number(state?.pid);
|
|
195
|
-
if (Number.isFinite(pid) && pid > 0) {
|
|
196
|
-
return alive(pid) ? { status: 'running', pid } : { status: 'stale_state', pid };
|
|
197
|
-
}
|
|
198
|
-
return { status: 'bad_state' };
|
|
199
|
-
} catch {
|
|
200
|
-
return { status: 'bad_state' };
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (existsSync(lockPath)) {
|
|
205
|
-
try {
|
|
206
|
-
const pid = Number(readFileSync(lockPath, 'utf-8').trim());
|
|
207
|
-
if (Number.isFinite(pid) && pid > 0) {
|
|
208
|
-
return alive(pid) ? { status: 'starting', pid } : { status: 'stale_lock', pid };
|
|
209
|
-
}
|
|
210
|
-
} catch {
|
|
211
|
-
// ignore
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return { status: 'stopped' };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function fetchHealth(internalServerUrl) {
|
|
219
|
-
const ctl = new AbortController();
|
|
220
|
-
const t = setTimeout(() => ctl.abort(), 1500);
|
|
221
|
-
try {
|
|
222
|
-
const res = await fetch(`${internalServerUrl}/health`, { method: 'GET', signal: ctl.signal });
|
|
223
|
-
const body = (await res.text()).trim();
|
|
224
|
-
return { ok: res.ok, status: res.status, body };
|
|
225
|
-
} catch {
|
|
226
|
-
return { ok: false, status: null, body: null };
|
|
227
|
-
} finally {
|
|
228
|
-
clearTimeout(t);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
76
|
function authLoginSuggestion(stackName) {
|
|
233
77
|
return stackName === 'main' ? 'happys auth login' : `happys stack auth ${stackName} login`;
|
|
234
78
|
}
|
|
@@ -678,8 +522,8 @@ async function cmdCopyFrom({ argv, json }) {
|
|
|
678
522
|
}
|
|
679
523
|
}
|
|
680
524
|
|
|
681
|
-
const sourceBaseDir = isLegacySource ? getLegacyHappyBaseDir() :
|
|
682
|
-
const sourceEnvRaw = isLegacySource ? '' : await readTextIfExists(
|
|
525
|
+
const sourceBaseDir = isLegacySource ? getLegacyHappyBaseDir() : resolveStackEnvPath(fromStackName).baseDir;
|
|
526
|
+
const sourceEnvRaw = isLegacySource ? '' : await readTextIfExists(resolveStackEnvPath(fromStackName).envPath);
|
|
683
527
|
const sourceEnv = sourceEnvRaw ? parseEnvToObject(sourceEnvRaw) : {};
|
|
684
528
|
const sourceCli = isLegacySource
|
|
685
529
|
? join(sourceBaseDir, 'cli')
|
|
@@ -735,9 +579,9 @@ async function cmdCopyFrom({ argv, json }) {
|
|
|
735
579
|
// so we can seed DB accounts reliably.
|
|
736
580
|
const managed = (targetEnv.HAPPY_STACKS_MANAGED_INFRA ?? targetEnv.HAPPY_LOCAL_MANAGED_INFRA ?? '1').toString().trim() !== '0';
|
|
737
581
|
if (targetServerComponent === 'happy-server' && withInfra && managed) {
|
|
738
|
-
const { port } =
|
|
582
|
+
const { port } = getInternalServerUrlCompat();
|
|
739
583
|
const publicServerUrl = `http://localhost:${port}`;
|
|
740
|
-
const envPath =
|
|
584
|
+
const envPath = resolveStackEnvPath(stackName).envPath;
|
|
741
585
|
const infra = await ensureHappyServerManagedInfra({
|
|
742
586
|
stackName,
|
|
743
587
|
baseDir: targetBaseDir,
|
|
@@ -838,9 +682,8 @@ async function cmdStatus({ json }) {
|
|
|
838
682
|
const rootDir = getRootDir(import.meta.url);
|
|
839
683
|
const stackName = getStackName();
|
|
840
684
|
|
|
841
|
-
const { port, url: internalServerUrl } =
|
|
842
|
-
const defaultPublicUrl =
|
|
843
|
-
const envPublicUrl = resolveEnvPublicUrlForStack({ stackName });
|
|
685
|
+
const { port, url: internalServerUrl } = getInternalServerUrlCompat();
|
|
686
|
+
const { defaultPublicUrl, envPublicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port, stackName });
|
|
844
687
|
const { publicServerUrl } = await resolvePublicServerUrl({
|
|
845
688
|
internalServerUrl,
|
|
846
689
|
defaultPublicUrl,
|
|
@@ -861,7 +704,12 @@ async function cmdStatus({ json }) {
|
|
|
861
704
|
};
|
|
862
705
|
|
|
863
706
|
const daemon = checkDaemonState(cliHomeDir);
|
|
864
|
-
const
|
|
707
|
+
const healthRaw = await fetchHappyHealth(internalServerUrl);
|
|
708
|
+
const health = {
|
|
709
|
+
ok: Boolean(healthRaw.ok),
|
|
710
|
+
status: healthRaw.status,
|
|
711
|
+
body: healthRaw.text ? healthRaw.text.trim() : null,
|
|
712
|
+
};
|
|
865
713
|
|
|
866
714
|
const out = {
|
|
867
715
|
stackName,
|
|
@@ -924,16 +772,15 @@ async function cmdLogin({ argv, json }) {
|
|
|
924
772
|
const stackName = getStackName();
|
|
925
773
|
const { kv } = parseArgs(argv);
|
|
926
774
|
|
|
927
|
-
const { port, url: internalServerUrl } =
|
|
928
|
-
const defaultPublicUrl =
|
|
929
|
-
const envPublicUrl = resolveEnvPublicUrlForStack({ stackName });
|
|
775
|
+
const { port, url: internalServerUrl } = getInternalServerUrlCompat();
|
|
776
|
+
const { defaultPublicUrl, envPublicUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort: port, stackName });
|
|
930
777
|
const { publicServerUrl } = await resolvePublicServerUrl({
|
|
931
778
|
internalServerUrl,
|
|
932
779
|
defaultPublicUrl,
|
|
933
780
|
envPublicUrl,
|
|
934
781
|
allowEnable: false,
|
|
935
782
|
});
|
|
936
|
-
const envWebappUrl =
|
|
783
|
+
const { envWebappUrl } = getWebappUrlEnvOverride({ env: process.env, stackName });
|
|
937
784
|
const expoWebappUrl = await resolveWebappUrlFromRunningExpo({ rootDir, stackName });
|
|
938
785
|
const webappUrl = envWebappUrl || expoWebappUrl || publicServerUrl;
|
|
939
786
|
const webappUrlSource = expoWebappUrl ? 'expo' : envWebappUrl ? 'stack env override' : 'server';
|
package/scripts/build.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
-
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
|
|
4
|
-
import { ensureDepsInstalled, pmExecBin, requireDir } from './utils/pm.mjs';
|
|
3
|
+
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
|
|
4
|
+
import { ensureDepsInstalled, pmExecBin, requireDir } from './utils/proc/pm.mjs';
|
|
5
|
+
import { resolveServerPortFromEnv } from './utils/server/urls.mjs';
|
|
5
6
|
import { dirname, join } from 'node:path';
|
|
6
7
|
import { readFile, rm, mkdir, writeFile } from 'node:fs/promises';
|
|
7
8
|
import { tailscaleServeHttpsUrl } from './tailscale.mjs';
|
|
@@ -43,9 +44,7 @@ async function main() {
|
|
|
43
44
|
const uiDir = getComponentDir(rootDir, 'happy');
|
|
44
45
|
await requireDir('happy', uiDir);
|
|
45
46
|
|
|
46
|
-
const serverPort = process.env
|
|
47
|
-
? parseInt(process.env.HAPPY_LOCAL_SERVER_PORT, 10)
|
|
48
|
-
: 3005;
|
|
47
|
+
const serverPort = resolveServerPortFromEnv({ env: process.env, defaultPort: 3005 });
|
|
49
48
|
|
|
50
49
|
// For Tauri builds we embed an explicit API base URL (tauri:// origins cannot use window.location.origin).
|
|
51
50
|
const internalServerUrl = `http://127.0.0.1:${serverPort}`;
|
package/scripts/cli-link.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
-
import { getComponentDir, getRootDir } from './utils/paths.mjs';
|
|
4
|
-
import { ensureCliBuilt, ensureHappyCliLocalNpmLinked } from './utils/pm.mjs';
|
|
3
|
+
import { getComponentDir, getRootDir } from './utils/paths/paths.mjs';
|
|
4
|
+
import { ensureCliBuilt, ensureHappyCliLocalNpmLinked } from './utils/proc/pm.mjs';
|
|
5
5
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
6
6
|
|
|
7
7
|
/**
|
package/scripts/completion.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
@@ -7,11 +7,11 @@ import { join } from 'node:path';
|
|
|
7
7
|
|
|
8
8
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
9
9
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
10
|
-
import { runCapture } from './utils/proc.mjs';
|
|
10
|
+
import { runCapture } from './utils/proc/proc.mjs';
|
|
11
11
|
import { getHappysRegistry } from './utils/cli/cli_registry.mjs';
|
|
12
|
-
import { expandHome } from './utils/canonical_home.mjs';
|
|
13
|
-
import { getHappyStacksHomeDir, getRootDir } from './utils/paths.mjs';
|
|
14
|
-
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
12
|
+
import { expandHome } from './utils/paths/canonical_home.mjs';
|
|
13
|
+
import { getHappyStacksHomeDir, getRootDir } from './utils/paths/paths.mjs';
|
|
14
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
15
15
|
|
|
16
16
|
function detectShell() {
|
|
17
17
|
const raw = (process.env.SHELL ?? '').toLowerCase();
|
package/scripts/daemon.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { spawnProc, run, runCapture } from './utils/proc.mjs';
|
|
2
|
-
import { resolveAuthSeedFromEnv } from './utils/
|
|
3
|
-
import { getStacksStorageRoot } from './utils/paths.mjs';
|
|
4
|
-
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
1
|
+
import { spawnProc, run, runCapture } from './utils/proc/proc.mjs';
|
|
2
|
+
import { resolveAuthSeedFromEnv } from './utils/stack/startup.mjs';
|
|
3
|
+
import { getStacksStorageRoot } from './utils/paths/paths.mjs';
|
|
4
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
5
|
+
import { runCaptureIfCommandExists } from './utils/proc/commands.mjs';
|
|
6
|
+
import { readLastLines } from './utils/fs/tail.mjs';
|
|
5
7
|
import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
|
|
6
8
|
import { chmod, copyFile, mkdir } from 'node:fs/promises';
|
|
7
9
|
import { join } from 'node:path';
|
|
@@ -28,7 +30,7 @@ export async function cleanupStaleDaemonState(homeDir) {
|
|
|
28
30
|
|
|
29
31
|
const lsofHasPath = async (pid, pathNeedle) => {
|
|
30
32
|
try {
|
|
31
|
-
const out = await
|
|
33
|
+
const out = await runCaptureIfCommandExists('lsof', ['-nP', '-p', String(pid)]);
|
|
32
34
|
return out.includes(pathNeedle);
|
|
33
35
|
} catch {
|
|
34
36
|
return false;
|
|
@@ -173,16 +175,6 @@ function getLatestDaemonLogPath(homeDir) {
|
|
|
173
175
|
}
|
|
174
176
|
}
|
|
175
177
|
|
|
176
|
-
function readLastLines(path, lines = 60) {
|
|
177
|
-
try {
|
|
178
|
-
const content = readFileSync(path, 'utf-8');
|
|
179
|
-
const parts = content.split('\n');
|
|
180
|
-
return parts.slice(Math.max(0, parts.length - lines)).join('\n');
|
|
181
|
-
} catch {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
178
|
function excerptIndicatesMissingAuth(excerpt) {
|
|
187
179
|
if (!excerpt) return false;
|
|
188
180
|
return (
|
|
@@ -297,7 +289,7 @@ async function killDaemonFromLockFile({ cliHomeDir }) {
|
|
|
297
289
|
// We do this by checking that `lsof -p <pid>` includes the lock path (or state file path).
|
|
298
290
|
let ownsLock = false;
|
|
299
291
|
try {
|
|
300
|
-
const out = await
|
|
292
|
+
const out = await runCaptureIfCommandExists('lsof', ['-nP', '-p', String(pid)]);
|
|
301
293
|
ownsLock = out.includes(lockPath) || out.includes(join(cliHomeDir, 'daemon.state.json')) || out.includes(join(cliHomeDir, 'logs'));
|
|
302
294
|
} catch {
|
|
303
295
|
ownsLock = false;
|
|
@@ -463,7 +455,7 @@ export async function startLocalDaemonWithAuth({
|
|
|
463
455
|
const logPath =
|
|
464
456
|
getLatestDaemonLogPath(cliHomeDir) ||
|
|
465
457
|
((!isSandboxed() || sandboxAllowsGlobalSideEffects()) ? getLatestDaemonLogPath(join(homedir(), '.happy')) : null);
|
|
466
|
-
const excerpt = logPath ? readLastLines(logPath, 120) : null;
|
|
458
|
+
const excerpt = logPath ? await readLastLines(logPath, 120) : null;
|
|
467
459
|
return { ok: false, exitCode, excerpt, logPath };
|
|
468
460
|
};
|
|
469
461
|
|
package/scripts/dev.mjs
CHANGED
|
@@ -1,36 +1,27 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
-
import { killProcessTree } from './utils/proc.mjs';
|
|
4
|
-
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
|
|
5
|
-
import { killPortListeners } from './utils/ports.mjs';
|
|
6
|
-
import { getServerComponentName, isHappyServerRunning } from './utils/server.mjs';
|
|
7
|
-
import { requireDir } from './utils/pm.mjs';
|
|
3
|
+
import { killProcessTree } from './utils/proc/proc.mjs';
|
|
4
|
+
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths/paths.mjs';
|
|
5
|
+
import { killPortListeners } from './utils/net/ports.mjs';
|
|
6
|
+
import { getServerComponentName, isHappyServerRunning } from './utils/server/server.mjs';
|
|
7
|
+
import { requireDir } from './utils/proc/pm.mjs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { isDaemonRunning, stopLocalDaemon } from './daemon.mjs';
|
|
12
12
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
13
|
-
import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/validate.mjs';
|
|
14
|
-
import { getExpoStatePaths, isStateProcessRunning } from './utils/expo.mjs';
|
|
15
|
-
import { isPidAlive, readStackRuntimeStateFile, recordStackRuntimeStart } from './utils/
|
|
16
|
-
import { resolveStackContext } from './utils/
|
|
17
|
-
import { resolveServerPortFromEnv, resolveServerUrls } from './utils/
|
|
18
|
-
import { ensureDevCliReady, prepareDaemonAuthSeed, startDevDaemon, watchHappyCliAndRestartDaemon } from './utils/
|
|
19
|
-
import { startDevServer, watchDevServerAndRestart } from './utils/
|
|
20
|
-
import { startDevExpoWebUi } from './utils/
|
|
21
|
-
import { resolveLocalhostHost } from './utils/localhost_host.mjs';
|
|
22
|
-
import { openUrlInBrowser } from './utils/browser.mjs';
|
|
23
|
-
import { waitForHttpOk } from './utils/server.mjs';
|
|
24
|
-
|
|
25
|
-
function sanitizeDnsLabel(raw, { fallback = 'stack' } = {}) {
|
|
26
|
-
const s = String(raw ?? '')
|
|
27
|
-
.toLowerCase()
|
|
28
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
29
|
-
.replace(/-+/g, '-')
|
|
30
|
-
.replace(/^-+/, '')
|
|
31
|
-
.replace(/-+$/, '');
|
|
32
|
-
return s || fallback;
|
|
33
|
-
}
|
|
13
|
+
import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/server/validate.mjs';
|
|
14
|
+
import { getExpoStatePaths, isStateProcessRunning } from './utils/expo/expo.mjs';
|
|
15
|
+
import { isPidAlive, readStackRuntimeStateFile, recordStackRuntimeStart } from './utils/stack/runtime_state.mjs';
|
|
16
|
+
import { resolveStackContext } from './utils/stack/context.mjs';
|
|
17
|
+
import { resolveServerPortFromEnv, resolveServerUrls } from './utils/server/urls.mjs';
|
|
18
|
+
import { ensureDevCliReady, prepareDaemonAuthSeed, startDevDaemon, watchHappyCliAndRestartDaemon } from './utils/dev/daemon.mjs';
|
|
19
|
+
import { startDevServer, watchDevServerAndRestart } from './utils/dev/server.mjs';
|
|
20
|
+
import { startDevExpoWebUi } from './utils/dev/expo_web.mjs';
|
|
21
|
+
import { resolveLocalhostHost } from './utils/paths/localhost_host.mjs';
|
|
22
|
+
import { openUrlInBrowser } from './utils/ui/browser.mjs';
|
|
23
|
+
import { waitForHttpOk } from './utils/server/server.mjs';
|
|
24
|
+
import { sanitizeDnsLabel } from './utils/net/dns.mjs';
|
|
34
25
|
|
|
35
26
|
/**
|
|
36
27
|
* Dev mode stack:
|