mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 10:36:27 +03:00
Port /impeccable generate to the engine crates
The Node-era server, CLI, hook, and pin halves of the generate command move into the Rust workspace, with the protocol unchanged: - crates/live: POST /agent-target is held open on a channel plus a timer thread (the manual-apply deferred pattern), releasing its turnstile ticket before it parks like /poll; /agent-target-result resolves it; /agent-target-claim is the roll call with its renewable lease. SSE connections carry the overlay's clientId: a late overlay is replayed every pending target, and a disconnect retires that overlay's report, releases its lease, and re-judges each roll call. Shutdown drains held requests with server_stopping. - crates/live/src/live_generate.rs: the live-generate verb (the router already forwards every live* verb), same flags, verdicts, and _instructions, spelled with the engine's self command. - crates/hook: every entry stands down on live preview markers (skipped: live-preview), checking the proposed content and the file on disk for hook-before-edit. - crates/context: pin accepts generate; the crate's command-metadata.json copy carries its entry. Tests: crates/cli/tests/agent_target.rs (six HTTP cases with an SSE reader), tests/live-agent-target.test.mjs rewritten to drive the binary (28 cases, registered in the live suite), hook stand-down cases, oracle goldens for live-generate plus the re-recorded pin list goldens, the e2e prompt assertion waiting for the journaled event, and the contract documented in docs/CLI-CONTRACT.md. AI-assisted: implemented and tested with Claude Code under maintainer direction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Abdul Wahab
co-authored by
Claude Fable 5
parent
fc89b0ed62
commit
d397140a77
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Tests for agent-initiated element targeting (the `generate` command):
|
||||
* POST /agent-target held-open pairing with POST /agent-target-result,
|
||||
* validation, the no-browser and timeout verdicts, and the live-generate CLI's
|
||||
* local failure modes.
|
||||
* Protocol tests for agent-initiated element targeting (the `generate`
|
||||
* command), driven against the engine binary: POST /agent-target held-open
|
||||
* pairing with POST /agent-target-result, validation, the roll call and its
|
||||
* leases, the no-browser and timeout verdicts, and the live-generate verb's
|
||||
* local failure modes. Skips cleanly without a binary (tests/lib/engine-bin.mjs).
|
||||
*
|
||||
* Run with: node --test tests/live-agent-target.test.mjs
|
||||
*/
|
||||
@@ -14,37 +15,66 @@ import { dirname, join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { execFile, execFileSync, spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { getLiveServerPath } from '../skill/scripts/lib/impeccable-paths.mjs';
|
||||
import { VISUAL_ACTIONS } from '../skill/scripts/live/vocabulary.mjs';
|
||||
import { ENGINE_MISSING_MESSAGE, engineEnv, findEngineBinary } from './lib/engine-bin.mjs';
|
||||
|
||||
// Resolve the repo from this file, not from cwd: the runner may be invoked
|
||||
// from tests/ or anywhere else.
|
||||
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const SERVER_SCRIPT = join(REPO_ROOT, 'skill/scripts/live-server.mjs');
|
||||
const GENERATE_SCRIPT = join(REPO_ROOT, 'skill/scripts/live-generate.mjs');
|
||||
const ENGINE_BIN = findEngineBinary();
|
||||
|
||||
// The action vocabulary lives in the engine (crates/live/src/vocabulary.rs);
|
||||
// read it from the Rust source so the matrix below can never drift from what
|
||||
// the live server accepts.
|
||||
function readVisualActions() {
|
||||
const rust = readFileSync(join(REPO_ROOT, 'crates/live/src/vocabulary.rs'), 'utf-8');
|
||||
const block = rust.match(/pub const VISUAL_ACTIONS: \[&str; (\d+)\] = \[([\s\S]*?)\];/);
|
||||
if (!block) throw new Error('VISUAL_ACTIONS not found in crates/live/src/vocabulary.rs');
|
||||
return [...block[2].matchAll(/"([a-z]+)"/g)].map((m) => m[1]);
|
||||
}
|
||||
const VISUAL_ACTIONS = readVisualActions();
|
||||
|
||||
function liveServerPath(cwd) {
|
||||
return join(cwd, '.impeccable/live/server.json');
|
||||
}
|
||||
|
||||
/** Run the live-generate verb; the JSON verdict is on stdout on every exit code. */
|
||||
function runGenerate(cwd, args) {
|
||||
return execFileSync(ENGINE_BIN, ['live-generate', ...args], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
env: engineEnv(ENGINE_BIN, {}),
|
||||
});
|
||||
}
|
||||
|
||||
function startServer(port, { cwd, env = {} } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn('node', [SERVER_SCRIPT, '--port=' + port], {
|
||||
const proc = spawn(ENGINE_BIN, ['live-server', '--port=' + port], {
|
||||
cwd,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...process.env, IMPECCABLE_LIVE_COPY_AGENT: 'off', ...env },
|
||||
env: engineEnv(ENGINE_BIN, { IMPECCABLE_LIVE_COPY_AGENT: 'off', ...env }),
|
||||
});
|
||||
let output = '';
|
||||
proc.stdout.on('data', (d) => {
|
||||
output += d.toString();
|
||||
if (output.includes('running on')) {
|
||||
try {
|
||||
const info = JSON.parse(readFileSync(getLiveServerPath(cwd), 'utf-8'));
|
||||
resolve({ proc, port: info.port, token: info.token, cwd });
|
||||
} catch {
|
||||
reject(new Error('Server started but PID file not readable'));
|
||||
}
|
||||
}
|
||||
});
|
||||
proc.stdout.on('data', (d) => { output += d.toString(); });
|
||||
proc.stderr.on('data', (d) => { output += d.toString(); });
|
||||
proc.on('error', reject);
|
||||
setTimeout(() => reject(new Error('Server start timeout. Output: ' + output)), 5000);
|
||||
// The server writes server.json on listen; poll for it rather than
|
||||
// parsing the banner, so a slow first start still resolves.
|
||||
const deadline = Date.now() + 10_000;
|
||||
const tick = () => {
|
||||
try {
|
||||
const info = JSON.parse(readFileSync(liveServerPath(cwd), 'utf-8'));
|
||||
if (info.port && info.token) {
|
||||
resolve({ proc, port: info.port, token: info.token, cwd });
|
||||
return;
|
||||
}
|
||||
} catch { /* not yet */ }
|
||||
if (Date.now() > deadline) {
|
||||
reject(new Error('Server start timeout. Output: ' + output));
|
||||
return;
|
||||
}
|
||||
setTimeout(tick, 50);
|
||||
};
|
||||
tick();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,7 +146,7 @@ async function openSseClient(server, { clientId } = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
describe('POST /agent-target', () => {
|
||||
describe('POST /agent-target', { skip: ENGINE_BIN ? false : ENGINE_MISSING_MESSAGE }, () => {
|
||||
let tmp;
|
||||
let server;
|
||||
|
||||
@@ -606,9 +636,9 @@ describe('POST /agent-target', () => {
|
||||
for (const action of VISUAL_ACTIONS) {
|
||||
const cli = new Promise((resolve) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[GENERATE_SCRIPT, '--selector', 'h1', '--action', action, '--dry-run'],
|
||||
{ cwd: tmp, encoding: 'utf-8' },
|
||||
ENGINE_BIN,
|
||||
['live-generate', '--selector', 'h1', '--action', action, '--dry-run'],
|
||||
{ cwd: tmp, encoding: 'utf-8', env: engineEnv(ENGINE_BIN, {}) },
|
||||
(err, stdout) => resolve({ code: err ? err.code : 0, stdout }),
|
||||
);
|
||||
});
|
||||
@@ -666,7 +696,7 @@ describe('POST /agent-target', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('live-generate CLI --wait-for-browser', () => {
|
||||
describe('live-generate CLI --wait-for-browser', { skip: ENGINE_BIN ? false : ENGINE_MISSING_MESSAGE }, () => {
|
||||
let tmp;
|
||||
let server;
|
||||
|
||||
@@ -687,10 +717,7 @@ describe('live-generate CLI --wait-for-browser', () => {
|
||||
|
||||
function runCli(cwd, args) {
|
||||
try {
|
||||
const stdout = execFileSync(process.execPath, [GENERATE_SCRIPT, ...args], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const stdout = runGenerate(cwd, args);
|
||||
return { code: 0, json: JSON.parse(stdout) };
|
||||
} catch (err) {
|
||||
return { code: err.status, json: JSON.parse(err.stdout) };
|
||||
@@ -721,9 +748,9 @@ describe('live-generate CLI --wait-for-browser', () => {
|
||||
// and the delayed connect would never happen.
|
||||
const child = new Promise((resolve) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[GENERATE_SCRIPT, '--selector', 'h1', '--action', 'bolder', '--wait-for-browser', '10000'],
|
||||
{ cwd: tmp, encoding: 'utf-8' },
|
||||
ENGINE_BIN,
|
||||
['live-generate', '--selector', 'h1', '--action', 'bolder', '--wait-for-browser', '10000'],
|
||||
{ cwd: tmp, encoding: 'utf-8', env: engineEnv(ENGINE_BIN, {}) },
|
||||
(err, stdout) => resolve({ code: err ? err.code : 0, stdout }),
|
||||
);
|
||||
});
|
||||
@@ -737,13 +764,10 @@ describe('live-generate CLI --wait-for-browser', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('live-generate CLI local failure modes', () => {
|
||||
describe('live-generate CLI local failure modes', { skip: ENGINE_BIN ? false : ENGINE_MISSING_MESSAGE }, () => {
|
||||
function runCli(cwd, args) {
|
||||
try {
|
||||
const stdout = execFileSync(process.execPath, [GENERATE_SCRIPT, ...args], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const stdout = runGenerate(cwd, args);
|
||||
return { code: 0, json: JSON.parse(stdout) };
|
||||
} catch (err) {
|
||||
return { code: err.status, json: JSON.parse(err.stdout) };
|
||||
@@ -756,7 +780,7 @@ describe('live-generate CLI local failure modes', () => {
|
||||
const { code, json } = runCli(tmp, ['--selector', 'h1', '--action', 'bolder']);
|
||||
assert.equal(code, 1);
|
||||
assert.equal(json.error, 'server_not_running');
|
||||
assert.match(json._instructions, /live\.mjs/);
|
||||
assert.match(json._instructions, / live\)/, 'names the boot verb');
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -885,10 +885,11 @@ for (const { name, fixture } of fixtures) {
|
||||
if (scenario.prompt) {
|
||||
// The configure bar rebuild once discarded the preset prompt, so
|
||||
// pin the regression at the wire: the journaled generate event
|
||||
// must carry the prompt the CLI was given.
|
||||
const journalPath = join(appRoot, '.impeccable/live/sessions', `${res.sessionId}.jsonl`);
|
||||
const journaled = readFileSync(journalPath, 'utf-8').trim().split('\n').map((l) => JSON.parse(l));
|
||||
const generateEvent = journaled.find((entry) => entry.type === 'generate')?.event;
|
||||
// must carry the prompt the CLI was given. The engine journals a
|
||||
// generate event when the agent leases it from /poll, so wait
|
||||
// for the entry instead of reading the journal right away.
|
||||
const [journaled] = await waitForJournalEvent(appRoot, res.sessionId, 'generate');
|
||||
const generateEvent = journaled?.event ?? journaled;
|
||||
assert.equal(
|
||||
generateEvent?.freeformPrompt,
|
||||
scenario.prompt,
|
||||
|
||||
@@ -3,7 +3,6 @@ import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { compileProviderBlocks } from '../scripts/lib/utils.js';
|
||||
import { VISUAL_ACTIONS } from '../skill/scripts/live/vocabulary.mjs';
|
||||
|
||||
const ROOT = process.cwd();
|
||||
|
||||
@@ -178,8 +177,20 @@ describe('live reference authoring contract', () => {
|
||||
// value the picker offers but the reference never names is a request
|
||||
// the agent cannot route.
|
||||
const generateMd = readFileSync(join(ROOT, 'skill/reference/generate.md'), 'utf-8');
|
||||
for (const action of VISUAL_ACTIONS) {
|
||||
for (const action of readVisualActions()) {
|
||||
assert.match(generateMd, new RegExp('`' + action + '`'), `generate.md must name \`${action}\``);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// The action vocabulary lives in the engine (crates/live/src/vocabulary.rs);
|
||||
// read it from the Rust source so the parity check needs no binary and can
|
||||
// never drift from what the live server accepts.
|
||||
function readVisualActions() {
|
||||
const rust = readFileSync(join(ROOT, 'crates/live/src/vocabulary.rs'), 'utf-8');
|
||||
const block = rust.match(/pub const VISUAL_ACTIONS: \[&str; (\d+)\] = \[([\s\S]*?)\];/);
|
||||
if (!block) throw new Error('VISUAL_ACTIONS not found in crates/live/src/vocabulary.rs');
|
||||
const actions = [...block[2].matchAll(/"([a-z]+)"/g)].map((m) => m[1]);
|
||||
if (actions.length !== Number(block[1])) throw new Error('VISUAL_ACTIONS length mismatch');
|
||||
return actions;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* `live-generate` (the `generate` command's agent-initiated targeting): the
|
||||
* verdicts the verb decides locally, without a browser, plus the one the
|
||||
* helper answers when no overlay is attached. Everything that needs an
|
||||
* overlay (the roll call, leases, replay) is covered by
|
||||
* tests/live-agent-target.test.mjs and crates/cli/tests/agent_target.rs.
|
||||
*/
|
||||
import { LIVE_FILES } from '../live-helpers.mjs';
|
||||
|
||||
const NORM = [
|
||||
['localhost:\\d{4,5}', 'g', 'localhost:<PORT>'],
|
||||
['"(port|serverPort)":(\\s*)\\d{4,5}', 'g', '"$1":$2<PORT>'],
|
||||
['Stopped live server on port \\d+\\.', 'g', 'Stopped live server on port <PORT>.'],
|
||||
];
|
||||
|
||||
export default [
|
||||
{
|
||||
id: 'live-generate-local-verdicts', workspace: 'live-html', files: [...LIVE_FILES],
|
||||
// No helper is recorded in the staged workspace, so every step short
|
||||
// of a valid request ends in the verb's own verdict, and the valid one
|
||||
// ends in server_not_running.
|
||||
steps: [
|
||||
{ verb: 'live-generate', args: ['--help'] },
|
||||
{ verb: 'live-generate', args: [] },
|
||||
{ verb: 'live-generate', args: ['--selector'] },
|
||||
{ verb: 'live-generate', args: ['--selector', 'h1', '--action', 'bold'] },
|
||||
{ verb: 'live-generate', args: ['--selector', 'h1', '--count', '9'] },
|
||||
{ verb: 'live-generate', args: ['--selector', 'h1', '--count', 'three'] },
|
||||
{ verb: 'live-generate', args: ['--selector', 'h1', '--index', '0'] },
|
||||
{ verb: 'live-generate', args: ['--selector', 'h1', '--wait-for-browser', 'soon'] },
|
||||
{ verb: 'live-generate', args: ['--selector', 'h1', '--action', 'bolder'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'live-generate-no-browser-connected', workspace: 'live-html', files: [...LIVE_FILES], normalize: NORM,
|
||||
// A running helper with no overlay attached answers at once instead of
|
||||
// holding the request.
|
||||
steps: [
|
||||
{ verb: 'live-server', daemon: true, readyFile: '.impeccable/live/server.json', readyTimeoutMs: 15000 },
|
||||
{ verb: 'live-generate', args: ['--selector', 'h1', '--action', 'bolder', '--count', '2', '--prompt', 'warmer'] },
|
||||
{ verb: 'live-server', args: ['stop'] },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"steps": [
|
||||
{
|
||||
"stdout": "Usage: impeccable live-generate --selector <css> [--text <snippet>] [--index <n>] [--action <name>] [--count <n>] [--prompt <text>] [--dry-run] [--wait-for-browser <ms>]\n\nFlags:\n --selector <css> required; resolved with document.querySelectorAll\n --text <snippet> optional; keeps only matches whose textContent contains it\n --index <n> optional; 1-based pick among the remaining matches\n --action <name> optional; one of the live action vocabulary (default: impeccable)\n --count <n> optional; variants to request, 1-8 (default: 3)\n --prompt <text> optional; freeform direction, same as typing before Go\n --dry-run optional; resolve and report without starting anything\n --wait-for-browser <ms> optional; poll the helper until a page with the\n overlay connects (or the budget runs out) before sending\n the target.\n\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null
|
||||
},
|
||||
{
|
||||
"stdout": "{\n \"ok\": false,\n \"error\": \"selector_required\",\n \"_instructions\": \"Pass --selector with a CSS selector for the element to target. Derive it from the page source: prefer an id, a unique class, or a landmark section, and add --text \\\"<visible text>\\\" when the class repeats.\"\n}\n",
|
||||
"stderr": "",
|
||||
"exit": 1,
|
||||
"signal": null
|
||||
},
|
||||
{
|
||||
"stdout": "{\n \"ok\": false,\n \"error\": \"missing_flag_value\",\n \"flag\": \"--selector\"\n}\n",
|
||||
"stderr": "",
|
||||
"exit": 1,
|
||||
"signal": null
|
||||
},
|
||||
{
|
||||
"stdout": "{\n \"ok\": false,\n \"error\": \"invalid_action\",\n \"action\": \"bold\",\n \"validActions\": [\n \"impeccable\",\n \"bolder\",\n \"quieter\",\n \"distill\",\n \"polish\",\n \"typeset\",\n \"colorize\",\n \"layout\",\n \"adapt\",\n \"animate\",\n \"delight\",\n \"overdrive\"\n ],\n \"_instructions\": \"Map the request wording onto the closest listed action (bold -> bolder, quiet/calmer -> quieter, simplify -> distill). When no action fits, use --action impeccable and carry the wording via --prompt.\"\n}\n",
|
||||
"stderr": "",
|
||||
"exit": 1,
|
||||
"signal": null
|
||||
},
|
||||
{
|
||||
"stdout": "{\n \"ok\": false,\n \"error\": \"invalid_count\",\n \"count\": \"9\",\n \"_instructions\": \"Pass --count as an integer from 1 to 8.\"\n}\n",
|
||||
"stderr": "",
|
||||
"exit": 1,
|
||||
"signal": null
|
||||
},
|
||||
{
|
||||
"stdout": "{\n \"ok\": false,\n \"error\": \"invalid_count\",\n \"count\": \"three\",\n \"_instructions\": \"Pass --count as an integer from 1 to 8.\"\n}\n",
|
||||
"stderr": "",
|
||||
"exit": 1,
|
||||
"signal": null
|
||||
},
|
||||
{
|
||||
"stdout": "{\n \"ok\": false,\n \"error\": \"invalid_index\",\n \"index\": \"0\",\n \"_instructions\": \"Pass --index as a 1-based integer position among the matches.\"\n}\n",
|
||||
"stderr": "",
|
||||
"exit": 1,
|
||||
"signal": null
|
||||
},
|
||||
{
|
||||
"stdout": "{\n \"ok\": false,\n \"error\": \"invalid_wait\",\n \"wait\": \"soon\",\n \"_instructions\": \"Pass --wait-for-browser as a positive integer of milliseconds, e.g. --wait-for-browser 120000.\"\n}\n",
|
||||
"stderr": "",
|
||||
"exit": 1,
|
||||
"signal": null
|
||||
},
|
||||
{
|
||||
"stdout": "{\n \"ok\": false,\n \"error\": \"server_not_running\",\n \"_instructions\": \"No live helper server is recorded for this project. Run the live boot first (<IMPECCABLE> live), open the app URL that serves a pageFiles entry, then rerun this command.\"\n}\n",
|
||||
"stderr": "",
|
||||
"exit": 1,
|
||||
"signal": null
|
||||
}
|
||||
],
|
||||
"files": {
|
||||
".impeccable/live/config.json": "{\n \"files\": [\"index.html\", \"public/**/*.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"steps": [
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"exit": null,
|
||||
"signal": null,
|
||||
"daemon": true
|
||||
},
|
||||
{
|
||||
"stdout": "{\n \"ok\": false,\n \"error\": \"no_browser_connected\",\n \"_instructions\": \"No page with the live overlay is connected. Open the app URL that serves a pageFiles entry yourself with your harness browser tool, then rerun this command. Only when no browser tool exists: give the user the URL and rerun with --wait-for-browser 120000 so the command fires as soon as they open the page.\"\n}\n",
|
||||
"stderr": "",
|
||||
"exit": 1,
|
||||
"signal": null
|
||||
},
|
||||
{
|
||||
"stdout": "Stopped live server on port <PORT>.\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null
|
||||
}
|
||||
],
|
||||
"files": {
|
||||
".impeccable/live/config.json": "{\n \"files\": [\"index.html\", \"public/**/*.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n"
|
||||
},
|
||||
"daemon": [
|
||||
{
|
||||
"stdout": "\nImpeccable live server running on http://localhost:<PORT>\nToken: <UUID>\n\nScript: http://localhost:<PORT>/live.js\nInject: managed by impeccable live-inject; Astro source tags use is:inline automatically.\nStop: impeccable live-server stop\n",
|
||||
"stderr": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "Unknown command: teach\nAvailable commands: craft, init, extract, document, shape, critique, audit, polish, bolder, quieter, distill, harden, onboard, live, animate, colorize, typeset, layout, delight, overdrive, clarify, adapt, optimize\n",
|
||||
"stderr": "Unknown command: teach\nAvailable commands: craft, init, extract, document, shape, critique, audit, polish, bolder, quieter, distill, harden, onboard, live, animate, colorize, typeset, layout, delight, overdrive, clarify, adapt, optimize, generate\n",
|
||||
"exit": 1,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "Unknown command: doctor\nAvailable commands: craft, init, extract, document, shape, critique, audit, polish, bolder, quieter, distill, harden, onboard, live, animate, colorize, typeset, layout, delight, overdrive, clarify, adapt, optimize\n",
|
||||
"stderr": "Unknown command: doctor\nAvailable commands: craft, init, extract, document, shape, critique, audit, polish, bolder, quieter, distill, harden, onboard, live, animate, colorize, typeset, layout, delight, overdrive, clarify, adapt, optimize, generate\n",
|
||||
"exit": 1,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "Usage: impeccable pin <pin|unpin> <command>\n\nAvailable commands: craft, init, extract, document, shape, critique, audit, polish, bolder, quieter, distill, harden, onboard, live, animate, colorize, typeset, layout, delight, overdrive, clarify, adapt, optimize\n",
|
||||
"stdout": "Usage: impeccable pin <pin|unpin> <command>\n\nAvailable commands: craft, init, extract, document, shape, critique, audit, polish, bolder, quieter, distill, harden, onboard, live, animate, colorize, typeset, layout, delight, overdrive, clarify, adapt, optimize, generate\n",
|
||||
"stderr": "",
|
||||
"exit": 1,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "Usage: impeccable pin <pin|unpin> <command>\n\nAvailable commands: craft, init, extract, document, shape, critique, audit, polish, bolder, quieter, distill, harden, onboard, live, animate, colorize, typeset, layout, delight, overdrive, clarify, adapt, optimize\n",
|
||||
"stdout": "Usage: impeccable pin <pin|unpin> <command>\n\nAvailable commands: craft, init, extract, document, shape, critique, audit, polish, bolder, quieter, distill, harden, onboard, live, animate, colorize, typeset, layout, delight, overdrive, clarify, adapt, optimize, generate\n",
|
||||
"stderr": "",
|
||||
"exit": 1,
|
||||
"signal": null,
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { detectProvider, getModel, hasKey, resolveModelList, PROVIDERS } from './providers.mjs';
|
||||
import { assertLauncherDenialWarningBeforeNextTool, assertPlanningFallbackWarning, LAUNCHER_FAILURE_WARNING, assertAdviceOnly, assertWorkflowAdvice, assertCommandComparison, missingReferences } from './assertions.mjs';
|
||||
import { assertCompleted } from '../skill-workflow/assertions.mjs';
|
||||
import { findEngineBinary } from '../lib/engine-bin.mjs';
|
||||
import {
|
||||
PRODUCT_MD_SAMPLE,
|
||||
PRODUCT_MD_SAMPLE_NO_REGISTER,
|
||||
@@ -106,10 +107,16 @@ function loadedBefore(trace, first, second) {
|
||||
*/
|
||||
function stopLiveHelper(workspace) {
|
||||
try {
|
||||
const engineBin = findEngineBinary();
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
[path.join(workspace, '.claude/skills/impeccable/scripts/live-server.mjs'), 'stop'],
|
||||
{ cwd: workspace, stdio: 'ignore', timeout: 10_000 },
|
||||
path.join(workspace, '.claude/skills/impeccable/scripts/impeccable'),
|
||||
['live-server', 'stop'],
|
||||
{
|
||||
cwd: workspace,
|
||||
stdio: 'ignore',
|
||||
timeout: 10_000,
|
||||
env: { ...process.env, ...(engineBin ? { IMPECCABLE_BIN: engineBin } : {}) },
|
||||
},
|
||||
);
|
||||
} catch { /* nothing was running */ }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user