@clearlysh/beehaven 0.8.16
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/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- package/skills/pair-on-canvas/SKILL.md +92 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli-state — CLI-side session state.
|
|
3
|
+
*
|
|
4
|
+
* Where we are (the "connected target") lives in `~/.beehaven/target.json` — or
|
|
5
|
+
* `target-<agent>.json` when $BEEHAVEN_AGENT is set, so concurrent agents don't
|
|
6
|
+
* clobber each other (see targetFileFor). Not daemon RAM: that makes the daemon
|
|
7
|
+
* stateless for navigation, so daemon restarts can't lose your target and
|
|
8
|
+
* `beehaven pwd` works without the daemon running at all.
|
|
9
|
+
*
|
|
10
|
+
* Contract:
|
|
11
|
+
* - `beehaven cd <addr>` → setTarget(...) writes the file
|
|
12
|
+
* - `beehaven cd home` → clearTarget() removes it (home is the default)
|
|
13
|
+
* - `beehaven disconnect` → clearTarget()
|
|
14
|
+
* - `beehaven pwd` → getTarget() reads it
|
|
15
|
+
* - any `beehaven call` → getTarget() reads it before falling back to daemon
|
|
16
|
+
*
|
|
17
|
+
* Why this shape instead of daemon-owned state:
|
|
18
|
+
* - Daemon restart used to silently lose your target, misrouting the
|
|
19
|
+
* next command to home. File-based state survives anything short of
|
|
20
|
+
* `rm -rf ~/.beehaven`.
|
|
21
|
+
* - `pwd` becomes infinitely cheap — no IPC round-trip.
|
|
22
|
+
* - The CLI can be used during Mac-app startup before the daemon's
|
|
23
|
+
* fully up.
|
|
24
|
+
*
|
|
25
|
+
* Why NOT put it in the daemon even as a sync'd cache:
|
|
26
|
+
* - Two sources of truth is always worse than one. Either side could
|
|
27
|
+
* diverge. Single source of truth is the file.
|
|
28
|
+
*
|
|
29
|
+
* The daemon's own `connectedBuilding` is mostly a focus/relay-routing
|
|
30
|
+
* optimization (it needs to know what to pre-warm) — but it IS still
|
|
31
|
+
* authoritative in one case, and this used to claim otherwise ("no longer
|
|
32
|
+
* authoritative for 'where is the user'"). It isn't true: when NO target file
|
|
33
|
+
* exists, `case 'call'` in cli.ts falls back to `daemonGetConnection()` and routes
|
|
34
|
+
* there (it calls itself a legacy fallback).
|
|
35
|
+
*
|
|
36
|
+
* That matters most for AGENTS. The fallback is machine-global — set by any other
|
|
37
|
+
* process, another room, the Mac app, a human's terminal — so an agent that never
|
|
38
|
+
* runs `connect` doesn't get "home", it inherits someone else's cursor, silently.
|
|
39
|
+
* Which is why the canvas skill tells agents to `beehaven connect home` FIRST: it
|
|
40
|
+
* looks redundant (home is the address-grammar default) and it is not. Pinning the
|
|
41
|
+
* target is what stops the inheritance. Pin it, or pass `--building` per call.
|
|
42
|
+
*/
|
|
43
|
+
import { deriveAgentScope } from './agent-scope.js';
|
|
44
|
+
import { agentForScope } from './agent-state.js';
|
|
45
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, renameSync } from 'fs';
|
|
46
|
+
import { join } from 'path';
|
|
47
|
+
import { homedir } from 'os';
|
|
48
|
+
const STATE_DIR = join(homedir(), '.beehaven');
|
|
49
|
+
/**
|
|
50
|
+
* PER-AGENT target scoping — one `target.json` per identity, not one per machine.
|
|
51
|
+
*
|
|
52
|
+
* This file's own docs have always promised it ("it rides with the identity-bound
|
|
53
|
+
* session: each terminal ($BEEHAVEN_AGENT) keeps its own target.json cwd") and the
|
|
54
|
+
* path never delivered it: `target.json`, one file, shared by every process on the
|
|
55
|
+
* box. `BEEHAVEN_AGENT` appeared exactly once in this module — in that sentence.
|
|
56
|
+
*
|
|
57
|
+
* It matters because AI AGENTS RUN CONCURRENTLY. The desktop hosts four rooms
|
|
58
|
+
* (claude/codex/gemini/pi) that can each shell out at the same time, and the
|
|
59
|
+
* documented workflow is `connect` then `call` — so room B's `connect team/x`
|
|
60
|
+
* silently re-pointed room A's next `call`, with no error and no way for A to
|
|
61
|
+
* know. A human types `connect` in one terminal and notices; a fleet of agents
|
|
62
|
+
* does not. Low blast radius today (every room connects to `home`, the same
|
|
63
|
+
* value), unbounded the moment one connects to a team or a store.
|
|
64
|
+
*
|
|
65
|
+
* Unscoped stays `target.json` — a human shell with no BEEHAVEN_AGENT set keeps
|
|
66
|
+
* the exact file it has today, so nothing migrates and nothing breaks.
|
|
67
|
+
*
|
|
68
|
+
* SANITISED because this lands in a filesystem path: `BEEHAVEN_AGENT=../../..`
|
|
69
|
+
* would otherwise escape STATE_DIR. Anything but [A-Za-z0-9_-] is dropped, and a
|
|
70
|
+
* name that sanitises to nothing falls back to the shared file rather than
|
|
71
|
+
* silently writing `target-.json`.
|
|
72
|
+
*/
|
|
73
|
+
/**
|
|
74
|
+
* ⚠⚠ A LOGGED-IN AGENT'S CURSOR FOLLOWS THE AGENT, NOT THE PROCESS.
|
|
75
|
+
*
|
|
76
|
+
* `deriveAgentScope()` returns a per-SESSION handle (`claude-<sessionId>`), which isolates
|
|
77
|
+
* concurrent agents but resets every conversation — so a durable agent would start at home
|
|
78
|
+
* each time and lose the workspace it had chosen.
|
|
79
|
+
*
|
|
80
|
+
* Once `agent login` has bound this scope to a named agent, key the cursor on the AGENT
|
|
81
|
+
* instead. That is what unifies the three handles: identity, workspace session and cursor
|
|
82
|
+
* all resolve from one name.
|
|
83
|
+
*
|
|
84
|
+
* ⚠ REVERSE LOOKUP, NOT A STORED POINTER. A pointer would be one more machine-global file to
|
|
85
|
+
* clobber — the exact bug this whole area exists to fix. The binding lives on the agent
|
|
86
|
+
* record, and `agentForScope` refuses when two agents claim one scope rather than picking.
|
|
87
|
+
*
|
|
88
|
+
* ⚠ LAZY + GUARDED: `agent-state` reads `~/.beehaven/agents/*`, which may not exist, and this
|
|
89
|
+
* runs at module load for TARGET_FILE. A throw here would break `pwd` for everyone.
|
|
90
|
+
*/
|
|
91
|
+
export function effectiveScope() {
|
|
92
|
+
const raw = deriveAgentScope();
|
|
93
|
+
if (!raw)
|
|
94
|
+
return ''; // a human — the shared file
|
|
95
|
+
try {
|
|
96
|
+
const bound = agentForScope(raw);
|
|
97
|
+
if (bound)
|
|
98
|
+
return bound;
|
|
99
|
+
}
|
|
100
|
+
catch { /* no agents dir, or a bad record — fall back to the process scope */ }
|
|
101
|
+
return raw;
|
|
102
|
+
}
|
|
103
|
+
function targetFileFor(agent = effectiveScope()) {
|
|
104
|
+
const safe = (agent || '').trim().replace(/[^A-Za-z0-9_-]/g, '');
|
|
105
|
+
return join(STATE_DIR, safe ? `target-${safe}.json` : 'target.json');
|
|
106
|
+
}
|
|
107
|
+
const TARGET_FILE = targetFileFor();
|
|
108
|
+
/** Exported for the scoping test — the path rule is security-relevant (it takes an env var into a filesystem path), so it gets asserted rather than eyeballed. */
|
|
109
|
+
export const __targetFileFor = targetFileFor;
|
|
110
|
+
/**
|
|
111
|
+
* Every address that means "the user's default workspace".
|
|
112
|
+
*
|
|
113
|
+
* `beehaven cd home` accepts `home` / `me` / `workspace` and PERSISTS the
|
|
114
|
+
* canonical `workspace`. Readers that only compared against the literal
|
|
115
|
+
* `'home'` therefore failed on a target the `cd` had just written — `sql`,
|
|
116
|
+
* `ls`, `cat`, `grep` and `tree` all answered "Cannot determine target"
|
|
117
|
+
* directly after a successful `cd home`, and suggested running it again.
|
|
118
|
+
* Writer and readers now share THIS set; extend it here or nowhere.
|
|
119
|
+
*/
|
|
120
|
+
export const HOME_ADDRESSES = new Set(['workspace', 'home', 'me']);
|
|
121
|
+
/** The address `cd` writes for the default workspace. */
|
|
122
|
+
export const CANONICAL_HOME_ADDRESS = 'workspace';
|
|
123
|
+
/** Re-exported so callers keep one import site; the derivation lives in agent-scope.ts. */
|
|
124
|
+
export { deriveAgentScope };
|
|
125
|
+
/** True when an address refers to the default workspace. */
|
|
126
|
+
export function isHomeAddress(address) {
|
|
127
|
+
return !!address && HOME_ADDRESSES.has(address);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Read the current target. Returns null when no target is set — which
|
|
131
|
+
* means "use home" per the address grammar.
|
|
132
|
+
*
|
|
133
|
+
* Never throws: bad JSON, missing file, unreadable permissions all
|
|
134
|
+
* return null. Defensive because this runs on every `beehaven call`.
|
|
135
|
+
*/
|
|
136
|
+
export function getTarget() {
|
|
137
|
+
try {
|
|
138
|
+
if (!existsSync(TARGET_FILE))
|
|
139
|
+
return null;
|
|
140
|
+
const raw = readFileSync(TARGET_FILE, 'utf-8');
|
|
141
|
+
const parsed = JSON.parse(raw);
|
|
142
|
+
if (typeof parsed?.address !== 'string' || !parsed.address)
|
|
143
|
+
return null;
|
|
144
|
+
if (typeof parsed?.connectedAt !== 'number')
|
|
145
|
+
return null;
|
|
146
|
+
return {
|
|
147
|
+
address: parsed.address,
|
|
148
|
+
connectedAt: parsed.connectedAt,
|
|
149
|
+
kind: parsed.kind,
|
|
150
|
+
cwd: typeof parsed.cwd === 'string' && parsed.cwd ? parsed.cwd : undefined,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Current working directory within the target DO. Returns "/" (root) when
|
|
159
|
+
* unset. Read by `ask` (routes to the cwd-layer Librarian), `pwd`, and the
|
|
160
|
+
* default `ls`/`tree`.
|
|
161
|
+
*/
|
|
162
|
+
export function getCwd() {
|
|
163
|
+
const t = getTarget();
|
|
164
|
+
return t?.cwd && t.cwd.trim() ? t.cwd : '/';
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Resolve a `cd` argument against the current cwd to an absolute DO path —
|
|
168
|
+
* the pure core of `beehaven cd`. Mirrors shell semantics:
|
|
169
|
+
* - "/", "~" → root
|
|
170
|
+
* - "." → unchanged
|
|
171
|
+
* - "/abs/path" → resolved from root (leading slash resets the base)
|
|
172
|
+
* - "rel/path" → resolved against `current`
|
|
173
|
+
* - ".." segments pop a level (never past root); "." / empty segments skip
|
|
174
|
+
* Always returns a normalized absolute path with no trailing slash (except "/").
|
|
175
|
+
* Kept side-effect-free so the navigation rules are unit-testable.
|
|
176
|
+
*/
|
|
177
|
+
export function resolveCwd(current, input) {
|
|
178
|
+
const cur = current && current.trim() ? current : '/';
|
|
179
|
+
let next;
|
|
180
|
+
if (input === '~' || input === '/') {
|
|
181
|
+
next = '/';
|
|
182
|
+
}
|
|
183
|
+
else if (input === '.') {
|
|
184
|
+
next = cur;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const base = input.startsWith('/') ? [] : cur === '/' ? [] : cur.split('/').filter(Boolean);
|
|
188
|
+
const rel = input.replace(/^\//, '');
|
|
189
|
+
for (const seg of rel.split('/')) {
|
|
190
|
+
if (seg === '..')
|
|
191
|
+
base.pop();
|
|
192
|
+
else if (seg && seg !== '.')
|
|
193
|
+
base.push(seg);
|
|
194
|
+
}
|
|
195
|
+
next = '/' + base.join('/');
|
|
196
|
+
}
|
|
197
|
+
if (next.length > 1)
|
|
198
|
+
next = next.replace(/\/+$/, '');
|
|
199
|
+
return next;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Set the cwd cursor within the CURRENT target DO, preserving the target. When
|
|
203
|
+
* no target is set (home), writes an explicit `home` target carrying the cwd so
|
|
204
|
+
* navigation persists. Pass "/" (or empty) to return to root.
|
|
205
|
+
*/
|
|
206
|
+
export function setCwd(cwd) {
|
|
207
|
+
const cur = getTarget();
|
|
208
|
+
const address = cur?.address ?? 'home';
|
|
209
|
+
const kind = cur?.kind ?? inferKind(address);
|
|
210
|
+
const normalized = !cwd || cwd === '/' ? undefined : cwd;
|
|
211
|
+
const target = {
|
|
212
|
+
address,
|
|
213
|
+
connectedAt: cur?.connectedAt ?? Date.now(),
|
|
214
|
+
kind,
|
|
215
|
+
...(normalized ? { cwd: normalized } : {}),
|
|
216
|
+
};
|
|
217
|
+
ensureStateDir();
|
|
218
|
+
const tmp = `${TARGET_FILE}.tmp-${process.pid}`;
|
|
219
|
+
writeFileSync(tmp, JSON.stringify(target, null, 2), { mode: 0o600 });
|
|
220
|
+
try {
|
|
221
|
+
renameSync(tmp, TARGET_FILE);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
try {
|
|
225
|
+
unlinkSync(tmp);
|
|
226
|
+
}
|
|
227
|
+
catch { }
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
return target;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Write a new target. Overwrites any existing target (last-write-wins;
|
|
234
|
+
* concurrent `cd` from two shells is user error, not something we try
|
|
235
|
+
* to reconcile).
|
|
236
|
+
*
|
|
237
|
+
* `kind` defaults to inferring from the address:
|
|
238
|
+
* - contains slash → 'cloud'
|
|
239
|
+
* - bare name → 'local'
|
|
240
|
+
* - 'home' / 'me' → 'home' (but callers should prefer clearTarget for home)
|
|
241
|
+
*/
|
|
242
|
+
export function setTarget(address, kind) {
|
|
243
|
+
if (!address || typeof address !== 'string') {
|
|
244
|
+
throw new Error('setTarget: address must be a non-empty string');
|
|
245
|
+
}
|
|
246
|
+
const resolvedKind = kind ?? inferKind(address);
|
|
247
|
+
const target = {
|
|
248
|
+
address,
|
|
249
|
+
connectedAt: Date.now(),
|
|
250
|
+
kind: resolvedKind,
|
|
251
|
+
};
|
|
252
|
+
ensureStateDir();
|
|
253
|
+
// Atomic write — tmp file then rename — so a crash mid-write can't
|
|
254
|
+
// leave a partial target.json that subsequent reads fail to parse.
|
|
255
|
+
const tmp = `${TARGET_FILE}.tmp-${process.pid}`;
|
|
256
|
+
writeFileSync(tmp, JSON.stringify(target, null, 2), { mode: 0o600 });
|
|
257
|
+
try {
|
|
258
|
+
// renameSync is atomic on POSIX
|
|
259
|
+
renameSync(tmp, TARGET_FILE);
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
try {
|
|
263
|
+
unlinkSync(tmp);
|
|
264
|
+
}
|
|
265
|
+
catch { }
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
return target;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Clear the current target. `beehaven cd home` and `beehaven disconnect`
|
|
272
|
+
* both call this; home is represented by the ABSENCE of a target file
|
|
273
|
+
* (not a target with address='home'), so downstream code has a single
|
|
274
|
+
* condition to check.
|
|
275
|
+
*/
|
|
276
|
+
export function clearTarget() {
|
|
277
|
+
try {
|
|
278
|
+
if (existsSync(TARGET_FILE))
|
|
279
|
+
unlinkSync(TARGET_FILE);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// Unlink can fail if the file just stopped existing between the
|
|
283
|
+
// check and the call. That's fine.
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Age of the current target in ms. Returns -1 when no target is set.
|
|
288
|
+
* Used by `pwd` to surface session freshness.
|
|
289
|
+
*/
|
|
290
|
+
export function getTargetAgeMs() {
|
|
291
|
+
const t = getTarget();
|
|
292
|
+
if (!t)
|
|
293
|
+
return -1;
|
|
294
|
+
return Math.max(0, Date.now() - t.connectedAt);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Split an address into `{buildingType, buildingId}`. Returns null for
|
|
298
|
+
* bare names (local apps) and for 'home'/'me'.
|
|
299
|
+
*/
|
|
300
|
+
export function parseAddress(address) {
|
|
301
|
+
// 'workspace' is the canonical default (direct over the relay); 'home'/'me' are silent
|
|
302
|
+
// aliases. A bare 'workspace' has no id → not a type/id address → null (default routing).
|
|
303
|
+
if (!address || address === 'workspace' || address === 'home' || address === 'me')
|
|
304
|
+
return null;
|
|
305
|
+
const slash = address.indexOf('/');
|
|
306
|
+
if (slash <= 0 || slash === address.length - 1)
|
|
307
|
+
return null;
|
|
308
|
+
return {
|
|
309
|
+
buildingType: address.slice(0, slash),
|
|
310
|
+
buildingId: address.slice(slash + 1),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
// ── Internal ────────────────────────────────────────────────────────────
|
|
314
|
+
function ensureStateDir() {
|
|
315
|
+
try {
|
|
316
|
+
if (!existsSync(STATE_DIR)) {
|
|
317
|
+
mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// If this fails we'll see the error on writeFileSync — no point
|
|
322
|
+
// pre-throwing here.
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function inferKind(address) {
|
|
326
|
+
if (address === 'workspace' || address === 'home' || address === 'me')
|
|
327
|
+
return 'home';
|
|
328
|
+
return address.includes('/') ? 'cloud' : 'local';
|
|
329
|
+
}
|
|
330
|
+
// Exposed for tests.
|
|
331
|
+
export const __testing = {
|
|
332
|
+
TARGET_FILE,
|
|
333
|
+
STATE_DIR,
|
|
334
|
+
};
|