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,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* beehaven compose — site-publishing command family.
|
|
3
|
+
*
|
|
4
|
+
* beehaven compose create [--name <name>]
|
|
5
|
+
* Creates a new composition in the user's workspace and writes
|
|
6
|
+
* `.beehaven.json` in the current directory linking them together.
|
|
7
|
+
* No build yet — use `push` when ready.
|
|
8
|
+
*
|
|
9
|
+
* beehaven compose push
|
|
10
|
+
* Reads `.beehaven.json` for compositionId. Detects the project's
|
|
11
|
+
* framework (Vite / Next static / Astro / SvelteKit-static / plain
|
|
12
|
+
* static). Runs the framework's build command. Uploads the built
|
|
13
|
+
* dist/ to R2 under compositions/<id>/build/ via the workspace's
|
|
14
|
+
* composition-scoped teleport flag. Updates composition metadata
|
|
15
|
+
* with source_type, entry_point, build_size, last_build_at.
|
|
16
|
+
*
|
|
17
|
+
* beehaven compose status
|
|
18
|
+
* Prints composition metadata, active share links, view counts.
|
|
19
|
+
*
|
|
20
|
+
* beehaven compose share [--password <pw>] [--expires <date>]
|
|
21
|
+
* [--allow <email,email>] [--immersive]
|
|
22
|
+
* Creates a share link. Prints the URL.
|
|
23
|
+
*
|
|
24
|
+
* beehaven compose rm
|
|
25
|
+
* Deletes composition + all share links + R2 assets. Confirms first.
|
|
26
|
+
*
|
|
27
|
+
* Requires an authed daemon connection (same as all `beehaven call`-
|
|
28
|
+
* backed commands). The CLI does the local FS work; the workspace DO
|
|
29
|
+
* handles the persistent state. Source code is not uploaded — only the
|
|
30
|
+
* built bundle (intentional — keeps R2 footprint small and avoids
|
|
31
|
+
* leaking .env.local / secrets / node_modules).
|
|
32
|
+
*/
|
|
33
|
+
import { spawnSync } from 'child_process';
|
|
34
|
+
import { readFileSync, writeFileSync, statSync, existsSync, readdirSync } from 'fs';
|
|
35
|
+
import { join, resolve as resolvePath, relative, basename, extname } from 'path';
|
|
36
|
+
import { rpcOnce } from '../ipc-client.js';
|
|
37
|
+
const LINK_FILE = '.beehaven.json';
|
|
38
|
+
// ── Link file (.beehaven.json) ───────────────────────────────────────
|
|
39
|
+
function loadLink(cwd) {
|
|
40
|
+
const path = join(cwd, LINK_FILE);
|
|
41
|
+
if (!existsSync(path))
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
const raw = readFileSync(path, 'utf-8');
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
if (!parsed?.compositionId)
|
|
47
|
+
return null;
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function saveLink(cwd, link) {
|
|
55
|
+
writeFileSync(join(cwd, LINK_FILE), JSON.stringify(link, null, 2) + '\n');
|
|
56
|
+
}
|
|
57
|
+
// ── Framework detection ──────────────────────────────────────────────
|
|
58
|
+
export function detectProject(cwd) {
|
|
59
|
+
const pkgPath = join(cwd, 'package.json');
|
|
60
|
+
if (existsSync(pkgPath)) {
|
|
61
|
+
let pkg = {};
|
|
62
|
+
try {
|
|
63
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
67
|
+
const hasScript = (name) => pkg.scripts && typeof pkg.scripts[name] === 'string';
|
|
68
|
+
// Vite — `vite.config.{ts,js,mjs}` + vite dep. Default output: dist/
|
|
69
|
+
if (hasConfig(cwd, 'vite.config') || deps.vite) {
|
|
70
|
+
return {
|
|
71
|
+
framework: 'vite',
|
|
72
|
+
buildCommand: hasScript('build') ? 'npm run build' : 'npx vite build',
|
|
73
|
+
distDir: 'dist',
|
|
74
|
+
entryPoint: 'index.html',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Next.js static export. `next.config.{ts,js,mjs}` WITH
|
|
78
|
+
// `output: 'export'` enables static mode → out/. If not set, Next
|
|
79
|
+
// needs a Node runtime which we don't support yet.
|
|
80
|
+
if (deps.next) {
|
|
81
|
+
const nextConfig = readIfExists(cwd, ['next.config.mjs', 'next.config.ts', 'next.config.js']);
|
|
82
|
+
if (nextConfig && /output\s*:\s*['"]export['"]/.test(nextConfig)) {
|
|
83
|
+
return {
|
|
84
|
+
framework: 'next-static',
|
|
85
|
+
buildCommand: hasScript('build') ? 'npm run build' : 'npx next build',
|
|
86
|
+
distDir: 'out',
|
|
87
|
+
entryPoint: 'index.html',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Non-static Next — can't host, bail with a helpful error.
|
|
91
|
+
throw new Error('Next.js is present but not configured for static export.\n' +
|
|
92
|
+
'Add `output: "export"` to next.config.{js,ts,mjs} and re-run, or use a build service that runs Node.');
|
|
93
|
+
}
|
|
94
|
+
// Astro — default output: dist/
|
|
95
|
+
if (deps.astro) {
|
|
96
|
+
return {
|
|
97
|
+
framework: 'astro',
|
|
98
|
+
buildCommand: hasScript('build') ? 'npm run build' : 'npx astro build',
|
|
99
|
+
distDir: 'dist',
|
|
100
|
+
entryPoint: 'index.html',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// SvelteKit with adapter-static → build/
|
|
104
|
+
if (deps['@sveltejs/kit']) {
|
|
105
|
+
const svelteConfig = readIfExists(cwd, ['svelte.config.js', 'svelte.config.ts']);
|
|
106
|
+
if (svelteConfig && /adapter-static/.test(svelteConfig)) {
|
|
107
|
+
return {
|
|
108
|
+
framework: 'sveltekit-static',
|
|
109
|
+
buildCommand: hasScript('build') ? 'npm run build' : 'npx vite build',
|
|
110
|
+
distDir: 'build',
|
|
111
|
+
entryPoint: 'index.html',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
throw new Error('SvelteKit is present but adapter-static is not configured.\n' +
|
|
115
|
+
'Switch to @sveltejs/adapter-static and re-run.');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// No package.json OR no known framework deps — treat as static site
|
|
119
|
+
// if there's an index.html at the root.
|
|
120
|
+
if (existsSync(join(cwd, 'index.html'))) {
|
|
121
|
+
return {
|
|
122
|
+
framework: 'static',
|
|
123
|
+
buildCommand: null,
|
|
124
|
+
distDir: '.',
|
|
125
|
+
entryPoint: 'index.html',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (readdirSync(cwd).some((f) => f.endsWith('.html'))) {
|
|
129
|
+
return {
|
|
130
|
+
framework: 'plain-html',
|
|
131
|
+
buildCommand: null,
|
|
132
|
+
distDir: '.',
|
|
133
|
+
entryPoint: readdirSync(cwd).find((f) => f.endsWith('.html')) || 'index.html',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
function hasConfig(cwd, stem) {
|
|
139
|
+
return ['.ts', '.js', '.mjs', '.cjs'].some((ext) => existsSync(join(cwd, stem + ext)));
|
|
140
|
+
}
|
|
141
|
+
function readIfExists(cwd, candidates) {
|
|
142
|
+
for (const c of candidates) {
|
|
143
|
+
const p = join(cwd, c);
|
|
144
|
+
if (existsSync(p)) {
|
|
145
|
+
try {
|
|
146
|
+
return readFileSync(p, 'utf-8');
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
// ── Walk + upload ────────────────────────────────────────────────────
|
|
156
|
+
function walkFiles(root, acc = [], ignore = new Set(['node_modules', '.git', '.beehaven.json'])) {
|
|
157
|
+
for (const name of readdirSync(root)) {
|
|
158
|
+
if (ignore.has(name))
|
|
159
|
+
continue;
|
|
160
|
+
const full = join(root, name);
|
|
161
|
+
const stat = statSync(full);
|
|
162
|
+
if (stat.isDirectory())
|
|
163
|
+
walkFiles(full, acc, ignore);
|
|
164
|
+
else
|
|
165
|
+
acc.push(full);
|
|
166
|
+
}
|
|
167
|
+
return acc;
|
|
168
|
+
}
|
|
169
|
+
async function uploadFile(file, relPath, compositionId) {
|
|
170
|
+
const INLINE_MAX_BYTES = 900 * 1024;
|
|
171
|
+
const content = readFileSync(file);
|
|
172
|
+
const size = content.length;
|
|
173
|
+
const mimeMap = {
|
|
174
|
+
html: 'text/html',
|
|
175
|
+
js: 'application/javascript',
|
|
176
|
+
mjs: 'application/javascript',
|
|
177
|
+
css: 'text/css',
|
|
178
|
+
json: 'application/json',
|
|
179
|
+
svg: 'image/svg+xml',
|
|
180
|
+
png: 'image/png',
|
|
181
|
+
jpg: 'image/jpeg',
|
|
182
|
+
jpeg: 'image/jpeg',
|
|
183
|
+
webp: 'image/webp',
|
|
184
|
+
gif: 'image/gif',
|
|
185
|
+
ico: 'image/x-icon',
|
|
186
|
+
mp3: 'audio/mpeg',
|
|
187
|
+
mp4: 'video/mp4',
|
|
188
|
+
wav: 'audio/wav',
|
|
189
|
+
woff: 'font/woff',
|
|
190
|
+
woff2: 'font/woff2',
|
|
191
|
+
ttf: 'font/ttf',
|
|
192
|
+
pdf: 'application/pdf',
|
|
193
|
+
zip: 'application/zip',
|
|
194
|
+
txt: 'text/plain',
|
|
195
|
+
md: 'text/markdown',
|
|
196
|
+
xml: 'application/xml',
|
|
197
|
+
};
|
|
198
|
+
const ext = extname(relPath).slice(1).toLowerCase();
|
|
199
|
+
const mimeType = mimeMap[ext] || 'application/octet-stream';
|
|
200
|
+
const key = `compositions/${compositionId}/build/${relPath}`;
|
|
201
|
+
try {
|
|
202
|
+
if (size <= INLINE_MAX_BYTES) {
|
|
203
|
+
const data = content.toString('base64');
|
|
204
|
+
const res = await rpcOnce('file-teleport-inline', {
|
|
205
|
+
key,
|
|
206
|
+
name: basename(relPath),
|
|
207
|
+
mimeType,
|
|
208
|
+
size,
|
|
209
|
+
data,
|
|
210
|
+
composition: true,
|
|
211
|
+
});
|
|
212
|
+
if (res?.error)
|
|
213
|
+
return { ok: false, size, error: res.error };
|
|
214
|
+
return { ok: true, size };
|
|
215
|
+
}
|
|
216
|
+
// Large-file path — get presigned URL, PUT directly, then notify.
|
|
217
|
+
const intent = await rpcOnce('get-upload-url', {
|
|
218
|
+
key,
|
|
219
|
+
contentType: mimeType,
|
|
220
|
+
maxSize: size,
|
|
221
|
+
});
|
|
222
|
+
if (!intent?.uploadUrl)
|
|
223
|
+
return { ok: false, size, error: intent?.error || 'no uploadUrl' };
|
|
224
|
+
let uploadUrl = intent.uploadUrl;
|
|
225
|
+
if (uploadUrl.startsWith('/')) {
|
|
226
|
+
// Prepend relay host from daemon config (same pattern as cmdTeleport).
|
|
227
|
+
let relayHost = 'https://relay.clearly.sh';
|
|
228
|
+
try {
|
|
229
|
+
const cfg = JSON.parse(readFileSync(join(process.env.HOME || '~', '.beehaven', 'config.json'), 'utf-8'));
|
|
230
|
+
if (cfg.endpoint)
|
|
231
|
+
relayHost = String(cfg.endpoint)
|
|
232
|
+
.replace(/^wss:/, 'https:')
|
|
233
|
+
.replace(/^ws:/, 'http:')
|
|
234
|
+
.replace(/\/$/, '');
|
|
235
|
+
}
|
|
236
|
+
catch { }
|
|
237
|
+
uploadUrl = relayHost + uploadUrl;
|
|
238
|
+
}
|
|
239
|
+
const putRes = await fetch(uploadUrl, {
|
|
240
|
+
method: 'PUT',
|
|
241
|
+
headers: { 'Content-Type': mimeType },
|
|
242
|
+
body: content,
|
|
243
|
+
});
|
|
244
|
+
if (!putRes.ok)
|
|
245
|
+
return { ok: false, size, error: `PUT ${putRes.status}` };
|
|
246
|
+
const confirm = await rpcOnce('file-uploaded', {
|
|
247
|
+
key,
|
|
248
|
+
name: basename(relPath),
|
|
249
|
+
mime: mimeType,
|
|
250
|
+
size,
|
|
251
|
+
composition: true,
|
|
252
|
+
});
|
|
253
|
+
if (confirm?.error)
|
|
254
|
+
return { ok: false, size, error: confirm.error };
|
|
255
|
+
return { ok: true, size };
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
return { ok: false, size, error: err?.message || String(err) };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ── Commands ─────────────────────────────────────────────────────────
|
|
262
|
+
export async function cmdComposeCreate(values) {
|
|
263
|
+
const cwd = process.cwd();
|
|
264
|
+
if (loadLink(cwd)) {
|
|
265
|
+
console.error(` This directory is already linked to a composition. Use \`beehaven compose push\` to update it, or delete ${LINK_FILE} first.`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
const name = values.name || basename(cwd);
|
|
269
|
+
try {
|
|
270
|
+
// Use the dedicated site-publish RPC so the composition lands with
|
|
271
|
+
// type='site' + source_type set (distinguishes pushed sites from
|
|
272
|
+
// legacy design compositions). Falls back to composition-create on
|
|
273
|
+
// older deployments for graceful rollout.
|
|
274
|
+
let res = await rpcOnce('compose-upload-init', { name });
|
|
275
|
+
if (!res?.id) {
|
|
276
|
+
res = await rpcOnce('composition-create', {
|
|
277
|
+
title: name,
|
|
278
|
+
data: JSON.stringify({ blocks: [] }),
|
|
279
|
+
type: 'site',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (!res?.id) {
|
|
283
|
+
console.error(` Failed to create composition: ${res?.error ?? 'unknown'}`);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
saveLink(cwd, { compositionId: res.id, name, createdAt: Date.now() });
|
|
287
|
+
console.log(` \u2713 Composition "${name}" created`);
|
|
288
|
+
console.log(` id: ${res.id}`);
|
|
289
|
+
console.log(` Linked to: ${cwd}`);
|
|
290
|
+
console.log('');
|
|
291
|
+
console.log(` Next: \`beehaven compose push\` to build + publish.`);
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
console.error(` Error: ${err?.message || err}`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
export async function cmdComposePush(_values) {
|
|
299
|
+
// Two callers: the CLI verb (`beehaven compose push`) reads
|
|
300
|
+
// `_values.dir` + `_values.composition` if present to support
|
|
301
|
+
// arbitrary dirs + explicit composition ids — the publish-landing
|
|
302
|
+
// verb uses this to upload Claude-generated builds without first
|
|
303
|
+
// running `compose create`. The legacy form (no overrides) falls
|
|
304
|
+
// back to cwd + .beehaven.json link file.
|
|
305
|
+
const cwd = String(_values?.dir || process.cwd());
|
|
306
|
+
const explicitCompId = String(_values?.composition || '').trim();
|
|
307
|
+
const link = explicitCompId
|
|
308
|
+
? { compositionId: explicitCompId, name: 'unnamed', createdAt: Date.now() }
|
|
309
|
+
: loadLink(cwd);
|
|
310
|
+
if (!link) {
|
|
311
|
+
console.error(` No composition linked. Run \`beehaven compose create\` first, or pass --composition <id> + --dir <path>.`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
// Detect project
|
|
315
|
+
let project;
|
|
316
|
+
try {
|
|
317
|
+
project = detectProject(cwd);
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
console.error(` ${err.message}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
if (!project) {
|
|
324
|
+
console.error(` No supported project detected in ${cwd}.\n Supported: Vite, Next (output: 'export'), Astro, SvelteKit (adapter-static), plain static HTML.`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
console.log(` Detected: ${project.framework}`);
|
|
328
|
+
// Build (if needed)
|
|
329
|
+
if (project.buildCommand) {
|
|
330
|
+
console.log(` Running: ${project.buildCommand}`);
|
|
331
|
+
const [cmd, ...args] = project.buildCommand.split(' ');
|
|
332
|
+
const res = spawnSync(cmd, args, { cwd, stdio: 'inherit' });
|
|
333
|
+
if (res.status !== 0) {
|
|
334
|
+
console.error(` Build failed with exit ${res.status}`);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Verify dist exists
|
|
339
|
+
const distPath = resolvePath(cwd, project.distDir);
|
|
340
|
+
if (!existsSync(distPath)) {
|
|
341
|
+
console.error(` Build output missing: ${distPath}`);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
const entryFullPath = join(distPath, project.entryPoint);
|
|
345
|
+
if (!existsSync(entryFullPath)) {
|
|
346
|
+
console.error(` Entry point missing: ${relative(cwd, entryFullPath)}`);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
// Walk + upload
|
|
350
|
+
const files = walkFiles(distPath);
|
|
351
|
+
let totalSize = 0;
|
|
352
|
+
let ok = 0, failed = 0;
|
|
353
|
+
console.log(` Uploading ${files.length} files...`);
|
|
354
|
+
for (const file of files) {
|
|
355
|
+
const relPath = relative(distPath, file);
|
|
356
|
+
const res = await uploadFile(file, relPath, link.compositionId);
|
|
357
|
+
if (res.ok) {
|
|
358
|
+
ok++;
|
|
359
|
+
totalSize += res.size;
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
failed++;
|
|
363
|
+
console.error(` \u2717 ${relPath}: ${res.error}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
console.log(` \u2713 Uploaded ${ok}/${files.length} files (${(totalSize / 1024).toFixed(1)}KB)`);
|
|
367
|
+
if (failed > 0) {
|
|
368
|
+
console.error(` ${failed} files failed — composition may be partial. Retry with \`beehaven compose push\`.`);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
// Stamp composition metadata (source_type, entry_point, build_size,
|
|
372
|
+
// last_build_at). Uses the dedicated finalize RPC; falls back to the
|
|
373
|
+
// legacy update-meta for older deployments.
|
|
374
|
+
try {
|
|
375
|
+
await rpcOnce('compose-upload-finalize', {
|
|
376
|
+
compositionId: link.compositionId,
|
|
377
|
+
totalBytes: totalSize,
|
|
378
|
+
entryPoint: project.entryPoint,
|
|
379
|
+
});
|
|
380
|
+
// compose-upload-init on replay sets source_type; idempotent refresh.
|
|
381
|
+
await rpcOnce('compose-upload-init', {
|
|
382
|
+
compositionId: link.compositionId,
|
|
383
|
+
sourceType: project.framework,
|
|
384
|
+
entryPoint: project.entryPoint,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
try {
|
|
389
|
+
await rpcOnce('composition-update-meta', {
|
|
390
|
+
compositionId: link.compositionId,
|
|
391
|
+
type: 'site',
|
|
392
|
+
source_type: project.framework,
|
|
393
|
+
entry_point: project.entryPoint,
|
|
394
|
+
build_size: totalSize,
|
|
395
|
+
last_build_at: Date.now(),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
/* non-blocking */
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
console.log('');
|
|
403
|
+
console.log(` Composition live at:`);
|
|
404
|
+
console.log(` https://clearly.sh/app/compositions/${link.compositionId}`);
|
|
405
|
+
console.log('');
|
|
406
|
+
console.log(` Create a public share link with:`);
|
|
407
|
+
console.log(` beehaven compose share --password <pw>`);
|
|
408
|
+
}
|
|
409
|
+
export async function cmdComposeStatus(_values) {
|
|
410
|
+
const cwd = process.cwd();
|
|
411
|
+
const link = loadLink(cwd);
|
|
412
|
+
if (!link) {
|
|
413
|
+
console.error(` No composition linked here. Run \`beehaven compose create\` first.`);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
console.log(` ${link.name}`);
|
|
417
|
+
console.log(` id: ${link.compositionId}`);
|
|
418
|
+
console.log(` linked: ${new Date(link.createdAt).toLocaleString()}`);
|
|
419
|
+
try {
|
|
420
|
+
const shares = await rpcOnce('share-link-list', { compositionId: link.compositionId });
|
|
421
|
+
const links = shares?.links ?? [];
|
|
422
|
+
console.log('');
|
|
423
|
+
console.log(` Share links: ${links.length}`);
|
|
424
|
+
for (const l of links) {
|
|
425
|
+
const lock = l.has_password ? '🔒' : '·';
|
|
426
|
+
const revoked = l.revoked_at ? ' (revoked)' : '';
|
|
427
|
+
const expired = l.expires_at && l.expires_at < Date.now() ? ' (expired)' : '';
|
|
428
|
+
console.log(` ${lock} /r/${l.id} — ${l.view_count} views${revoked}${expired}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
console.error(` (couldn't load share links: ${err?.message})`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
export async function cmdComposeShare(values) {
|
|
436
|
+
const cwd = process.cwd();
|
|
437
|
+
const link = loadLink(cwd);
|
|
438
|
+
if (!link) {
|
|
439
|
+
console.error(` No composition linked here. Run \`beehaven compose create\` first.`);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
const input = { compositionId: link.compositionId };
|
|
443
|
+
if (values.password)
|
|
444
|
+
input.password = String(values.password);
|
|
445
|
+
if (values.label)
|
|
446
|
+
input.label = String(values.label);
|
|
447
|
+
if (values.immersive)
|
|
448
|
+
input.immersive = true;
|
|
449
|
+
if (values.expires) {
|
|
450
|
+
const t = Date.parse(String(values.expires));
|
|
451
|
+
if (Number.isFinite(t))
|
|
452
|
+
input.expiresAt = t;
|
|
453
|
+
}
|
|
454
|
+
if (values.allow) {
|
|
455
|
+
const list = String(values.allow)
|
|
456
|
+
.split(',')
|
|
457
|
+
.map((s) => s.trim())
|
|
458
|
+
.filter(Boolean);
|
|
459
|
+
if (list.length > 0)
|
|
460
|
+
input.allowList = list;
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const res = await rpcOnce('share-link-create', input);
|
|
464
|
+
if (!res?.id) {
|
|
465
|
+
console.error(` Failed: ${res?.error ?? 'unknown'}`);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
console.log(` \u2713 Share link created`);
|
|
469
|
+
console.log(` https://clearly.sh/r/${res.id}`);
|
|
470
|
+
if (input.password)
|
|
471
|
+
console.log(` Password: ${input.password}`);
|
|
472
|
+
if (input.expiresAt)
|
|
473
|
+
console.log(` Expires: ${new Date(input.expiresAt).toLocaleString()}`);
|
|
474
|
+
if (input.allowList)
|
|
475
|
+
console.log(` Allow-list: ${input.allowList.join(', ')}`);
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
console.error(` Error: ${err?.message || err}`);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
export async function cmdComposeRm(values) {
|
|
483
|
+
const cwd = process.cwd();
|
|
484
|
+
const link = loadLink(cwd);
|
|
485
|
+
if (!link) {
|
|
486
|
+
console.error(` No composition linked here.`);
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
if (!values.force) {
|
|
490
|
+
console.error(` This will delete composition "${link.name}" and all its share links.`);
|
|
491
|
+
console.error(` Re-run with --force to confirm.`);
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
await rpcOnce('composition-delete', { id: link.compositionId });
|
|
496
|
+
// Remove link file last so we don't orphan the directory if the RPC fails
|
|
497
|
+
try {
|
|
498
|
+
writeFileSync(join(cwd, LINK_FILE), '', { flag: 'wx' });
|
|
499
|
+
}
|
|
500
|
+
catch { }
|
|
501
|
+
console.log(` \u2713 Composition deleted`);
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
console.error(` Error: ${err?.message || err}`);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `beehaven composition` (#47b) — bind a Clearly composition (canvas
|
|
3
|
+
* board) to a local repo, so a daemon-spawned coding agent works in the
|
|
4
|
+
* real codebase and shares a Claude Code project 1:1 with that repo.
|
|
5
|
+
*
|
|
6
|
+
* beehaven composition bind <compositionId> <repoPath>
|
|
7
|
+
* beehaven composition unbind <compositionId>
|
|
8
|
+
* beehaven composition list
|
|
9
|
+
*
|
|
10
|
+
* The binding lives in `~/.clearly/composition-repos.json` and is read
|
|
11
|
+
* fresh by the daemon on the next omni session for that composition — no
|
|
12
|
+
* daemon restart needed to (un)bind. See `composition-repos.ts` for the
|
|
13
|
+
* resolution rules (stale bindings fall back to the scratch dir).
|
|
14
|
+
*/
|
|
15
|
+
import { resolve as resolvePath } from 'node:path';
|
|
16
|
+
import { existsSync, statSync } from 'node:fs';
|
|
17
|
+
import { setCompositionRepo, listCompositionRepos, resolveBoundRepo, compositionRepoMapPath, } from '../composition-repos.js';
|
|
18
|
+
export async function cmdComposition(sub, positionals, values) {
|
|
19
|
+
switch (sub) {
|
|
20
|
+
case 'bind':
|
|
21
|
+
return cmdBind(positionals, values);
|
|
22
|
+
case 'unbind':
|
|
23
|
+
case 'unmap':
|
|
24
|
+
return cmdUnbind(positionals, values);
|
|
25
|
+
case 'list':
|
|
26
|
+
case 'ls':
|
|
27
|
+
case undefined:
|
|
28
|
+
case '':
|
|
29
|
+
return cmdList(values);
|
|
30
|
+
default:
|
|
31
|
+
console.error(` Unknown subcommand: ${sub}`);
|
|
32
|
+
console.error(' Usage: beehaven composition <bind|unbind|list> [args]');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function cmdBind(positionals, values) {
|
|
37
|
+
const compositionId = positionals[0];
|
|
38
|
+
const rawPath = positionals[1];
|
|
39
|
+
if (!compositionId || !rawPath) {
|
|
40
|
+
console.error(' Usage: beehaven composition bind <compositionId> <repoPath>');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const repoPath = resolvePath(rawPath);
|
|
45
|
+
if (!existsSync(repoPath) || !statSync(repoPath).isDirectory()) {
|
|
46
|
+
console.error(` ✗ Not a directory: ${repoPath}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const isGit = existsSync(resolvePath(repoPath, '.git'));
|
|
51
|
+
setCompositionRepo(compositionId, repoPath);
|
|
52
|
+
if (values.json) {
|
|
53
|
+
console.log(JSON.stringify({ compositionId, repoPath, isGit }, null, 2));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
console.log(` ✓ Bound composition ${compositionId}`);
|
|
57
|
+
console.log(` → ${repoPath}${isGit ? '' : ' (note: no .git — agent will still run here)'}`);
|
|
58
|
+
console.log(` The next Claude-model run on this board opens its agent in that repo,`);
|
|
59
|
+
console.log(` sharing the repo's Claude Code project (1:1). No daemon restart needed.`);
|
|
60
|
+
}
|
|
61
|
+
function cmdUnbind(positionals, values) {
|
|
62
|
+
const compositionId = positionals[0];
|
|
63
|
+
if (!compositionId) {
|
|
64
|
+
console.error(' Usage: beehaven composition unbind <compositionId>');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const had = resolveBoundRepo(compositionId);
|
|
69
|
+
setCompositionRepo(compositionId, null);
|
|
70
|
+
if (values.json) {
|
|
71
|
+
console.log(JSON.stringify({ compositionId, unbound: true, was: had }, null, 2));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.log(` ✓ Unbound composition ${compositionId}${had ? ` (was ${had})` : ''}`);
|
|
75
|
+
console.log(` Future runs fall back to ~/.clearly/compositions/${compositionId}/`);
|
|
76
|
+
}
|
|
77
|
+
function cmdList(values) {
|
|
78
|
+
const map = listCompositionRepos();
|
|
79
|
+
const entries = Object.entries(map);
|
|
80
|
+
if (values.json) {
|
|
81
|
+
console.log(JSON.stringify(map, null, 2));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (entries.length === 0) {
|
|
85
|
+
console.log(' No composition→repo bindings.');
|
|
86
|
+
console.log(' Bind one: beehaven composition bind <compositionId> <repoPath>');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
console.log(` Composition → repo bindings (${compositionRepoMapPath()})`);
|
|
90
|
+
for (const [id, path] of entries) {
|
|
91
|
+
const live = existsSync(path);
|
|
92
|
+
console.log(` ${id}`);
|
|
93
|
+
console.log(` → ${path}${live ? '' : ' ✗ missing — falls back to scratch dir'}`);
|
|
94
|
+
}
|
|
95
|
+
}
|