@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,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `beehaven voice` — push-to-talk REPL orchestrating existing hives.
|
|
3
|
+
*
|
|
4
|
+
* This command does NO direct audio handling. Every piece of the loop
|
|
5
|
+
* routes through beehaven hives:
|
|
6
|
+
*
|
|
7
|
+
* 1. user hits ENTER → call `recorder:record { source: 'mic' }`
|
|
8
|
+
* 2. user hits ENTER again → call `recorder:stop`
|
|
9
|
+
* 3. get the transcribed text from the recording result
|
|
10
|
+
* 4. forward the text to the connected DO via `ask`
|
|
11
|
+
* 5. speak the response via `avatar:speak { text }`
|
|
12
|
+
*
|
|
13
|
+
* That means voice mode works wherever the recorder + avatar hives
|
|
14
|
+
* are installed — no Node-level microphone or TTS libraries ship with
|
|
15
|
+
* the CLI, and the voice experience stays tied to whatever audio
|
|
16
|
+
* platform the hives themselves handle (today: macOS BlackHole +
|
|
17
|
+
* Kokoro TTS via the Clearly Dev Mac app bundle).
|
|
18
|
+
*
|
|
19
|
+
* ## Exit
|
|
20
|
+
* Ctrl+C or type `:quit` at the prompt.
|
|
21
|
+
*/
|
|
22
|
+
import { isDaemonRunning } from '../daemon-client.js';
|
|
23
|
+
import { appRpcOnce } from '../ipc-client.js';
|
|
24
|
+
import { cmdAsk } from './workspace.js';
|
|
25
|
+
import { color } from '../spinner.js';
|
|
26
|
+
import { createInterface } from 'readline';
|
|
27
|
+
async function callApp(building, action, input) {
|
|
28
|
+
return appRpcOnce(action, input ?? {}, { building, instanceId: 'live', timeout: 30_000 });
|
|
29
|
+
}
|
|
30
|
+
export async function cmdVoice(values) {
|
|
31
|
+
if (!(await isDaemonRunning())) {
|
|
32
|
+
console.error(color.red('✗') + ' Daemon not running. Start it: beehaven start --headless\n (or open the Hive Mac app, which runs one for you.)');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
// Probe for recorder + avatar hives. Graceful failure if either's missing.
|
|
36
|
+
const recorderOk = await probeHive('recorder', 'list-sources');
|
|
37
|
+
const avatarOk = await probeHive('avatar', 'emote');
|
|
38
|
+
if (!recorderOk) {
|
|
39
|
+
console.error(color.red('✗') + ' recorder hive not installed. Install via `beehaven install recorder`.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
if (!avatarOk) {
|
|
43
|
+
console.error(color.yellow('!') + ' avatar hive not installed — responses will be text-only.');
|
|
44
|
+
console.error(' Install via `beehaven install avatar` for spoken replies.');
|
|
45
|
+
}
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(` ${color.bold('Beehaven Voice')} — push-to-talk REPL`);
|
|
48
|
+
console.log(` ${color.dim('Press ENTER to start recording, ENTER again to stop. Type :quit to exit.')}`);
|
|
49
|
+
console.log('');
|
|
50
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
51
|
+
let recording = false;
|
|
52
|
+
const ask = async (transcript) => {
|
|
53
|
+
try {
|
|
54
|
+
// Delegate to existing cmdAsk so auto-routing to the connected
|
|
55
|
+
// building stays in one place. Capture its stdout by swapping
|
|
56
|
+
// console.log — cheesy but avoids duplicating the RPC plumbing.
|
|
57
|
+
let captured = '';
|
|
58
|
+
const origLog = console.log.bind(console);
|
|
59
|
+
console.log = (...args) => {
|
|
60
|
+
captured += args.join(' ') + '\n';
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
await cmdAsk(transcript, values);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
console.log = origLog;
|
|
67
|
+
}
|
|
68
|
+
return captured.trim() || null;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.error(color.red('✗') + ` ask failed: ${err?.message ?? err}`);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const record = async () => {
|
|
76
|
+
try {
|
|
77
|
+
await callApp('recorder', 'record', { source: 'mic' });
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
console.error(color.red('✗') + ` recorder:record failed: ${err?.message ?? err}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const stop = async () => {
|
|
86
|
+
try {
|
|
87
|
+
const result = (await callApp('recorder', 'stop', {}));
|
|
88
|
+
const transcript = result?.transcript ?? result?.text ?? '';
|
|
89
|
+
return typeof transcript === 'string' && transcript.length > 0 ? transcript : null;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
console.error(color.red('✗') + ` recorder:stop failed: ${err?.message ?? err}`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const speak = async (text) => {
|
|
97
|
+
if (!avatarOk) {
|
|
98
|
+
console.log(text);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await callApp('avatar', 'speak', { text });
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
console.error(color.yellow('!') + ` avatar:speak failed (printing text instead): ${err?.message ?? err}`);
|
|
106
|
+
console.log(text);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
process.on('SIGINT', () => {
|
|
110
|
+
rl.close();
|
|
111
|
+
console.log('\n Goodbye.');
|
|
112
|
+
process.exit(0);
|
|
113
|
+
});
|
|
114
|
+
rl.on('line', async (line) => {
|
|
115
|
+
const trimmed = line.trim();
|
|
116
|
+
if (trimmed === ':quit' || trimmed === ':q') {
|
|
117
|
+
rl.close();
|
|
118
|
+
console.log(' Goodbye.');
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
if (!recording) {
|
|
122
|
+
recording = true;
|
|
123
|
+
console.log(color.dim(' ● recording… (press ENTER again to stop)'));
|
|
124
|
+
await record();
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
recording = false;
|
|
128
|
+
const transcript = await stop();
|
|
129
|
+
if (!transcript) {
|
|
130
|
+
console.log(color.dim(' (no speech detected)'));
|
|
131
|
+
rl.prompt();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
console.log(color.bold(' you: ') + transcript);
|
|
135
|
+
const response = await ask(transcript);
|
|
136
|
+
if (response) {
|
|
137
|
+
console.log(color.bold(' ai: ') + response);
|
|
138
|
+
await speak(response);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
rl.prompt();
|
|
142
|
+
});
|
|
143
|
+
rl.setPrompt(' ▸ ');
|
|
144
|
+
rl.prompt();
|
|
145
|
+
}
|
|
146
|
+
// ── Helper — probe whether a hive is installed ──────────────────────────
|
|
147
|
+
async function probeHive(appName, probeAction) {
|
|
148
|
+
try {
|
|
149
|
+
// Try a read-only action that every hive handles. If the daemon
|
|
150
|
+
// responds with a known-app error, the hive isn't installed.
|
|
151
|
+
await callApp(appName, probeAction, {});
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
const msg = String(err?.message ?? err ?? '').toLowerCase();
|
|
156
|
+
// If the error is "action not found," the hive IS installed — just
|
|
157
|
+
// didn't have this specific probe action. Treat as success.
|
|
158
|
+
if (msg.includes('unknown action') || msg.includes('action not found'))
|
|
159
|
+
return true;
|
|
160
|
+
if (msg.includes('not installed') || msg.includes('no such app') || msg.includes('not found'))
|
|
161
|
+
return false;
|
|
162
|
+
// Ambiguous — assume installed and let the real call fail loudly
|
|
163
|
+
// if it's actually broken.
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `beehaven watch git` — capture a local git diff and land it on the Clearly
|
|
3
|
+
* spatial canvas as a DiffNode, so an agent's (or your own) code changes show up
|
|
4
|
+
* on the composition the user is working in.
|
|
5
|
+
*
|
|
6
|
+
* The diff is sent to the connected DO via the LOCKED canvas contract:
|
|
7
|
+
*
|
|
8
|
+
* action: canvas-act
|
|
9
|
+
* input: { compositionId, action:'canvas.add-diff', args:{ diff, title?, commit? } }
|
|
10
|
+
*
|
|
11
|
+
* Routing reuses the exact `call` path: resolveBuilding(action, input, --building)
|
|
12
|
+
* then rpcOnce(resolved.action, resolved.input, timeout). The cli.ts `watch`
|
|
13
|
+
* dispatch derives --building from the connected target (default home) before
|
|
14
|
+
* calling us, so a bare `beehaven connect home && beehaven watch git <id>` lands
|
|
15
|
+
* on home — the DO that holds compositions.
|
|
16
|
+
*
|
|
17
|
+
* Modes:
|
|
18
|
+
* beehaven watch git <compId> # working-tree diff (git diff HEAD), one-shot
|
|
19
|
+
* beehaven watch git --staged # staged diff (git diff --staged)
|
|
20
|
+
* beehaven watch git --commit <sha> # that commit's diff + subject as title
|
|
21
|
+
* beehaven watch git --range <a>..<b> # diff between two refs
|
|
22
|
+
* beehaven watch git --watch # poll HEAD; push each NEW commit's diff
|
|
23
|
+
*
|
|
24
|
+
* The composition id comes from --composition / --comp / -c, or positionals[0].
|
|
25
|
+
* With none given we default to the most-recently-updated composition (via
|
|
26
|
+
* canvas-list-compositions); pass --composition <id> to be explicit.
|
|
27
|
+
*
|
|
28
|
+
* The action it calls (canvas.add-diff) ships with the worker in a parallel
|
|
29
|
+
* piece of this feature — until that deploys the call returns "Unknown action",
|
|
30
|
+
* which is expected. The wiring is correct against the contract.
|
|
31
|
+
*/
|
|
32
|
+
import { execFileSync } from 'node:child_process';
|
|
33
|
+
import { resolveBuilding, getTimeout } from './workspace.js';
|
|
34
|
+
import { rpcOnce } from '../ipc-client.js';
|
|
35
|
+
// ── git helpers ────────────────────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Run a git command in the CWD, returning trimmed stdout. Throws GitError on
|
|
38
|
+
* failure (non-git dir, bad ref, git not installed) so callers can fail with a
|
|
39
|
+
* friendly message instead of a stack trace.
|
|
40
|
+
*/
|
|
41
|
+
function git(args) {
|
|
42
|
+
try {
|
|
43
|
+
return execFileSync('git', args, {
|
|
44
|
+
cwd: process.cwd(),
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
// A whole-repo diff can be large; lift the default 1MB buffer. It's just
|
|
47
|
+
// a string in the RPC, so a big diff is fine — we only need not to choke
|
|
48
|
+
// capturing it.
|
|
49
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
50
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
51
|
+
}).toString();
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
const stderr = (err?.stderr ? err.stderr.toString() : '') || err?.message || 'git failed';
|
|
55
|
+
throw new GitError(stderr.trim());
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
class GitError extends Error {
|
|
59
|
+
}
|
|
60
|
+
/** Best-effort current branch name (empty string if detached / unavailable). */
|
|
61
|
+
function currentBranch() {
|
|
62
|
+
try {
|
|
63
|
+
const b = git(['rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
64
|
+
return b === 'HEAD' ? '' : b;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Confirm we're inside a git work tree, or throw a clear GitError. */
|
|
71
|
+
function assertGitRepo() {
|
|
72
|
+
let inside = '';
|
|
73
|
+
try {
|
|
74
|
+
inside = git(['rev-parse', '--is-inside-work-tree']).trim();
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
throw new GitError(`not a git repository (or git is unavailable): ${err?.message ?? ''}`.trim());
|
|
78
|
+
}
|
|
79
|
+
if (inside !== 'true')
|
|
80
|
+
throw new GitError('not inside a git work tree');
|
|
81
|
+
}
|
|
82
|
+
/** Short SHA for a ref (falls back to the raw ref string on failure). */
|
|
83
|
+
function shortSha(ref) {
|
|
84
|
+
try {
|
|
85
|
+
return git(['rev-parse', '--short', ref]).trim();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return ref;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Subject line of a commit (empty string on failure). */
|
|
92
|
+
function commitSubject(ref) {
|
|
93
|
+
try {
|
|
94
|
+
return git(['log', '-1', '--format=%s', ref]).trim();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Count of files touched by a diff string (cheap heuristic on the patch). */
|
|
101
|
+
function fileCount(diff) {
|
|
102
|
+
const m = diff.match(/^diff --git /gm);
|
|
103
|
+
return m ? m.length : 0;
|
|
104
|
+
}
|
|
105
|
+
// ── help ───────────────────────────────────────────────────────────────────
|
|
106
|
+
function printWatchGitHelp() {
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(' beehaven watch git — push a local git diff onto the Clearly canvas');
|
|
109
|
+
console.log(' ─────────────────────────────────────────────────────────────────');
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(' Usage:');
|
|
112
|
+
console.log(' beehaven watch git [<compositionId>] [mode-flag]');
|
|
113
|
+
console.log('');
|
|
114
|
+
console.log(' Composition target (required — one of):');
|
|
115
|
+
console.log(' <compositionId> positional');
|
|
116
|
+
console.log(' --composition <id> | --comp <id> | -c <id>');
|
|
117
|
+
console.log(' (none) defaults to your most-recently-updated');
|
|
118
|
+
console.log(' composition; ambiguous → pass --composition');
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(' Modes (default = working-tree diff, one-shot):');
|
|
121
|
+
console.log(' (default) git diff HEAD (unstaged + working changes)');
|
|
122
|
+
console.log(' --staged git diff --staged');
|
|
123
|
+
console.log(" --commit <sha> that commit's diff, subject as the title");
|
|
124
|
+
console.log(' --range <a>..<b> git diff <a> <b>');
|
|
125
|
+
console.log(" --watch poll HEAD ~2s; push each NEW commit's diff");
|
|
126
|
+
console.log(' (runs until Ctrl-C)');
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log(' Examples:');
|
|
129
|
+
console.log(' beehaven connect home');
|
|
130
|
+
console.log(' beehaven watch git # latest comp, current working changes');
|
|
131
|
+
console.log(' beehaven watch git comp_abc123 --staged');
|
|
132
|
+
console.log(' beehaven watch git --commit HEAD~1 -c comp_abc123');
|
|
133
|
+
console.log(' beehaven watch git --range main..feature');
|
|
134
|
+
console.log(' beehaven watch git --watch # live: each new commit → a diff card');
|
|
135
|
+
console.log('');
|
|
136
|
+
}
|
|
137
|
+
// ── composition resolution ──────────────────────────────────────────────────
|
|
138
|
+
/**
|
|
139
|
+
* Resolve which composition the diff lands on. Precedence:
|
|
140
|
+
* 1. explicit flag (--composition / --comp / -c) or positionals[0]
|
|
141
|
+
* 2. the most-recently-updated composition (canvas-list-compositions returns
|
|
142
|
+
* rows ordered by updated_at DESC, so compositions[0] is newest)
|
|
143
|
+
* Throws on an ambiguous/empty default so we never guess wrong.
|
|
144
|
+
*/
|
|
145
|
+
async function resolveCompositionId(positionals, values) {
|
|
146
|
+
const explicit = typeof values.composition === 'string' && values.composition.trim()
|
|
147
|
+
? values.composition.trim()
|
|
148
|
+
: typeof values.comp === 'string' && values.comp.trim()
|
|
149
|
+
? values.comp.trim()
|
|
150
|
+
: typeof values.c === 'string' && values.c.trim()
|
|
151
|
+
? values.c.trim()
|
|
152
|
+
: positionals[0] && positionals[0].trim()
|
|
153
|
+
? positionals[0].trim()
|
|
154
|
+
: '';
|
|
155
|
+
if (explicit)
|
|
156
|
+
return explicit;
|
|
157
|
+
// No id given — try the most-recent composition on the connected DO.
|
|
158
|
+
const resolved = resolveBuilding('canvas-list-compositions', { limit: 10 }, values.building);
|
|
159
|
+
let raw;
|
|
160
|
+
try {
|
|
161
|
+
raw = await rpcOnce(resolved.action, resolved.input, getTimeout(values));
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
throw new Error(`Could not list compositions to pick a default (${err?.message ?? 'rpc failed'}).\n` +
|
|
165
|
+
` Pass one explicitly: beehaven watch git --composition <id>`);
|
|
166
|
+
}
|
|
167
|
+
// Unwrap the admin:*-rpc proxy envelope when --building routed the call.
|
|
168
|
+
const result = raw?.type === 'ack' && raw?.result !== undefined
|
|
169
|
+
? raw.result
|
|
170
|
+
: raw?.result !== undefined && values.building
|
|
171
|
+
? raw.result
|
|
172
|
+
: raw;
|
|
173
|
+
if (raw?.type === 'error' || result?.error) {
|
|
174
|
+
const msg = raw?.message ?? result?.error ?? 'unknown error';
|
|
175
|
+
throw new Error(`Could not list compositions to pick a default (${msg}).\n` +
|
|
176
|
+
` Pass one explicitly: beehaven watch git --composition <id>`);
|
|
177
|
+
}
|
|
178
|
+
const comps = Array.isArray(result?.compositions)
|
|
179
|
+
? result.compositions
|
|
180
|
+
: [];
|
|
181
|
+
if (comps.length === 0) {
|
|
182
|
+
throw new Error('No compositions found on this workspace to default to.\n' +
|
|
183
|
+
' Create one in the app, then pass: beehaven watch git --composition <id>');
|
|
184
|
+
}
|
|
185
|
+
const picked = comps[0];
|
|
186
|
+
if (!picked?.id) {
|
|
187
|
+
throw new Error('Could not determine a default composition — pass --composition <id>.');
|
|
188
|
+
}
|
|
189
|
+
console.error(`\x1b[2m→ no composition given; defaulting to most-recent: ${picked.title ?? picked.id} (${picked.id})\x1b[0m`);
|
|
190
|
+
return picked.id;
|
|
191
|
+
}
|
|
192
|
+
/** Capture the diff + title for the chosen one-shot mode. */
|
|
193
|
+
function captureOneShot(values) {
|
|
194
|
+
// --commit <sha>: that single commit's patch, its subject as the title.
|
|
195
|
+
const commit = typeof values.commit === 'string' && values.commit.trim() ? values.commit.trim() : '';
|
|
196
|
+
if (commit) {
|
|
197
|
+
const sha = shortSha(commit);
|
|
198
|
+
const subject = commitSubject(commit);
|
|
199
|
+
// `git show <sha>` includes the patch; restrict to the patch with diff form
|
|
200
|
+
// so the node renders a clean diff (not the commit header twice).
|
|
201
|
+
const diff = git(['diff', `${commit}^`, commit]);
|
|
202
|
+
return { diff, title: subject || `Commit ${sha}`, commit: sha };
|
|
203
|
+
}
|
|
204
|
+
// --range <a>..<b> (also tolerates "a b" / "a..b").
|
|
205
|
+
const range = typeof values.range === 'string' && values.range.trim() ? values.range.trim() : '';
|
|
206
|
+
if (range) {
|
|
207
|
+
const parts = range.includes('..')
|
|
208
|
+
? range.split('..').filter(Boolean)
|
|
209
|
+
: range.split(/\s+/).filter(Boolean);
|
|
210
|
+
if (parts.length !== 2) {
|
|
211
|
+
throw new GitError(`--range expects <a>..<b> (got "${range}")`);
|
|
212
|
+
}
|
|
213
|
+
const [a, b] = parts;
|
|
214
|
+
const diff = git(['diff', a, b]);
|
|
215
|
+
return { diff, title: `Diff ${shortSha(a)}..${shortSha(b)}` };
|
|
216
|
+
}
|
|
217
|
+
// --staged: the index against HEAD.
|
|
218
|
+
if (values.staged === true) {
|
|
219
|
+
const diff = git(['diff', '--staged']);
|
|
220
|
+
const stat = git(['diff', '--staged', '--shortstat']).trim();
|
|
221
|
+
const branch = currentBranch();
|
|
222
|
+
return {
|
|
223
|
+
diff,
|
|
224
|
+
title: `Staged changes${branch ? ` — ${branch}` : ''}${stat ? ` (${stat})` : ''}`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// Default: working-tree changes (unstaged + tracked modifications) vs HEAD.
|
|
228
|
+
const diff = git(['diff', 'HEAD']);
|
|
229
|
+
const stat = git(['diff', 'HEAD', '--shortstat']).trim();
|
|
230
|
+
const branch = currentBranch();
|
|
231
|
+
return {
|
|
232
|
+
diff,
|
|
233
|
+
title: `Working changes${branch ? ` — ${branch}` : ''}${stat ? ` (${stat})` : ''}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
// ── push ────────────────────────────────────────────────────────────────────
|
|
237
|
+
/**
|
|
238
|
+
* Send one diff to the canvas via canvas-act → canvas.add-diff, reusing the
|
|
239
|
+
* exact `call` path (resolveBuilding + rpcOnce). Returns the unwrapped value or
|
|
240
|
+
* throws on a proxied/transport error. Skips empty diffs (no empty card).
|
|
241
|
+
*/
|
|
242
|
+
async function pushDiff(compositionId, cap, values) {
|
|
243
|
+
if (!cap.diff || !cap.diff.trim()) {
|
|
244
|
+
return { skipped: true };
|
|
245
|
+
}
|
|
246
|
+
const args = { diff: cap.diff };
|
|
247
|
+
if (cap.title)
|
|
248
|
+
args.title = cap.title;
|
|
249
|
+
if (cap.commit)
|
|
250
|
+
args.commit = cap.commit;
|
|
251
|
+
const input = { compositionId, action: 'canvas.add-diff', args };
|
|
252
|
+
const resolved = resolveBuilding('canvas-act', input, values.building);
|
|
253
|
+
if (!values.json) {
|
|
254
|
+
const target = values.building ? values.building : 'home';
|
|
255
|
+
console.error(`\x1b[2m→ ${target} canvas-act canvas.add-diff (${fileCount(cap.diff)} files, ${cap.diff.length}B)\x1b[0m`);
|
|
256
|
+
}
|
|
257
|
+
const raw = await rpcOnce(resolved.action, resolved.input, getTimeout(values));
|
|
258
|
+
if (raw?.type === 'error') {
|
|
259
|
+
throw new Error(raw.message ?? 'canvas-act failed');
|
|
260
|
+
}
|
|
261
|
+
const value = raw?.type === 'ack' && raw?.result !== undefined
|
|
262
|
+
? raw.result
|
|
263
|
+
: raw?.result !== undefined && values.building
|
|
264
|
+
? raw.result
|
|
265
|
+
: raw;
|
|
266
|
+
return { skipped: false, value };
|
|
267
|
+
}
|
|
268
|
+
// ── entrypoint ──────────────────────────────────────────────────────────────
|
|
269
|
+
export async function cmdWatchGit(positionals, values) {
|
|
270
|
+
// Subcommand help — reachable as `watch git --help`, `-h`, or `watch git help`.
|
|
271
|
+
if (values.help === true || positionals[0] === 'help') {
|
|
272
|
+
printWatchGitHelp();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// Validate we're in a git repo up front — every mode needs it.
|
|
276
|
+
try {
|
|
277
|
+
assertGitRepo();
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
console.error(` ${err?.message ?? 'not a git repository'}`);
|
|
281
|
+
console.error(' Run beehaven watch git from inside a git repository.');
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
let compositionId;
|
|
285
|
+
try {
|
|
286
|
+
compositionId = await resolveCompositionId(positionals, values);
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
console.error(` ${err?.message ?? 'could not resolve a composition'}`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
return; // unreachable, keeps TS happy about compositionId being assigned
|
|
292
|
+
}
|
|
293
|
+
// ── --watch: poll HEAD, push each NEW commit's diff until Ctrl-C ──────────
|
|
294
|
+
if (values.watch === true) {
|
|
295
|
+
let prev = '';
|
|
296
|
+
try {
|
|
297
|
+
prev = git(['rev-parse', 'HEAD']).trim();
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// Empty repo (no commits yet) — start from nothing; the first commit fires a push.
|
|
301
|
+
prev = '';
|
|
302
|
+
}
|
|
303
|
+
console.error(` Watching HEAD for new commits → composition ${compositionId} (Ctrl-C to stop)`);
|
|
304
|
+
if (prev)
|
|
305
|
+
console.error(`\x1b[2m baseline: ${shortSha(prev)}\x1b[0m`);
|
|
306
|
+
let stopped = false;
|
|
307
|
+
const onSig = () => {
|
|
308
|
+
stopped = true;
|
|
309
|
+
console.error('\n stopped.');
|
|
310
|
+
process.exit(0);
|
|
311
|
+
};
|
|
312
|
+
process.on('SIGINT', onSig);
|
|
313
|
+
process.on('SIGTERM', onSig);
|
|
314
|
+
// Simple ~2s poll loop. Cheap (one rev-parse) and robust — no .git/logs
|
|
315
|
+
// watching needed, and it survives rebases/amends (any HEAD move fires).
|
|
316
|
+
while (!stopped) {
|
|
317
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
318
|
+
if (stopped)
|
|
319
|
+
break;
|
|
320
|
+
let head = '';
|
|
321
|
+
try {
|
|
322
|
+
head = git(['rev-parse', 'HEAD']).trim();
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
continue; // transient (mid-rebase, etc.) — try again next tick
|
|
326
|
+
}
|
|
327
|
+
if (!head || head === prev)
|
|
328
|
+
continue;
|
|
329
|
+
// New commit(s). Diff the span prev..head (or just the new commit when
|
|
330
|
+
// there was no baseline). Title = the newest commit's subject.
|
|
331
|
+
const sha = shortSha(head);
|
|
332
|
+
const subject = commitSubject(head);
|
|
333
|
+
let diff = '';
|
|
334
|
+
try {
|
|
335
|
+
diff = prev ? git(['diff', prev, head]) : git(['show', head]);
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
console.error(` ! could not capture diff for ${sha}: ${err?.message ?? 'git failed'}`);
|
|
339
|
+
prev = head;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const cap = { diff, title: subject || `Commit ${sha}`, commit: sha };
|
|
343
|
+
try {
|
|
344
|
+
const res = await pushDiff(compositionId, cap, values);
|
|
345
|
+
if (res.skipped) {
|
|
346
|
+
console.error(` (no changes in ${sha} — skipped)`);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
console.log(` ✓ pushed ${fileCount(diff)} files for ${sha} "${subject || '(no subject)'}"`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
console.error(` ! push failed for ${sha}: ${err?.message ?? 'unknown error'}`);
|
|
354
|
+
}
|
|
355
|
+
prev = head;
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// ── one-shot ─────────────────────────────────────────────────────────────
|
|
360
|
+
let cap;
|
|
361
|
+
try {
|
|
362
|
+
cap = captureOneShot(values);
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
console.error(` ${err?.message ?? 'could not capture a diff'}`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (!cap.diff || !cap.diff.trim()) {
|
|
370
|
+
console.log(' no changes');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const res = await pushDiff(compositionId, cap, values);
|
|
375
|
+
if (res.skipped) {
|
|
376
|
+
console.log(' no changes');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (values.json) {
|
|
380
|
+
console.log(JSON.stringify(res.value, null, 2));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (res.value && typeof res.value === 'object') {
|
|
384
|
+
console.log(JSON.stringify(res.value, null, 2));
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
console.log(res.value);
|
|
388
|
+
}
|
|
389
|
+
console.error(` ✓ pushed ${fileCount(cap.diff)} files — "${cap.title}" → composition ${compositionId}`);
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
console.error(JSON.stringify({ error: err?.message ?? 'unknown error' }, null, 2));
|
|
393
|
+
const s = String(err?.message ?? '').toLowerCase();
|
|
394
|
+
if (s.includes('unknown action')) {
|
|
395
|
+
console.error('');
|
|
396
|
+
console.error("\x1b[2m canvas.add-diff isn't on the deployed worker yet — this is expected until");
|
|
397
|
+
console.error(' the backend piece ships. The wiring is correct against the contract.\x1b[0m');
|
|
398
|
+
}
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
}
|