@thecolonylab/hivemind 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bin/hivemind.js +2 -0
- package/dist/browser/session.js +21 -0
- package/dist/cli.js +78 -0
- package/dist/commands/apply.js +33 -0
- package/dist/commands/coverage.js +98 -0
- package/dist/commands/coverageLocal.js +60 -0
- package/dist/commands/create.js +40 -0
- package/dist/commands/dashboard.js +12 -0
- package/dist/commands/explore.js +34 -0
- package/dist/commands/init.js +50 -0
- package/dist/commands/login.js +20 -0
- package/dist/commands/remoteExecutor.js +188 -0
- package/dist/commands/report.js +44 -0
- package/dist/commands/run.js +171 -0
- package/dist/commands/runLocal.js +99 -0
- package/dist/config.js +37 -0
- package/dist/credentials.js +24 -0
- package/dist/report/artifacts.js +48 -0
- package/dist/report/html.js +299 -0
- package/dist/report/prComment.js +50 -0
- package/dist/report/terminal.js +21 -0
- package/dist/serverClient.js +57 -0
- package/dist/testFile/discover.js +19 -0
- package/dist/testFile/listExisting.js +24 -0
- package/dist/testFile/loadAllTests.js +31 -0
- package/dist/testFile/parse.js +91 -0
- package/dist/testFile/resolve.js +65 -0
- package/dist/testFile/select.js +39 -0
- package/dist/testFile/types.js +1 -0
- package/dist/testFile/write.js +37 -0
- package/dist/types.js +4 -0
- package/package.json +35 -0
- package/skill/AGENTS.md.template +17 -0
- package/skill/hivemind-cli/SKILL.md +142 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hivemind
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/hivemind.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
const VIEWPORT = { width: 1280, height: 800 };
|
|
3
|
+
/** No video recording — evidence is per-step base64 screenshots sent back to the server, not a
|
|
4
|
+
* screen recording. */
|
|
5
|
+
export async function openSession(opts = {}) {
|
|
6
|
+
const browser = await chromium.launch({ headless: !opts.headed });
|
|
7
|
+
const context = await browser.newContext({
|
|
8
|
+
viewport: VIEWPORT,
|
|
9
|
+
locale: 'en-US',
|
|
10
|
+
});
|
|
11
|
+
const page = await context.newPage();
|
|
12
|
+
return {
|
|
13
|
+
browser,
|
|
14
|
+
context,
|
|
15
|
+
page,
|
|
16
|
+
close: async () => {
|
|
17
|
+
await context.close();
|
|
18
|
+
await browser.close();
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { runCommand } from './commands/run.js';
|
|
5
|
+
import { createCommand } from './commands/create.js';
|
|
6
|
+
import { reportCommand, historyCommand } from './commands/report.js';
|
|
7
|
+
import { dashboardCommand } from './commands/dashboard.js';
|
|
8
|
+
import { exploreCommand } from './commands/explore.js';
|
|
9
|
+
import { coverageCommand } from './commands/coverage.js';
|
|
10
|
+
import { applyCommand } from './commands/apply.js';
|
|
11
|
+
import { initCommand } from './commands/init.js';
|
|
12
|
+
import { loginCommand } from './commands/login.js';
|
|
13
|
+
const program = new Command();
|
|
14
|
+
program
|
|
15
|
+
.name('hivemind')
|
|
16
|
+
.description('Agentic E2E testing. This CLI is the Forager: it runs on your machine and executes exactly ' +
|
|
17
|
+
'what the Hivemind brain tells it to. Describe a flow in plain English — the server drives ' +
|
|
18
|
+
'a real browser and reports pass/fail. No LLM key needed on your machine.')
|
|
19
|
+
.version('0.1.0');
|
|
20
|
+
program
|
|
21
|
+
.command('login <key>')
|
|
22
|
+
.description('Store an API key (generate one from the dashboard\'s Settings page) so this machine is signed in')
|
|
23
|
+
.action(loginCommand);
|
|
24
|
+
program
|
|
25
|
+
.command('init')
|
|
26
|
+
.description('Set up hivemind in this project — hive/tests/, hivemind.config.json, AGENTS.md, and the agent skill file')
|
|
27
|
+
.action(initCommand);
|
|
28
|
+
program
|
|
29
|
+
.command('run <target>')
|
|
30
|
+
.description('Run a test file or every test file in a folder — cloud execution by default')
|
|
31
|
+
.option('--url <url>', 'Target URL (overrides the test file and config)')
|
|
32
|
+
.option('--env <name>', 'Named environment from hivemind.config.json')
|
|
33
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
34
|
+
.option('--changed [base]', 'Only run tests tagged for what changed vs. this base ref (default: main)')
|
|
35
|
+
.option('--local', 'Run the browser on this machine instead of the cloud server — required for localhost/private URLs, needs Playwright browsers installed locally')
|
|
36
|
+
.option('--headed', 'Local mode only: show the browser window instead of running headless')
|
|
37
|
+
.option('--concurrency <n>', 'Swarm mode: run up to n tests at once instead of one at a time (default 1)', '1')
|
|
38
|
+
.option('--pr-comment <path>', 'Write a PR-comment-ready markdown report to this path (post it yourself, e.g. via `gh pr comment --body-file`)')
|
|
39
|
+
.action(runCommand);
|
|
40
|
+
program
|
|
41
|
+
.command('create <request>')
|
|
42
|
+
.description('Turn a plain-English request into a saved test file')
|
|
43
|
+
.option('--url <url>', 'URL to save into the test file')
|
|
44
|
+
.option('--out <dir>', 'Output directory (default hive/tests)')
|
|
45
|
+
.action(createCommand);
|
|
46
|
+
program
|
|
47
|
+
.command('explore <url>')
|
|
48
|
+
.description('Send a Scout: crawls the site for real (follows links), maps pages, writes starter tests for what it finds')
|
|
49
|
+
.option('--out <dir>', 'Output directory (default hive/tests/generated)')
|
|
50
|
+
.action(exploreCommand);
|
|
51
|
+
program
|
|
52
|
+
.command('coverage')
|
|
53
|
+
.description('Reads the real git diff, checks which known pages are affected, reports gaps and drafts tests for them — nothing unaffected gets touched')
|
|
54
|
+
.option('--changed [base]', 'Base ref to diff against (default: main)')
|
|
55
|
+
.option('--out <dir>', 'Output directory for drafted tests (default hive/tests/generated)')
|
|
56
|
+
.option('--local', 'Run the browser on this machine instead of the cloud server — use this in CI, where the PR\'s changes usually aren\'t deployed anywhere the server could reach')
|
|
57
|
+
.option('--headed', 'Local mode only: show the browser window instead of running headless')
|
|
58
|
+
.option('--pr-comment <path>', 'Write a PR-comment-ready markdown report to this path (post it yourself, e.g. via `gh pr comment --body-file`)')
|
|
59
|
+
.action(coverageCommand);
|
|
60
|
+
program
|
|
61
|
+
.command('apply <code>')
|
|
62
|
+
.description('Pull down a test coverage drafted for a gap — the short code (e.g. amber-fern-cove) is shown in the PR comment')
|
|
63
|
+
.option('--out <dir>', 'Output directory (default hive/tests/generated)')
|
|
64
|
+
.action(applyCommand);
|
|
65
|
+
program
|
|
66
|
+
.command('report')
|
|
67
|
+
.description("Open this project's most recent run report")
|
|
68
|
+
.action(reportCommand);
|
|
69
|
+
program
|
|
70
|
+
.command('history')
|
|
71
|
+
.description('List runs the hivemind server has executed')
|
|
72
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
73
|
+
.action(historyCommand);
|
|
74
|
+
program
|
|
75
|
+
.command('dashboard')
|
|
76
|
+
.description("Open the hivemind server's hosted dashboard")
|
|
77
|
+
.action(dashboardCommand);
|
|
78
|
+
program.parseAsync(process.argv);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { getDraftTest } from '../serverClient.js';
|
|
3
|
+
import { saveTestFile, slugify } from '../testFile/write.js';
|
|
4
|
+
/** Pulls down a test `coverage` drafted for a gap it found — identified by the short code
|
|
5
|
+
* (e.g. "amber-fern-cove") shown in the PR comment, not by pasting any code. Mirrors Momentic's
|
|
6
|
+
* own `apply` command: the actual content lives on the server, the comment only ever shows the
|
|
7
|
+
* id, and this is the one command that turns that id into a real file in your project. */
|
|
8
|
+
export async function applyCommand(code, opts) {
|
|
9
|
+
let draft;
|
|
10
|
+
try {
|
|
11
|
+
draft = await getDraftTest(code);
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
console.error(err.message);
|
|
15
|
+
process.exitCode = 2;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const test = {
|
|
19
|
+
name: draft.name,
|
|
20
|
+
url: draft.url,
|
|
21
|
+
tags: draft.tags,
|
|
22
|
+
params: {},
|
|
23
|
+
before: [],
|
|
24
|
+
steps: draft.steps,
|
|
25
|
+
};
|
|
26
|
+
const outDir = opts.out || 'hive/tests/generated';
|
|
27
|
+
const filePath = join(outDir, `${slugify(draft.name)}.md`);
|
|
28
|
+
await saveTestFile(test, filePath);
|
|
29
|
+
console.log(`Wrote ${filePath}`);
|
|
30
|
+
console.log(` ${test.steps.length} steps, tags: ${test.tags.join(', ') || '(none)'}`);
|
|
31
|
+
console.log(` why hivemind drafted this: ${draft.reason}`);
|
|
32
|
+
console.log(` review it, then commit it like any other file.`);
|
|
33
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { getChangedFiles, getFullDiff } from '../testFile/select.js';
|
|
4
|
+
import { coverageOnServer } from '../serverClient.js';
|
|
5
|
+
import { checkCoverageLocally } from './coverageLocal.js';
|
|
6
|
+
import { saveTestFile, slugify } from '../testFile/write.js';
|
|
7
|
+
import { loadAllTestsFull } from '../testFile/loadAllTests.js';
|
|
8
|
+
import { renderCoveragePrComment } from '../report/prComment.js';
|
|
9
|
+
/**
|
|
10
|
+
* The actual diff-aware check: reads the real code diff (not just filenames), sends it — plus
|
|
11
|
+
* every local test with its full steps — to the server. The server figures out which known
|
|
12
|
+
* pages are genuinely affected, RUNS whichever local tests already target each one (this is the
|
|
13
|
+
* regression check: your change might break something a test wasn't written for), and only
|
|
14
|
+
* after that checks whether anything on the page is still left uncovered.
|
|
15
|
+
*/
|
|
16
|
+
export async function coverageCommand(opts) {
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
const base = typeof opts.changed === 'string' ? opts.changed : 'main';
|
|
19
|
+
const changedFiles = getChangedFiles(cwd, base);
|
|
20
|
+
if (changedFiles.length === 0) {
|
|
21
|
+
console.log(`No changes vs ${base} — nothing to check.`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const diff = getFullDiff(cwd, base);
|
|
25
|
+
const localTests = await loadAllTestsFull(cwd);
|
|
26
|
+
const serverUrl = process.env.HIVEMIND_SERVER_URL || 'https://app.thecolonylab.com';
|
|
27
|
+
console.log(`🐝 Scout reading the diff (${changedFiles.length} file${changedFiles.length === 1 ? '' : 's'} changed vs ${base})...`);
|
|
28
|
+
if (opts.local) {
|
|
29
|
+
console.log(` 🐝 Forager active — browser on this machine, Hivemind brain via ${serverUrl}`);
|
|
30
|
+
}
|
|
31
|
+
let result;
|
|
32
|
+
try {
|
|
33
|
+
result = opts.local
|
|
34
|
+
? await checkCoverageLocally({ serverUrl, diff, localTests, headed: opts.headed })
|
|
35
|
+
: await coverageOnServer(diff, localTests);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
console.error(err.message);
|
|
39
|
+
process.exitCode = 2;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// The server only knows its own path (e.g. /artifacts/...) — it has no idea what URL you're
|
|
43
|
+
// reaching it at (localhost in dev, a real domain in prod), so the full link gets built here.
|
|
44
|
+
for (const page of result.affectedPages) {
|
|
45
|
+
for (const t of page.testsRun) {
|
|
46
|
+
if (t.reportUrl)
|
|
47
|
+
t.reportUrl = `${serverUrl}${t.reportUrl}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!result.affectedPages.length) {
|
|
51
|
+
console.log('No known pages look affected by this change — nothing to re-check.');
|
|
52
|
+
if (opts.prComment) {
|
|
53
|
+
await writeFile(opts.prComment, renderCoveragePrComment([]), 'utf-8');
|
|
54
|
+
console.log(`PR comment written to ${opts.prComment}`);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const outDir = opts.out || 'hive/tests/generated';
|
|
59
|
+
let regressions = 0;
|
|
60
|
+
let gaps = 0;
|
|
61
|
+
for (const page of result.affectedPages) {
|
|
62
|
+
console.log(`\n${page.url}${page.isNew ? ' (new)' : ''}`);
|
|
63
|
+
console.log(` why: ${page.reason}`);
|
|
64
|
+
if (!page.testsRun.length) {
|
|
65
|
+
console.log(` (no existing test targets this page yet)`);
|
|
66
|
+
}
|
|
67
|
+
for (const t of page.testsRun) {
|
|
68
|
+
const icon = t.status === 'passed' ? '✓' : t.status === 'error' ? '⚠' : '✗';
|
|
69
|
+
console.log(` ${icon} ${t.name} — ${t.status}${t.status !== 'passed' ? `: ${t.verdict}` : ''}`);
|
|
70
|
+
if (t.reportUrl)
|
|
71
|
+
console.log(` report: ${t.reportUrl}`);
|
|
72
|
+
if (t.status !== 'passed')
|
|
73
|
+
regressions++;
|
|
74
|
+
}
|
|
75
|
+
if (!page.proposedTest)
|
|
76
|
+
continue;
|
|
77
|
+
gaps++;
|
|
78
|
+
const test = {
|
|
79
|
+
name: page.proposedTest.name,
|
|
80
|
+
url: page.url,
|
|
81
|
+
tags: page.proposedTest.tags || [],
|
|
82
|
+
params: {},
|
|
83
|
+
before: [],
|
|
84
|
+
steps: page.proposedTest.steps,
|
|
85
|
+
};
|
|
86
|
+
const filePath = join(outDir, `${slugify(test.name)}.md`);
|
|
87
|
+
await saveTestFile(test, filePath);
|
|
88
|
+
console.log(` + new gap found — wrote ${filePath} (also saved on the server as "${page.proposedTest.code}" — run \`hivemind apply ${page.proposedTest.code}\` to pull it down anywhere else)`);
|
|
89
|
+
}
|
|
90
|
+
console.log(`\n${regressions} regression${regressions === 1 ? '' : 's'}, ${gaps} new gap${gaps === 1 ? '' : 's'} found across ${result.affectedPages.length} affected page${result.affectedPages.length === 1 ? '' : 's'}.`);
|
|
91
|
+
if (opts.prComment) {
|
|
92
|
+
await writeFile(opts.prComment, renderCoveragePrComment(result.affectedPages), 'utf-8');
|
|
93
|
+
console.log(`PR comment written to ${opts.prComment}`);
|
|
94
|
+
}
|
|
95
|
+
// Only a broken existing test is a real failure. A drafted test for a new gap is a good outcome
|
|
96
|
+
// (something to review), not a failure — it shouldn't flip the exit code.
|
|
97
|
+
process.exitCode = regressions > 0 ? 1 : 0;
|
|
98
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { openSession } from '../browser/session.js';
|
|
3
|
+
import { Forager } from './remoteExecutor.js';
|
|
4
|
+
import { readApiKey } from '../credentials.js';
|
|
5
|
+
/**
|
|
6
|
+
* `coverage --local` — for the real case a plain cloud coverage check can't handle: the PR's
|
|
7
|
+
* changed code is only running inside this machine's own sandbox (a CI runner, usually), with no
|
|
8
|
+
* public URL the server could ever reach. Same split as `runTestLocally`: the browser runs here
|
|
9
|
+
* (the Forager), the server still does all the deciding — which pages are affected, what's
|
|
10
|
+
* covered, what to write — this machine just looks at whatever page it's told to.
|
|
11
|
+
*/
|
|
12
|
+
export async function checkCoverageLocally(opts) {
|
|
13
|
+
const session = await openSession({ headed: opts.headed });
|
|
14
|
+
const executor = new Forager(session.page, Date.now());
|
|
15
|
+
try {
|
|
16
|
+
const wsUrl = opts.serverUrl.replace(/^http/, 'ws') + '/ws/local-coverage';
|
|
17
|
+
const apiKey = await readApiKey();
|
|
18
|
+
const result = await new Promise((resolve, reject) => {
|
|
19
|
+
const headers = { 'User-Agent': 'Hivemind-Forager/0.1.0' };
|
|
20
|
+
if (apiKey)
|
|
21
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
22
|
+
const socket = new WebSocket(wsUrl, { headers });
|
|
23
|
+
socket.on('open', () => {
|
|
24
|
+
socket.send(JSON.stringify({ type: 'start', diff: opts.diff, localTests: opts.localTests }));
|
|
25
|
+
});
|
|
26
|
+
socket.on('message', async (raw) => {
|
|
27
|
+
let msg;
|
|
28
|
+
try {
|
|
29
|
+
msg = JSON.parse(raw.toString());
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (msg.type === 'call') {
|
|
35
|
+
const toolResult = await executor.execute(msg.name, msg.input);
|
|
36
|
+
socket.send(JSON.stringify({ type: 'result', callId: msg.callId, ...toolResult }));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (msg.type === 'finish') {
|
|
40
|
+
resolve(msg.result);
|
|
41
|
+
socket.close();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (msg.type === 'error') {
|
|
45
|
+
reject(new Error(msg.message));
|
|
46
|
+
socket.close();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
socket.on('error', (err) => reject(err));
|
|
50
|
+
socket.on('close', (code) => {
|
|
51
|
+
if (code !== 1000 && code !== 1005)
|
|
52
|
+
reject(new Error(`Connection to hivemind server closed unexpectedly (code ${code})`));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await session.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { createOnServer } from '../serverClient.js';
|
|
3
|
+
import { saveTestFile, slugify } from '../testFile/write.js';
|
|
4
|
+
import { listExistingTests } from '../testFile/listExisting.js';
|
|
5
|
+
import { loadConfig } from '../config.js';
|
|
6
|
+
export async function createCommand(request, opts) {
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const existingTests = await listExistingTests(cwd);
|
|
9
|
+
const config = await loadConfig(cwd);
|
|
10
|
+
const accountLabels = Object.keys(config.accounts || {});
|
|
11
|
+
let proposed;
|
|
12
|
+
try {
|
|
13
|
+
proposed = await createOnServer(request, opts.url, existingTests, accountLabels);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
console.error(err.message);
|
|
17
|
+
process.exitCode = 2;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const test = {
|
|
21
|
+
name: proposed.name,
|
|
22
|
+
url: opts.url,
|
|
23
|
+
login: proposed.login,
|
|
24
|
+
tags: proposed.tags || [],
|
|
25
|
+
params: proposed.params || {},
|
|
26
|
+
before: (proposed.before || []).map((b) => ({ name: b.name, params: b.params || {} })),
|
|
27
|
+
steps: proposed.steps,
|
|
28
|
+
};
|
|
29
|
+
const outDir = opts.out || 'hive/tests';
|
|
30
|
+
const filePath = join(outDir, `${slugify(proposed.name)}.md`);
|
|
31
|
+
await saveTestFile(test, filePath);
|
|
32
|
+
console.log(`Wrote ${filePath}`);
|
|
33
|
+
console.log(` ${test.steps.length} steps, tags: ${test.tags.join(', ') || '(none)'}`);
|
|
34
|
+
if (test.before.length) {
|
|
35
|
+
console.log(` reused existing test(s) as precondition: ${test.before.map((b) => b.name).join(', ')}`);
|
|
36
|
+
}
|
|
37
|
+
console.log(proposed.grounded
|
|
38
|
+
? ` grounded: looked at the real page at ${opts.url} before drafting these steps`
|
|
39
|
+
: ` blind guess: no --url given, drafted from common patterns only — review carefully`);
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* The dashboard is now hosted BY the hivemind server (it has real, shared, multi-machine data —
|
|
4
|
+
* every run any CLI has ever submitted). This command is just a convenience shortcut to open it;
|
|
5
|
+
* there is no local dashboard server anymore.
|
|
6
|
+
*/
|
|
7
|
+
export async function dashboardCommand() {
|
|
8
|
+
const url = process.env.HIVEMIND_SERVER_URL || 'https://app.thecolonylab.com';
|
|
9
|
+
console.log(`Opening ${url}`);
|
|
10
|
+
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
11
|
+
exec(`${opener} "${url}"`);
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { exploreOnServer } from '../serverClient.js';
|
|
3
|
+
import { saveTestFile, slugify } from '../testFile/write.js';
|
|
4
|
+
import { listExistingTests } from '../testFile/listExisting.js';
|
|
5
|
+
/**
|
|
6
|
+
* A real multi-page crawl, not a one-shot snapshot — the server maps pages, follows doors, and
|
|
7
|
+
* proposes starter tests along the way. Existing tests are sent along (same pattern `create`
|
|
8
|
+
* uses) so it doesn't propose a duplicate for a flow already covered.
|
|
9
|
+
*/
|
|
10
|
+
export async function exploreCommand(url, opts) {
|
|
11
|
+
console.log(`🐝 Scout dispatched to ${url}...`);
|
|
12
|
+
const existingTests = await listExistingTests(process.cwd());
|
|
13
|
+
let proposed;
|
|
14
|
+
try {
|
|
15
|
+
proposed = await exploreOnServer(url, existingTests);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
console.error(err.message);
|
|
19
|
+
process.exitCode = 2;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const outDir = opts.out || 'hive/tests/generated';
|
|
23
|
+
const written = [];
|
|
24
|
+
for (const p of proposed) {
|
|
25
|
+
const test = { name: p.name, url, tags: p.tags || [], params: {}, before: [], steps: p.steps };
|
|
26
|
+
const filePath = join(outDir, `${slugify(p.name)}.md`);
|
|
27
|
+
await saveTestFile(test, filePath);
|
|
28
|
+
written.push(filePath);
|
|
29
|
+
}
|
|
30
|
+
console.log(`\nWrote ${written.length} starter test${written.length === 1 ? '' : 's'} to ${outDir}/:`);
|
|
31
|
+
for (const f of written)
|
|
32
|
+
console.log(` ${f}`);
|
|
33
|
+
console.log(`\nReview and edit these like any other code, then run: hivemind run ${outDir}/`);
|
|
34
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { mkdir, readFile, writeFile, copyFile, access } from 'node:fs/promises';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
// dist/commands/init.js -> package root (skill/ ships alongside dist/, see package.json "files")
|
|
6
|
+
const packageRoot = join(__dirname, '..', '..');
|
|
7
|
+
async function exists(path) {
|
|
8
|
+
return access(path)
|
|
9
|
+
.then(() => true)
|
|
10
|
+
.catch(() => false);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* One-time setup for a project adopting hivemind — matches TesterArmy's `ta agent init` pattern.
|
|
14
|
+
* Idempotent: safe to run again later, never overwrites something already there.
|
|
15
|
+
*/
|
|
16
|
+
export async function initCommand() {
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
await mkdir(join(cwd, 'hive', 'tests'), { recursive: true });
|
|
19
|
+
console.log('✓ hive/tests/ ready');
|
|
20
|
+
const configPath = join(cwd, 'hivemind.config.json');
|
|
21
|
+
if (await exists(configPath)) {
|
|
22
|
+
console.log('- hivemind.config.json already exists, left untouched');
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const starter = {
|
|
26
|
+
environments: { default: 'http://localhost:3000' },
|
|
27
|
+
accounts: {},
|
|
28
|
+
areas: {},
|
|
29
|
+
};
|
|
30
|
+
await writeFile(configPath, JSON.stringify(starter, null, 2) + '\n', 'utf-8');
|
|
31
|
+
console.log('✓ hivemind.config.json created — edit environments.default to match your app');
|
|
32
|
+
}
|
|
33
|
+
const agentsPath = join(cwd, 'AGENTS.md');
|
|
34
|
+
const template = await readFile(join(packageRoot, 'skill', 'AGENTS.md.template'), 'utf-8');
|
|
35
|
+
const existingAgents = (await exists(agentsPath)) ? await readFile(agentsPath, 'utf-8') : '';
|
|
36
|
+
if (existingAgents.includes('<!-- HIVEMIND:START -->')) {
|
|
37
|
+
console.log('- AGENTS.md already has the hivemind block, left untouched');
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const updated = existingAgents ? `${existingAgents.trimEnd()}\n\n${template}` : template;
|
|
41
|
+
await writeFile(agentsPath, updated, 'utf-8');
|
|
42
|
+
console.log(existingAgents ? '✓ AGENTS.md updated with hivemind instructions' : '✓ AGENTS.md created with hivemind instructions');
|
|
43
|
+
}
|
|
44
|
+
const skillDir = join(cwd, '.agents', 'skills', 'hivemind-cli');
|
|
45
|
+
await mkdir(skillDir, { recursive: true });
|
|
46
|
+
await copyFile(join(packageRoot, 'skill', 'hivemind-cli', 'SKILL.md'), join(skillDir, 'SKILL.md'));
|
|
47
|
+
console.log('✓ .agents/skills/hivemind-cli/SKILL.md installed');
|
|
48
|
+
console.log('\nDone. A coding agent working in this repo now knows how to use hivemind.');
|
|
49
|
+
console.log('Next: hivemind create "<what to test>" --url <your app url>');
|
|
50
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { writeApiKey } from '../credentials.js';
|
|
2
|
+
import { serverUrl } from '../serverClient.js';
|
|
3
|
+
/** Stores an API key generated from the web dashboard's Settings page — the CLI has no browser,
|
|
4
|
+
* so it can't sign in the normal way; this is the whole auth story for it. */
|
|
5
|
+
export async function loginCommand(key) {
|
|
6
|
+
if (!key.startsWith('hm_')) {
|
|
7
|
+
console.error('That doesn\'t look like a hivemind API key (should start with "hm_"). Generate one from the dashboard\'s Settings page.');
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const res = await fetch(`${serverUrl()}/api/auth/me`, { headers: { Authorization: `Bearer ${key}` } }).catch(() => null);
|
|
12
|
+
if (!res || !res.ok) {
|
|
13
|
+
console.error(`Could not verify that key against ${serverUrl()} — check the key and that HIVEMIND_SERVER_URL is correct.`);
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const user = (await res.json());
|
|
18
|
+
await writeApiKey(key);
|
|
19
|
+
console.log(`✓ Signed in as ${user.email}`);
|
|
20
|
+
}
|