happy-stacks 0.0.0 → 0.1.2
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 +22 -4
- package/bin/happys.mjs +76 -5
- package/docs/server-flavors.md +61 -2
- package/docs/stacks.md +16 -4
- package/extras/swiftbar/auth-login.sh +5 -5
- package/extras/swiftbar/happy-stacks.5s.sh +83 -41
- package/extras/swiftbar/happys-term.sh +151 -0
- package/extras/swiftbar/happys.sh +52 -0
- package/extras/swiftbar/lib/render.sh +74 -56
- package/extras/swiftbar/lib/system.sh +37 -6
- package/extras/swiftbar/lib/utils.sh +180 -4
- package/extras/swiftbar/pnpm-term.sh +2 -122
- package/extras/swiftbar/pnpm.sh +2 -13
- package/extras/swiftbar/set-server-flavor.sh +8 -8
- package/extras/swiftbar/wt-pr.sh +1 -1
- package/package.json +1 -1
- package/scripts/auth.mjs +374 -3
- package/scripts/daemon.mjs +78 -11
- package/scripts/dev.mjs +122 -17
- package/scripts/init.mjs +238 -32
- package/scripts/migrate.mjs +292 -0
- package/scripts/mobile.mjs +51 -19
- package/scripts/run.mjs +118 -26
- package/scripts/service.mjs +176 -37
- package/scripts/stack.mjs +665 -22
- package/scripts/stop.mjs +157 -0
- package/scripts/tailscale.mjs +147 -21
- package/scripts/typecheck.mjs +145 -0
- package/scripts/ui_gateway.mjs +248 -0
- package/scripts/uninstall.mjs +3 -3
- package/scripts/utils/cli_registry.mjs +23 -0
- package/scripts/utils/config.mjs +9 -1
- package/scripts/utils/env.mjs +37 -15
- package/scripts/utils/expo.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +430 -0
- package/scripts/utils/pm.mjs +11 -2
- package/scripts/utils/ports.mjs +51 -13
- package/scripts/utils/proc.mjs +46 -5
- package/scripts/utils/server.mjs +37 -0
- package/scripts/utils/stack_stop.mjs +206 -0
- package/scripts/utils/validate.mjs +42 -1
- package/scripts/worktrees.mjs +53 -7
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Happy Stacks
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
|
|
3
5
|
Run the **Happy** stack locally (or many stacks in parallel) and access it remotely and securely (using Tailscale).
|
|
4
6
|
|
|
5
7
|
`happy-stacks` is a CLI (`happys`) that orchestrate the real upstream repos
|
|
@@ -54,6 +56,17 @@ node ./bin/happys.mjs bootstrap --interactive
|
|
|
54
56
|
Notes:
|
|
55
57
|
|
|
56
58
|
- In a cloned repo, `pnpm <script>` still works, but `happys <command>` is now the recommended UX (same underlying scripts).
|
|
59
|
+
- To make the installed `~/.happy-stacks/bin/happys` shim (LaunchAgents / SwiftBar) run your local checkout without publishing to npm, set:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
echo 'HAPPY_STACKS_CLI_ROOT_DIR=/path/to/your/happy-stacks-checkout' >> ~/.happy-stacks/.env
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or (recommended) persist it via init:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
happys init --cli-root-dir=/path/to/your/happy-stacks-checkout
|
|
69
|
+
```
|
|
57
70
|
|
|
58
71
|
### Step 2: Run the main stack
|
|
59
72
|
|
|
@@ -143,9 +156,10 @@ Diagram:
|
|
|
143
156
|
v
|
|
144
157
|
local machine (this repo)
|
|
145
158
|
+--------------------------------+
|
|
146
|
-
| happy-server
|
|
159
|
+
| happy-server-light OR |
|
|
160
|
+
| happy-server (via UI gateway) |
|
|
147
161
|
| - listens on :PORT |
|
|
148
|
-
| - serves UI
|
|
162
|
+
| - serves UI at / |
|
|
149
163
|
+--------------------------------+
|
|
150
164
|
^
|
|
151
165
|
| internal loopback
|
|
@@ -201,7 +215,8 @@ Details: `[docs/worktrees-and-forks.md](docs/worktrees-and-forks.md)`.
|
|
|
201
215
|
### Server flavor (server-light vs full server)
|
|
202
216
|
|
|
203
217
|
- Use `happy-server-light` for a light local stack (no Redis, no Postgres, no Docker), and UI serving via server-light.
|
|
204
|
-
- Use `happy-server` when you need
|
|
218
|
+
- Use `happy-server` when you need a more production-like server (Postgres + Redis + S3-compatible storage) or want to develop server changes for upstream.
|
|
219
|
+
- Happy Stacks can **manage the required infra automatically per stack** (via Docker Compose) and runs a **UI gateway** so you still get a single `https://...ts.net` URL that serves the UI + proxies API/websockets/files.
|
|
205
220
|
|
|
206
221
|
Switch globally:
|
|
207
222
|
|
|
@@ -310,5 +325,8 @@ Notes:
|
|
|
310
325
|
|
|
311
326
|
- Canonical env prefix is `HAPPY_STACKS_*` (legacy `HAPPY_LOCAL_*` still works).
|
|
312
327
|
- Canonical stack storage is `~/.happy/stacks` (legacy `~/.happy/local` is still supported).
|
|
328
|
+
- **Repo env templates**:
|
|
329
|
+
- **Use `.env.example` as the canonical template** (copy it to `.env` if you’re running this repo directly).
|
|
330
|
+
- If an LLM tool refuses to read/edit `.env.example` due to safety restrictions, **do not create an `env.example` workaround**—instead, ask the user to apply the change manually.
|
|
313
331
|
|
|
314
|
-
For contributor/LLM workflow expectations: `[AGENTS.md](AGENTS.md)`.
|
|
332
|
+
For contributor/LLM workflow expectations: `[AGENTS.md](AGENTS.md)`.
|
package/bin/happys.mjs
CHANGED
|
@@ -13,12 +13,82 @@ function getCliRootDir() {
|
|
|
13
13
|
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
function expandHome(p) {
|
|
17
|
+
return String(p ?? '').replace(/^~(?=\/)/, homedir());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dotenvGetQuick(envPath, key) {
|
|
21
|
+
try {
|
|
22
|
+
if (!envPath || !existsSync(envPath)) return '';
|
|
23
|
+
const lines = readFileSync(envPath, 'utf-8').split('\n');
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
27
|
+
if (!trimmed.startsWith(`${key}=`)) continue;
|
|
28
|
+
let v = trimmed.slice(`${key}=`.length).trim();
|
|
29
|
+
if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1);
|
|
30
|
+
if (v.startsWith("'") && v.endsWith("'")) v = v.slice(1, -1);
|
|
31
|
+
return v;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// ignore
|
|
20
35
|
}
|
|
21
|
-
return
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveCliRootDir() {
|
|
40
|
+
const fromEnv = (
|
|
41
|
+
process.env.HAPPY_STACKS_CLI_ROOT_DIR ??
|
|
42
|
+
process.env.HAPPY_LOCAL_CLI_ROOT_DIR ??
|
|
43
|
+
process.env.HAPPY_STACKS_DEV_CLI_ROOT_DIR ??
|
|
44
|
+
process.env.HAPPY_LOCAL_DEV_CLI_ROOT_DIR ??
|
|
45
|
+
''
|
|
46
|
+
).trim();
|
|
47
|
+
if (fromEnv) return expandHome(fromEnv);
|
|
48
|
+
|
|
49
|
+
// Stable pointer file: even if the real home dir is elsewhere, `happys init` writes the pointer here.
|
|
50
|
+
const canonicalEnv = join(homedir(), '.happy-stacks', '.env');
|
|
51
|
+
const v =
|
|
52
|
+
dotenvGetQuick(canonicalEnv, 'HAPPY_STACKS_CLI_ROOT_DIR') ||
|
|
53
|
+
dotenvGetQuick(canonicalEnv, 'HAPPY_LOCAL_CLI_ROOT_DIR') ||
|
|
54
|
+
dotenvGetQuick(canonicalEnv, 'HAPPY_STACKS_DEV_CLI_ROOT_DIR') ||
|
|
55
|
+
dotenvGetQuick(canonicalEnv, 'HAPPY_LOCAL_DEV_CLI_ROOT_DIR') ||
|
|
56
|
+
'';
|
|
57
|
+
return v ? expandHome(v) : '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function maybeReexecToCliRoot(cliRootDir) {
|
|
61
|
+
if ((process.env.HAPPY_STACKS_CLI_REEXEC ?? process.env.HAPPY_STACKS_DEV_REEXEC ?? '') === '1') return;
|
|
62
|
+
if ((process.env.HAPPY_STACKS_CLI_ROOT_DISABLE ?? process.env.HAPPY_STACKS_DEV_CLI_DISABLE ?? '') === '1') return;
|
|
63
|
+
|
|
64
|
+
const cliRoot = resolveCliRootDir();
|
|
65
|
+
if (!cliRoot) return;
|
|
66
|
+
if (cliRoot === cliRootDir) return;
|
|
67
|
+
|
|
68
|
+
const cliBin = join(cliRoot, 'bin', 'happys.mjs');
|
|
69
|
+
if (!existsSync(cliBin)) return;
|
|
70
|
+
|
|
71
|
+
const argv = process.argv.slice(2);
|
|
72
|
+
const res = spawnSync(process.execPath, [cliBin, ...argv], {
|
|
73
|
+
stdio: 'inherit',
|
|
74
|
+
cwd: cliRoot,
|
|
75
|
+
env: {
|
|
76
|
+
...process.env,
|
|
77
|
+
HAPPY_STACKS_CLI_REEXEC: '1',
|
|
78
|
+
HAPPY_STACKS_CLI_ROOT_DIR: cliRoot,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
process.exit(res.status ?? 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveHomeDir() {
|
|
85
|
+
const fromEnv = (process.env.HAPPY_STACKS_HOME_DIR ?? process.env.HAPPY_LOCAL_HOME_DIR ?? '').trim();
|
|
86
|
+
if (fromEnv) return expandHome(fromEnv);
|
|
87
|
+
|
|
88
|
+
// Stable pointer file: even if the real home dir is elsewhere, `happys init` writes the pointer here.
|
|
89
|
+
const canonicalEnv = join(homedir(), '.happy-stacks', '.env');
|
|
90
|
+
const v = dotenvGetQuick(canonicalEnv, 'HAPPY_STACKS_HOME_DIR') || dotenvGetQuick(canonicalEnv, 'HAPPY_LOCAL_HOME_DIR') || '';
|
|
91
|
+
return v ? expandHome(v) : join(homedir(), '.happy-stacks');
|
|
22
92
|
}
|
|
23
93
|
|
|
24
94
|
function maybeAutoUpdateNotice(cliRootDir, cmd) {
|
|
@@ -123,6 +193,7 @@ function runNodeScript(cliRootDir, scriptRelPath, args) {
|
|
|
123
193
|
|
|
124
194
|
function main() {
|
|
125
195
|
const cliRootDir = getCliRootDir();
|
|
196
|
+
maybeReexecToCliRoot(cliRootDir);
|
|
126
197
|
const argv = process.argv.slice(2);
|
|
127
198
|
|
|
128
199
|
const cmd = argv.find((a) => !a.startsWith('--')) ?? 'help';
|
package/docs/server-flavors.md
CHANGED
|
@@ -13,8 +13,67 @@ Both are forks/flavors of the same upstream server repo (`slopus/happy-server`),
|
|
|
13
13
|
|
|
14
14
|
- **`happy-server`** (full server)
|
|
15
15
|
- closer to upstream “full” behavior (useful when developing server changes meant to go upstream)
|
|
16
|
-
- typically does **not** serve the built UI
|
|
17
|
-
- useful when you need to test upstream/server-only behavior or reproduce upstream issues
|
|
16
|
+
- the upstream server typically does **not** serve the built UI itself, but happy-stacks provides a **UI gateway** so you still get a single URL that serves the UI and proxies API/websockets/files
|
|
17
|
+
- useful when you need to test upstream/server-only behavior or reproduce upstream issues, with per-stack infra isolation
|
|
18
|
+
|
|
19
|
+
## Full server infra (no AWS required)
|
|
20
|
+
|
|
21
|
+
`happy-server` requires:
|
|
22
|
+
|
|
23
|
+
- Postgres (`DATABASE_URL`)
|
|
24
|
+
- Redis (`REDIS_URL`)
|
|
25
|
+
- S3-compatible object storage (`S3_*`)
|
|
26
|
+
|
|
27
|
+
Happy Stacks can **manage this for you automatically per stack** using Docker Compose (Postgres + Redis + Minio),
|
|
28
|
+
so you **do not need AWS S3**.
|
|
29
|
+
|
|
30
|
+
This happens automatically when you run `happys start/dev --server=happy-server` (or when a stack uses `happy-server`),
|
|
31
|
+
unless you disable it with:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
export HAPPY_STACKS_MANAGED_INFRA=0
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If disabled, you must provide `DATABASE_URL`, `REDIS_URL`, and `S3_*` yourself.
|
|
38
|
+
|
|
39
|
+
## UI serving with `happy-server`
|
|
40
|
+
|
|
41
|
+
The upstream `happy-server` does not serve the built UI itself.
|
|
42
|
+
|
|
43
|
+
For a “one URL” UX (especially with Tailscale), happy-stacks starts a lightweight **UI gateway** that:
|
|
44
|
+
|
|
45
|
+
- serves the built UI at `/` (if a build exists)
|
|
46
|
+
- reverse-proxies API calls to the backend server
|
|
47
|
+
- reverse-proxies realtime websocket upgrades (`/v1/updates`)
|
|
48
|
+
- reverse-proxies public files (to local Minio)
|
|
49
|
+
|
|
50
|
+
This means `happys start --server=happy-server` can still work end-to-end without requiring AWS S3 or a separate nginx setup.
|
|
51
|
+
|
|
52
|
+
## Migrating between flavors (SQLite ⇢ Postgres)
|
|
53
|
+
|
|
54
|
+
Happy Stacks includes an **experimental** migration helper that can copy core chat data from a
|
|
55
|
+
`happy-server-light` stack (SQLite) into a `happy-server` stack (Postgres):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
happys migrate light-to-server --from-stack=main --to-stack=full1
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Optional: include local public files (server-light `files/`) by mirroring them into Minio:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
happys migrate light-to-server --from-stack=main --to-stack=full1 --include-files
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Notes:
|
|
68
|
+
- This preserves IDs (session URLs remain valid on the target server).
|
|
69
|
+
- It also copies the `HANDY_MASTER_SECRET` from the source stack into the target stack’s secret file so auth tokens remain valid.
|
|
70
|
+
|
|
71
|
+
## Prisma behavior (why start is safer under LaunchAgents)
|
|
72
|
+
|
|
73
|
+
- **`happys start`** is “production-like”. It avoids running heavyweight schema sync loops under launchd KeepAlive.
|
|
74
|
+
- **`happys dev`** is for rapid iteration:
|
|
75
|
+
- for `happy-server`: Happy Stacks runs `prisma migrate deploy` by default (configurable via `HAPPY_STACKS_PRISMA_MIGRATE`).
|
|
76
|
+
- for `happy-server-light`: the upstream dev script runs `prisma db push` by default (configurable via `HAPPY_STACKS_PRISMA_PUSH`).
|
|
18
77
|
|
|
19
78
|
Important: for a given run (`happys start` / `happys dev`) you choose **one** flavor.
|
|
20
79
|
|
package/docs/stacks.md
CHANGED
|
@@ -7,6 +7,7 @@ A “stack” is just:
|
|
|
7
7
|
- a dedicated **server port**
|
|
8
8
|
- isolated directories for **UI build output**, **CLI home**, and **logs**
|
|
9
9
|
- optional per-component overrides (point at specific worktrees)
|
|
10
|
+
- (when using `happy-server`) isolated **infra** (Postgres/Redis/Minio) managed per-stack
|
|
10
11
|
|
|
11
12
|
Stacks are configured via a plain env file stored under:
|
|
12
13
|
|
|
@@ -52,6 +53,15 @@ The wizard lets you:
|
|
|
52
53
|
- pick or create worktrees for `happy`, `happy-cli`, and the chosen server component
|
|
53
54
|
- choose which Git remote to base newly-created worktrees on (defaults to `upstream`)
|
|
54
55
|
|
|
56
|
+
When creating `--server=happy-server` stacks, happy-stacks will also reserve additional ports and persist
|
|
57
|
+
the stack-scoped infra config in the stack env file (so restarts are stable):
|
|
58
|
+
|
|
59
|
+
- `HAPPY_STACKS_PG_PORT`
|
|
60
|
+
- `HAPPY_STACKS_REDIS_PORT`
|
|
61
|
+
- `HAPPY_STACKS_MINIO_PORT`
|
|
62
|
+
- `HAPPY_STACKS_MINIO_CONSOLE_PORT`
|
|
63
|
+
- `DATABASE_URL`, `REDIS_URL`, `S3_*`
|
|
64
|
+
|
|
55
65
|
## Run a stack
|
|
56
66
|
|
|
57
67
|
Dev mode:
|
|
@@ -128,9 +138,9 @@ Global/non-stack commands:
|
|
|
128
138
|
- `happys bootstrap` (sets up shared component repos)
|
|
129
139
|
- `happys cli:link` (installs `happy` shim under `~/.happy-stacks/bin/`)
|
|
130
140
|
|
|
131
|
-
## Services (
|
|
141
|
+
## Services (autostart)
|
|
132
142
|
|
|
133
|
-
Each stack can have its own
|
|
143
|
+
Each stack can have its own autostart service (so multiple stacks can start at login).
|
|
134
144
|
|
|
135
145
|
```bash
|
|
136
146
|
happys stack service exp1 install
|
|
@@ -141,10 +151,12 @@ happys stack service exp1 logs
|
|
|
141
151
|
|
|
142
152
|
Implementation notes:
|
|
143
153
|
|
|
144
|
-
- Service label is stack-scoped:
|
|
154
|
+
- Service name/label is stack-scoped:
|
|
145
155
|
- `main` → `com.happy.stacks` (legacy: `com.happy.local`)
|
|
146
156
|
- `exp1` → `com.happy.stacks.exp1` (legacy: `com.happy.local.exp1`)
|
|
147
|
-
-
|
|
157
|
+
- macOS: implemented via **launchd LaunchAgents**
|
|
158
|
+
- Linux: implemented via **systemd user services** (if available)
|
|
159
|
+
- The service persists `HAPPY_STACKS_ENV_FILE` (and legacy `HAPPY_LOCAL_ENV_FILE`), so you can edit the stack env file without reinstalling.
|
|
148
160
|
|
|
149
161
|
## Component/worktree selection per stack
|
|
150
162
|
|
|
@@ -18,14 +18,14 @@ _cli_home_dir="${4:-}" # ignored (kept for backwards compatibility)
|
|
|
18
18
|
HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-$HOME/.happy-stacks}"
|
|
19
19
|
HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
if [[ ! -x "$
|
|
23
|
-
echo "missing terminal happys wrapper: $
|
|
21
|
+
HAPPYS_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
|
|
22
|
+
if [[ ! -x "$HAPPYS_TERM" ]]; then
|
|
23
|
+
echo "missing terminal happys wrapper: $HAPPYS_TERM" >&2
|
|
24
24
|
exit 1
|
|
25
25
|
fi
|
|
26
26
|
|
|
27
27
|
if [[ "$stack" == "main" ]]; then
|
|
28
|
-
exec "$
|
|
28
|
+
exec "$HAPPYS_TERM" auth login
|
|
29
29
|
fi
|
|
30
30
|
|
|
31
|
-
exec "$
|
|
31
|
+
exec "$HAPPYS_TERM" stack auth "$stack" login
|
|
@@ -17,7 +17,44 @@
|
|
|
17
17
|
# Configuration
|
|
18
18
|
# ============================================================================
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# SwiftBar runs with a minimal environment, so users often won't have
|
|
21
|
+
# HAPPY_STACKS_HOME_DIR / HAPPY_STACKS_WORKSPACE_DIR exported.
|
|
22
|
+
# Treat ~/.happy-stacks/.env as the canonical pointer file (written by `happys init`).
|
|
23
|
+
CANONICAL_ENV_FILE="$HOME/.happy-stacks/.env"
|
|
24
|
+
|
|
25
|
+
_dotenv_get_quick() {
|
|
26
|
+
# Usage: _dotenv_get_quick /path/to/env KEY
|
|
27
|
+
local file="$1"
|
|
28
|
+
local key="$2"
|
|
29
|
+
[[ -n "$file" && -n "$key" && -f "$file" ]] || return 0
|
|
30
|
+
local line
|
|
31
|
+
line="$(grep -E "^${key}=" "$file" 2>/dev/null | head -n 1 || true)"
|
|
32
|
+
[[ -n "$line" ]] || return 0
|
|
33
|
+
local v="${line#*=}"
|
|
34
|
+
v="${v%$'\r'}"
|
|
35
|
+
# Strip simple surrounding quotes.
|
|
36
|
+
if [[ "$v" == \"*\" && "$v" == *\" ]]; then v="${v#\"}"; v="${v%\"}"; fi
|
|
37
|
+
if [[ "$v" == \'*\' && "$v" == *\' ]]; then v="${v#\'}"; v="${v%\'}"; fi
|
|
38
|
+
echo "$v"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_expand_home_quick() {
|
|
42
|
+
local p="$1"
|
|
43
|
+
if [[ "$p" == "~/"* ]]; then
|
|
44
|
+
echo "$HOME/${p#~/}"
|
|
45
|
+
else
|
|
46
|
+
echo "$p"
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_home_from_canonical=""
|
|
51
|
+
if [[ -f "$CANONICAL_ENV_FILE" ]]; then
|
|
52
|
+
_home_from_canonical="$(_dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_STACKS_HOME_DIR")"
|
|
53
|
+
[[ -z "$_home_from_canonical" ]] && _home_from_canonical="$(_dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_LOCAL_HOME_DIR")"
|
|
54
|
+
fi
|
|
55
|
+
_home_from_canonical="$(_expand_home_quick "${_home_from_canonical:-}")"
|
|
56
|
+
|
|
57
|
+
HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-${_home_from_canonical:-$HOME/.happy-stacks}}"
|
|
21
58
|
HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
|
|
22
59
|
HAPPY_LOCAL_PORT="${HAPPY_LOCAL_PORT:-3005}"
|
|
23
60
|
|
|
@@ -26,17 +63,6 @@ if [[ -n "${HAPPY_STACKS_WT_TERMINAL:-}" && -z "${HAPPY_LOCAL_WT_TERMINAL:-}" ]]
|
|
|
26
63
|
if [[ -n "${HAPPY_STACKS_WT_SHELL:-}" && -z "${HAPPY_LOCAL_WT_SHELL:-}" ]]; then HAPPY_LOCAL_WT_SHELL="$HAPPY_STACKS_WT_SHELL"; fi
|
|
27
64
|
if [[ -n "${HAPPY_STACKS_SWIFTBAR_ICON_PATH:-}" && -z "${HAPPY_LOCAL_SWIFTBAR_ICON_PATH:-}" ]]; then HAPPY_LOCAL_SWIFTBAR_ICON_PATH="$HAPPY_STACKS_SWIFTBAR_ICON_PATH"; fi
|
|
28
65
|
|
|
29
|
-
# Storage root migrated from ~/.happy/local -> ~/.happy/stacks/main.
|
|
30
|
-
if [[ -z "${HAPPY_HOME_DIR:-}" ]]; then
|
|
31
|
-
if [[ -d "$HOME/.happy/stacks/main" ]] || [[ ! -d "$HOME/.happy/local" ]]; then
|
|
32
|
-
HAPPY_HOME_DIR="$HOME/.happy/stacks/main"
|
|
33
|
-
else
|
|
34
|
-
HAPPY_HOME_DIR="$HOME/.happy/local"
|
|
35
|
-
fi
|
|
36
|
-
fi
|
|
37
|
-
CLI_HOME_DIR="$HAPPY_HOME_DIR/cli"
|
|
38
|
-
LOGS_DIR="$HAPPY_HOME_DIR/logs"
|
|
39
|
-
|
|
40
66
|
# Colors
|
|
41
67
|
GREEN="#34C759"
|
|
42
68
|
RED="#FF3B30"
|
|
@@ -82,7 +108,15 @@ MAIN_ENV_FILE="$(resolve_main_env_file)"
|
|
|
82
108
|
|
|
83
109
|
ensure_launchctl_cache
|
|
84
110
|
|
|
85
|
-
|
|
111
|
+
if [[ -z "$MAIN_ENV_FILE" ]]; then
|
|
112
|
+
MAIN_ENV_FILE="$(resolve_stack_env_file main)"
|
|
113
|
+
fi
|
|
114
|
+
HAPPY_HOME_DIR="$(resolve_stack_base_dir main "$MAIN_ENV_FILE")"
|
|
115
|
+
CLI_HOME_DIR="$(resolve_stack_cli_home_dir main "$MAIN_ENV_FILE")"
|
|
116
|
+
LOGS_DIR="$HAPPY_HOME_DIR/logs"
|
|
117
|
+
MAIN_LABEL="$(resolve_stack_label main)"
|
|
118
|
+
|
|
119
|
+
MAIN_COLLECT="$(collect_stack_status "$MAIN_PORT" "$CLI_HOME_DIR" "$MAIN_LABEL" "$HAPPY_HOME_DIR")"
|
|
86
120
|
IFS=$'\t' read -r MAIN_LEVEL MAIN_SERVER_STATUS MAIN_SERVER_PID MAIN_SERVER_METRICS MAIN_DAEMON_STATUS MAIN_DAEMON_PID MAIN_DAEMON_METRICS MAIN_DAEMON_UPTIME MAIN_LAST_HEARTBEAT MAIN_LAUNCHAGENT_STATUS MAIN_AUTOSTART_PID MAIN_AUTOSTART_METRICS <<<"$MAIN_COLLECT"
|
|
87
121
|
for v in MAIN_SERVER_PID MAIN_SERVER_METRICS MAIN_DAEMON_PID MAIN_DAEMON_METRICS MAIN_DAEMON_UPTIME MAIN_LAST_HEARTBEAT MAIN_AUTOSTART_PID MAIN_AUTOSTART_METRICS; do
|
|
88
122
|
if [[ "${!v}" == "-" ]]; then
|
|
@@ -112,10 +146,10 @@ echo "---"
|
|
|
112
146
|
echo "Main stack"
|
|
113
147
|
echo "---"
|
|
114
148
|
export MAIN_LEVEL="$MAIN_LEVEL"
|
|
115
|
-
render_stack_info "" "main" "$MAIN_PORT" "$MAIN_SERVER_COMPONENT" "$HAPPY_HOME_DIR" "$CLI_HOME_DIR" "
|
|
116
|
-
render_component_server "" "main" "$MAIN_PORT" "$MAIN_SERVER_COMPONENT" "$MAIN_SERVER_STATUS" "$MAIN_SERVER_PID" "$MAIN_SERVER_METRICS" "$TAILSCALE_URL" "
|
|
149
|
+
render_stack_info "" "main" "$MAIN_PORT" "$MAIN_SERVER_COMPONENT" "$HAPPY_HOME_DIR" "$CLI_HOME_DIR" "$MAIN_LABEL" "$MAIN_ENV_FILE" "$TAILSCALE_URL"
|
|
150
|
+
render_component_server "" "main" "$MAIN_PORT" "$MAIN_SERVER_COMPONENT" "$MAIN_SERVER_STATUS" "$MAIN_SERVER_PID" "$MAIN_SERVER_METRICS" "$TAILSCALE_URL" "$MAIN_LABEL"
|
|
117
151
|
render_component_daemon "" "$MAIN_DAEMON_STATUS" "$MAIN_DAEMON_PID" "$MAIN_DAEMON_METRICS" "$MAIN_DAEMON_UPTIME" "$MAIN_LAST_HEARTBEAT" "$CLI_HOME_DIR/daemon.state.json" "main"
|
|
118
|
-
render_component_autostart "" "main" "
|
|
152
|
+
render_component_autostart "" "main" "$MAIN_LABEL" "$MAIN_LAUNCHAGENT_STATUS" "$MAIN_AUTOSTART_PID" "$MAIN_AUTOSTART_METRICS" "$LOGS_DIR"
|
|
119
153
|
render_component_tailscale "" "main" "$TAILSCALE_URL"
|
|
120
154
|
|
|
121
155
|
echo "---"
|
|
@@ -123,33 +157,39 @@ echo "Stacks"
|
|
|
123
157
|
echo "---"
|
|
124
158
|
|
|
125
159
|
if [[ -n "$PNPM_BIN" ]]; then
|
|
126
|
-
|
|
127
|
-
echo "New stack (interactive) | bash=$
|
|
128
|
-
echo "List stacks | bash=$
|
|
160
|
+
HAPPYS_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
|
|
161
|
+
echo "New stack (interactive) | bash=$HAPPYS_TERM param1=stack param2=new param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
162
|
+
echo "List stacks | bash=$HAPPYS_TERM param1=stack param2=list dir=$HAPPY_LOCAL_DIR terminal=false"
|
|
129
163
|
echo "---"
|
|
130
164
|
fi
|
|
131
165
|
|
|
132
|
-
STACKS_DIR="$
|
|
133
|
-
|
|
134
|
-
|
|
166
|
+
STACKS_DIR="$(resolve_stacks_storage_root)"
|
|
167
|
+
LEGACY_STACKS_DIR="$HOME/.happy/local/stacks"
|
|
168
|
+
if [[ -d "$STACKS_DIR" ]] || [[ -d "$LEGACY_STACKS_DIR" ]]; then
|
|
169
|
+
STACK_NAMES="$(
|
|
170
|
+
{
|
|
171
|
+
ls -1 "$STACKS_DIR" 2>/dev/null || true
|
|
172
|
+
ls -1 "$LEGACY_STACKS_DIR" 2>/dev/null || true
|
|
173
|
+
} | sort -u
|
|
174
|
+
)"
|
|
135
175
|
if [[ -z "$STACK_NAMES" ]]; then
|
|
136
176
|
echo "No stacks found | color=$GRAY"
|
|
137
177
|
fi
|
|
138
178
|
for s in $STACK_NAMES; do
|
|
139
|
-
env_file="$
|
|
179
|
+
env_file="$(resolve_stack_env_file "$s")"
|
|
140
180
|
[[ -f "$env_file" ]] || continue
|
|
141
181
|
|
|
142
|
-
port="$(dotenv_get "$env_file" "
|
|
182
|
+
port="$(dotenv_get "$env_file" "HAPPY_STACKS_SERVER_PORT")"
|
|
183
|
+
[[ -z "$port" ]] && port="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_PORT")"
|
|
143
184
|
[[ -n "$port" ]] || continue
|
|
144
185
|
|
|
145
|
-
server_component="$(dotenv_get "$env_file" "
|
|
186
|
+
server_component="$(dotenv_get "$env_file" "HAPPY_STACKS_SERVER_COMPONENT")"
|
|
187
|
+
[[ -z "$server_component" ]] && server_component="$(dotenv_get "$env_file" "HAPPY_LOCAL_SERVER_COMPONENT")"
|
|
146
188
|
[[ -n "$server_component" ]] || server_component="happy-server-light"
|
|
147
189
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
base_dir="$STACKS_DIR/$s"
|
|
152
|
-
label="com.happy.stacks.$s"
|
|
190
|
+
base_dir="$(resolve_stack_base_dir "$s" "$env_file")"
|
|
191
|
+
cli_home_dir="$(resolve_stack_cli_home_dir "$s" "$env_file")"
|
|
192
|
+
label="$(resolve_stack_label "$s")"
|
|
153
193
|
|
|
154
194
|
COLLECT="$(collect_stack_status "$port" "$cli_home_dir" "$label" "$base_dir")"
|
|
155
195
|
IFS=$'\t' read -r LEVEL SERVER_STATUS SERVER_PID SERVER_METRICS DAEMON_STATUS DAEMON_PID DAEMON_METRICS DAEMON_UPTIME LAST_HEARTBEAT LAUNCHAGENT_STATUS AUTOSTART_PID AUTOSTART_METRICS <<<"$COLLECT"
|
|
@@ -169,22 +209,22 @@ if [[ -d "$STACKS_DIR" ]]; then
|
|
|
169
209
|
render_components_menu "--" "stack" "$s" "$env_file"
|
|
170
210
|
done
|
|
171
211
|
else
|
|
172
|
-
echo "No stacks dir found at
|
|
212
|
+
echo "No stacks dir found at: $(shorten_path "$STACKS_DIR" 52) | color=$GRAY"
|
|
173
213
|
fi
|
|
174
214
|
|
|
175
215
|
echo "---"
|
|
176
|
-
render_components_menu "" "main" "main" ""
|
|
216
|
+
render_components_menu "" "main" "main" "$MAIN_ENV_FILE"
|
|
177
217
|
|
|
178
218
|
echo "Worktrees | sfimage=arrow.triangle.branch"
|
|
179
219
|
if [[ -z "$PNPM_BIN" ]]; then
|
|
180
220
|
echo "--⚠️ happys not found (run: npx happy-stacks init, or install happy-stacks globally)"
|
|
181
221
|
else
|
|
182
|
-
|
|
183
|
-
echo "--Use (interactive) | bash=$
|
|
184
|
-
echo "--New (interactive) | bash=$
|
|
222
|
+
HAPPYS_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
|
|
223
|
+
echo "--Use (interactive) | bash=$HAPPYS_TERM param1=wt param2=use param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
224
|
+
echo "--New (interactive) | bash=$HAPPYS_TERM param1=wt param2=new param3=--interactive dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
185
225
|
echo "--PR worktree (prompt) | bash=$HAPPY_LOCAL_DIR/extras/swiftbar/wt-pr.sh dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
186
226
|
echo "--Sync mirrors (all) | bash=$PNPM_BIN param1=wt param2=sync-all dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
187
|
-
echo "--Update all (dry-run) | bash=$
|
|
227
|
+
echo "--Update all (dry-run) | bash=$HAPPYS_TERM param1=wt param2=update-all param3=--dry-run dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
188
228
|
echo "--Update all (apply) | bash=$PNPM_BIN param1=wt param2=update-all dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
189
229
|
fi
|
|
190
230
|
|
|
@@ -193,10 +233,10 @@ echo "Setup / Tools"
|
|
|
193
233
|
if [[ -z "$PNPM_BIN" ]]; then
|
|
194
234
|
echo "--⚠️ happys not found (run: npx happy-stacks init, or install happy-stacks globally)"
|
|
195
235
|
else
|
|
196
|
-
|
|
197
|
-
echo "--Bootstrap (clone/install) | bash=$
|
|
198
|
-
echo "--CLI link (install happy wrapper) | bash=$
|
|
199
|
-
echo "--Mobile dev helper | bash=$
|
|
236
|
+
HAPPYS_TERM="$HAPPY_LOCAL_DIR/extras/swiftbar/happys-term.sh"
|
|
237
|
+
echo "--Bootstrap (clone/install) | bash=$HAPPYS_TERM param1=bootstrap dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
238
|
+
echo "--CLI link (install happy wrapper) | bash=$HAPPYS_TERM param1=cli:link dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
239
|
+
echo "--Mobile dev helper | bash=$HAPPYS_TERM param1=mobile dir=$HAPPY_LOCAL_DIR terminal=false"
|
|
200
240
|
fi
|
|
201
241
|
|
|
202
242
|
echo "---"
|
|
@@ -215,4 +255,6 @@ echo "--1h | bash=$SET_INTERVAL param1=1h dir=$HAPPY_LOCAL_DIR terminal=false re
|
|
|
215
255
|
echo "--2h | bash=$SET_INTERVAL param1=2h dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
216
256
|
echo "--6h | bash=$SET_INTERVAL param1=6h dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
217
257
|
echo "--12h | bash=$SET_INTERVAL param1=12h dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
218
|
-
echo "--1d | bash=$SET_INTERVAL param1=1d dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
258
|
+
echo "--1d | bash=$SET_INTERVAL param1=1d dir=$HAPPY_LOCAL_DIR terminal=false refresh=true"
|
|
259
|
+
|
|
260
|
+
exit 0
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Open preferred terminal and run a happys command.
|
|
5
|
+
#
|
|
6
|
+
# Preference order follows wt shell semantics:
|
|
7
|
+
# - HAPPY_LOCAL_WT_TERMINAL=ghostty|iterm|terminal|current
|
|
8
|
+
# (also accepts "auto" which tries ghostty->iterm->terminal->current)
|
|
9
|
+
#
|
|
10
|
+
# Notes:
|
|
11
|
+
# - iTerm / Terminal: we run the command automatically via AppleScript.
|
|
12
|
+
# - Ghostty: best-effort; if we can't run the command, we open Ghostty in the dir and copy the command to clipboard.
|
|
13
|
+
|
|
14
|
+
CANONICAL_ENV_FILE="$HOME/.happy-stacks/.env"
|
|
15
|
+
|
|
16
|
+
dotenv_get_quick() {
|
|
17
|
+
local file="$1"
|
|
18
|
+
local key="$2"
|
|
19
|
+
[[ -n "$file" && -n "$key" && -f "$file" ]] || return 0
|
|
20
|
+
local line
|
|
21
|
+
line="$(grep -E "^${key}=" "$file" 2>/dev/null | head -n 1 || true)"
|
|
22
|
+
[[ -n "$line" ]] || return 0
|
|
23
|
+
local v="${line#*=}"
|
|
24
|
+
v="${v%$'\r'}"
|
|
25
|
+
if [[ "$v" == \"*\" && "$v" == *\" ]]; then v="${v#\"}"; v="${v%\"}"; fi
|
|
26
|
+
if [[ "$v" == \'*\' && "$v" == *\' ]]; then v="${v#\'}"; v="${v%\'}"; fi
|
|
27
|
+
echo "$v"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
expand_home_quick() {
|
|
31
|
+
local p="$1"
|
|
32
|
+
if [[ "$p" == "~/"* ]]; then
|
|
33
|
+
echo "$HOME/${p#~/}"
|
|
34
|
+
else
|
|
35
|
+
echo "$p"
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
home_from_canonical=""
|
|
40
|
+
ws_from_canonical=""
|
|
41
|
+
if [[ -f "$CANONICAL_ENV_FILE" ]]; then
|
|
42
|
+
home_from_canonical="$(dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_STACKS_HOME_DIR")"
|
|
43
|
+
[[ -z "$home_from_canonical" ]] && home_from_canonical="$(dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_LOCAL_HOME_DIR")"
|
|
44
|
+
ws_from_canonical="$(dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_STACKS_WORKSPACE_DIR")"
|
|
45
|
+
[[ -z "$ws_from_canonical" ]] && ws_from_canonical="$(dotenv_get_quick "$CANONICAL_ENV_FILE" "HAPPY_LOCAL_WORKSPACE_DIR")"
|
|
46
|
+
fi
|
|
47
|
+
home_from_canonical="$(expand_home_quick "${home_from_canonical:-}")"
|
|
48
|
+
ws_from_canonical="$(expand_home_quick "${ws_from_canonical:-}")"
|
|
49
|
+
|
|
50
|
+
HAPPY_STACKS_HOME_DIR="${HAPPY_STACKS_HOME_DIR:-${home_from_canonical:-$HOME/.happy-stacks}}"
|
|
51
|
+
HAPPY_LOCAL_DIR="${HAPPY_LOCAL_DIR:-$HAPPY_STACKS_HOME_DIR}"
|
|
52
|
+
|
|
53
|
+
WORKDIR="${HAPPY_STACKS_WORKSPACE_DIR:-${ws_from_canonical:-$HAPPY_STACKS_HOME_DIR/workspace}}"
|
|
54
|
+
if [[ ! -d "$WORKDIR" ]]; then
|
|
55
|
+
WORKDIR="$HOME"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
HAPPYS_SH="$HAPPY_LOCAL_DIR/extras/swiftbar/happys.sh"
|
|
59
|
+
if [[ ! -x "$HAPPYS_SH" ]]; then
|
|
60
|
+
echo "missing happys wrapper: $HAPPYS_SH" >&2
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
pref_raw="$(echo "${HAPPY_STACKS_WT_TERMINAL:-${HAPPY_LOCAL_WT_TERMINAL:-auto}}" | tr '[:upper:]' '[:lower:]')"
|
|
65
|
+
pref="$pref_raw"
|
|
66
|
+
if [[ "$pref" == "" ]]; then pref="auto"; fi
|
|
67
|
+
|
|
68
|
+
cmd=( "$HAPPYS_SH" "$@" )
|
|
69
|
+
|
|
70
|
+
escape_for_osascript_string() {
|
|
71
|
+
local s="$1"
|
|
72
|
+
s="${s//\\/\\\\}"
|
|
73
|
+
s="${s//\"/\\\"}"
|
|
74
|
+
echo "$s"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
shell_cmd() {
|
|
78
|
+
local joined=""
|
|
79
|
+
local q
|
|
80
|
+
joined="cd \"${WORKDIR//\"/\\\"}\"; "
|
|
81
|
+
for q in "${cmd[@]}"; do
|
|
82
|
+
local escaped
|
|
83
|
+
escaped="$(printf "%s" "$q" | sed "s/'/'\\\\''/g")"
|
|
84
|
+
joined+="'${escaped}' "
|
|
85
|
+
done
|
|
86
|
+
joined+="; echo; echo \"[happy-stacks] done\"; exec /bin/zsh -i"
|
|
87
|
+
echo "$joined"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
run_iterm() {
|
|
91
|
+
if ! command -v osascript >/dev/null 2>&1; then
|
|
92
|
+
return 1
|
|
93
|
+
fi
|
|
94
|
+
local s
|
|
95
|
+
s="$(shell_cmd)"
|
|
96
|
+
s="$(escape_for_osascript_string "$s")"
|
|
97
|
+
osascript \
|
|
98
|
+
-e 'tell application "iTerm" to activate' \
|
|
99
|
+
-e 'tell application "iTerm" to create window with default profile' \
|
|
100
|
+
-e "tell application \"iTerm\" to tell current session of current window to write text \"${s}\"" >/dev/null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
run_terminal_app() {
|
|
104
|
+
if ! command -v osascript >/dev/null 2>&1; then
|
|
105
|
+
return 1
|
|
106
|
+
fi
|
|
107
|
+
local s
|
|
108
|
+
s="$(shell_cmd)"
|
|
109
|
+
s="$(escape_for_osascript_string "$s")"
|
|
110
|
+
osascript \
|
|
111
|
+
-e 'tell application "Terminal" to activate' \
|
|
112
|
+
-e "tell application \"Terminal\" to do script \"${s}\"" >/dev/null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
run_ghostty() {
|
|
116
|
+
if ! command -v ghostty >/dev/null 2>&1; then
|
|
117
|
+
return 1
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
local s
|
|
121
|
+
s="$(shell_cmd)"
|
|
122
|
+
if ghostty --working-directory "$WORKDIR" -e /bin/zsh -lc "$s" >/dev/null 2>&1; then
|
|
123
|
+
return 0
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
echo -n "$s" | pbcopy 2>/dev/null || true
|
|
127
|
+
ghostty --working-directory "$WORKDIR" >/dev/null 2>&1 || true
|
|
128
|
+
return 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try_one() {
|
|
132
|
+
local t="$1"
|
|
133
|
+
case "$t" in
|
|
134
|
+
ghostty) run_ghostty ;;
|
|
135
|
+
iterm) run_iterm ;;
|
|
136
|
+
terminal) run_terminal_app ;;
|
|
137
|
+
current) ( cd "$WORKDIR"; exec "${cmd[@]}" ) ;;
|
|
138
|
+
*) return 1 ;;
|
|
139
|
+
esac
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if [[ "$pref" == "auto" ]]; then
|
|
143
|
+
for t in ghostty iterm terminal current; do
|
|
144
|
+
if try_one "$t"; then
|
|
145
|
+
exit 0
|
|
146
|
+
fi
|
|
147
|
+
done
|
|
148
|
+
exit 1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
try_one "$pref"
|