@ytspar/sweetlink 1.18.0 → 1.20.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/README.md +40 -0
- package/dist/cli/outputSchemas.d.ts +57 -1
- package/dist/cli/outputSchemas.d.ts.map +1 -1
- package/dist/cli/outputSchemas.js +36 -1
- package/dist/cli/outputSchemas.js.map +1 -1
- package/dist/cli/sweetlink.js +537 -36
- package/dist/cli/sweetlink.js.map +1 -1
- package/dist/daemon/browser.d.ts.map +1 -1
- package/dist/daemon/browser.js.map +1 -1
- package/dist/daemon/client.d.ts +7 -0
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +16 -2
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/demo.d.ts.map +1 -1
- package/dist/daemon/demo.js +6 -2
- package/dist/daemon/demo.js.map +1 -1
- package/dist/daemon/diff.d.ts.map +1 -1
- package/dist/daemon/diff.js +5 -3
- package/dist/daemon/diff.js.map +1 -1
- package/dist/daemon/evidence.d.ts.map +1 -1
- package/dist/daemon/evidence.js +5 -5
- package/dist/daemon/evidence.js.map +1 -1
- package/dist/daemon/index.js +1 -1
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/listeners.d.ts.map +1 -1
- package/dist/daemon/listeners.js +7 -5
- package/dist/daemon/listeners.js.map +1 -1
- package/dist/daemon/recording.d.ts +5 -0
- package/dist/daemon/recording.d.ts.map +1 -1
- package/dist/daemon/recording.js +34 -11
- package/dist/daemon/recording.js.map +1 -1
- package/dist/daemon/refs.d.ts.map +1 -1
- package/dist/daemon/refs.js.map +1 -1
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +419 -80
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/summary.d.ts +1 -1
- package/dist/daemon/summary.d.ts.map +1 -1
- package/dist/daemon/types.d.ts +1 -1
- package/dist/daemon/types.d.ts.map +1 -1
- package/dist/daemon/types.js.map +1 -1
- package/dist/daemon/viewer.d.ts +1 -1
- package/dist/daemon/viewer.d.ts.map +1 -1
- package/dist/daemon/viewer.js +18 -10
- package/dist/daemon/viewer.js.map +1 -1
- package/dist/ruler.js +3 -1
- package/dist/ruler.js.map +1 -1
- package/dist/runs.d.ts +34 -0
- package/dist/runs.d.ts.map +1 -0
- package/dist/runs.js +61 -0
- package/dist/runs.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +20 -10
- package/dist/server/index.js.map +1 -1
- package/dist/simulator/android.d.ts +35 -0
- package/dist/simulator/android.d.ts.map +1 -0
- package/dist/simulator/android.js +127 -0
- package/dist/simulator/android.js.map +1 -0
- package/dist/simulator/ios.d.ts +39 -0
- package/dist/simulator/ios.d.ts.map +1 -0
- package/dist/simulator/ios.js +123 -0
- package/dist/simulator/ios.js.map +1 -0
- package/dist/term/ansi.d.ts +37 -0
- package/dist/term/ansi.d.ts.map +1 -0
- package/dist/term/ansi.js +205 -0
- package/dist/term/ansi.js.map +1 -0
- package/dist/term/player.d.ts +25 -0
- package/dist/term/player.d.ts.map +1 -0
- package/dist/term/player.js +243 -0
- package/dist/term/player.js.map +1 -0
- package/dist/term/recorder.d.ts +33 -0
- package/dist/term/recorder.d.ts.map +1 -0
- package/dist/term/recorder.js +77 -0
- package/dist/term/recorder.js.map +1 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +8 -4
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/sweetlink.js
CHANGED
|
@@ -9,13 +9,13 @@ import * as fs from 'fs';
|
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { WebSocket } from 'ws';
|
|
11
11
|
import { detectCDP, getNetworkRequestsViaCDP } from '../cdp.js';
|
|
12
|
+
import { DaemonRequestError, daemonRequest, ensureDaemon, getDaemonStatus, stopDaemon, } from '../daemon/client.js';
|
|
13
|
+
import { uploadEvidence } from '../daemon/evidence.js';
|
|
14
|
+
import { extractPort } from '../daemon/stateFile.js';
|
|
12
15
|
import { screenshotViaPlaywright } from '../playwright.js';
|
|
13
16
|
import { getCardHeaderPreset, getNavigationPreset, measureViaPlaywright } from '../ruler.js';
|
|
14
17
|
import { DEFAULT_WS_PORT, MAX_PORT_RETRIES, WS_PORT_OFFSET } from '../types.js';
|
|
15
18
|
import { SCREENSHOT_DIR } from '../urlUtils.js';
|
|
16
|
-
import { daemonRequest, ensureDaemon, getDaemonStatus, stopDaemon } from '../daemon/client.js';
|
|
17
|
-
import { extractPort } from '../daemon/stateFile.js';
|
|
18
|
-
import { uploadEvidence } from '../daemon/evidence.js';
|
|
19
19
|
import { emitJson, printOutputSchema } from './outputSchemas.js';
|
|
20
20
|
const COMMON_APP_PORTS = [3000, 3001, 4000, 5173, 5174, 8000, 8080];
|
|
21
21
|
/**
|
|
@@ -118,7 +118,9 @@ function reportScreenshotSuccess(outputPath, width, height, method, selector) {
|
|
|
118
118
|
try {
|
|
119
119
|
sizeKb = ` · ${Math.round(fs.statSync(outputPath).size / 1024)}KB`;
|
|
120
120
|
}
|
|
121
|
-
catch {
|
|
121
|
+
catch {
|
|
122
|
+
/* file may have been moved or removed */
|
|
123
|
+
}
|
|
122
124
|
const selPart = selector ? ` · ${selector}` : '';
|
|
123
125
|
if (process.env.SWEETLINK_VERBOSE === '1') {
|
|
124
126
|
console.log(`[Sweetlink] ✓ Screenshot saved to: ${getRelativePath(outputPath)}`);
|
|
@@ -188,8 +190,7 @@ async function discoverServer(target) {
|
|
|
188
190
|
}
|
|
189
191
|
const lowerTarget = target.toLowerCase();
|
|
190
192
|
// Exact match on branch or app name
|
|
191
|
-
const exact = results.find((r) => r.gitBranch?.toLowerCase() === lowerTarget ||
|
|
192
|
-
r.appName?.toLowerCase() === lowerTarget);
|
|
193
|
+
const exact = results.find((r) => r.gitBranch?.toLowerCase() === lowerTarget || r.appName?.toLowerCase() === lowerTarget);
|
|
193
194
|
if (exact)
|
|
194
195
|
return `ws://localhost:${exact.port}`;
|
|
195
196
|
// Partial match (branch contains target)
|
|
@@ -485,8 +486,11 @@ async function screenshot(options) {
|
|
|
485
486
|
`Use --index N (with click) or a more specific selector to pick another.`);
|
|
486
487
|
}
|
|
487
488
|
// UX: hint at --full-page when content extends below the viewport.
|
|
488
|
-
if (!options.selector &&
|
|
489
|
-
|
|
489
|
+
if (!options.selector &&
|
|
490
|
+
!options.fullPage &&
|
|
491
|
+
data.pageHeight &&
|
|
492
|
+
data.viewportHeight &&
|
|
493
|
+
data.pageHeight > data.viewportHeight + 4) {
|
|
490
494
|
const overflow = data.pageHeight - data.viewportHeight;
|
|
491
495
|
console.log(`[Sweetlink] ℹ Page extends ${overflow}px below the viewport. ` +
|
|
492
496
|
`Use --full-page to capture all of it.`);
|
|
@@ -1547,6 +1551,27 @@ async function getVitals(options) {
|
|
|
1547
1551
|
process.exit(1);
|
|
1548
1552
|
}
|
|
1549
1553
|
}
|
|
1554
|
+
function printInspectSummary(data) {
|
|
1555
|
+
console.log('\n[Sweetlink] Agent Inspect');
|
|
1556
|
+
console.log(` URL: ${data.url}`);
|
|
1557
|
+
console.log(` Title: ${data.title || '(untitled)'}`);
|
|
1558
|
+
console.log(` Viewport: ${data.viewport.width}x${data.viewport.height}` +
|
|
1559
|
+
(data.viewport.deviceScaleFactor ? ` @${data.viewport.deviceScaleFactor}x` : ''));
|
|
1560
|
+
console.log(` Artifacts: ${getRelativePath(data.artifacts.dir)}`);
|
|
1561
|
+
console.log(` Refs: ${data.counts.refs} | Console: ${data.counts.consoleErrors} errors, ${data.counts.consoleWarnings} warnings | Network: ${data.counts.networkFailures} failures`);
|
|
1562
|
+
if (data.counts.a11yViolations !== undefined) {
|
|
1563
|
+
console.log(` A11y: ${data.counts.a11yViolations} violations, ${data.counts.a11yIncomplete ?? 0} incomplete`);
|
|
1564
|
+
}
|
|
1565
|
+
if (data.nextActions.length > 0) {
|
|
1566
|
+
console.log('\n Suggested next actions:');
|
|
1567
|
+
for (const action of data.nextActions) {
|
|
1568
|
+
console.log(` - ${action}`);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
console.log(`\n Summary: ${getRelativePath(data.artifacts.summaryMarkdown)}`);
|
|
1572
|
+
console.log(` JSON: ${getRelativePath(data.artifacts.contextJson)}`);
|
|
1573
|
+
console.log(` PNG: ${getRelativePath(data.artifacts.screenshotPng)}`);
|
|
1574
|
+
}
|
|
1550
1575
|
// Per-command help text, keyed by canonical command name
|
|
1551
1576
|
const COMMAND_HELP = {
|
|
1552
1577
|
screenshot: ` screenshot [options]
|
|
@@ -1595,6 +1620,30 @@ const COMMAND_HELP = {
|
|
|
1595
1620
|
pnpm sweetlink screenshot --force-cdp --width 375 --height 667 # Playwright at iPhone SE
|
|
1596
1621
|
pnpm sweetlink screenshot --hifi # Pixel-perfect via daemon
|
|
1597
1622
|
pnpm sweetlink screenshot --responsive # 3 breakpoints via daemon`,
|
|
1623
|
+
inspect: ` inspect [options] (alias: context)
|
|
1624
|
+
Capture one LLM-ready frontend context bundle from the daemon.
|
|
1625
|
+
|
|
1626
|
+
Includes:
|
|
1627
|
+
- Full-page screenshot artifact
|
|
1628
|
+
- Interactive @e refs and accessibility snapshot
|
|
1629
|
+
- Console and network deltas from daemon buffers
|
|
1630
|
+
- Axe accessibility summary when axe-core is available
|
|
1631
|
+
- Page timing/viewport metadata and suggested next actions
|
|
1632
|
+
|
|
1633
|
+
Options:
|
|
1634
|
+
--url <url> Target URL (default: http://localhost:3000)
|
|
1635
|
+
--last <number> Console/network entries to include (default: 50)
|
|
1636
|
+
--label <text> Scenario label for artifact directory
|
|
1637
|
+
--expected <text> Expected outcome to embed in the bundle
|
|
1638
|
+
--action <text> Action transcript item (repeatable)
|
|
1639
|
+
--no-a11y Skip axe accessibility audit
|
|
1640
|
+
--format <type> Output format: text (default), json
|
|
1641
|
+
--output <path> Also write the context JSON to this path
|
|
1642
|
+
|
|
1643
|
+
Examples:
|
|
1644
|
+
pnpm sweetlink inspect --url http://localhost:5173
|
|
1645
|
+
pnpm sweetlink context --label "checkout empty state" --expected "CTA is visible"
|
|
1646
|
+
pnpm sweetlink inspect --format json --output .tmp/inspect.json`,
|
|
1598
1647
|
query: ` query --selector <css-selector> [options]
|
|
1599
1648
|
Query DOM elements and return data
|
|
1600
1649
|
|
|
@@ -1875,6 +1924,45 @@ const COMMAND_HELP = {
|
|
|
1875
1924
|
pnpm sweetlink snapshot -i
|
|
1876
1925
|
pnpm sweetlink click @e3
|
|
1877
1926
|
pnpm sweetlink record stop`,
|
|
1927
|
+
sim: ` sim <ios|android> <command...>
|
|
1928
|
+
Record iOS Simulator or Android Emulator screen while running a command.
|
|
1929
|
+
Wraps \`xcrun simctl io booted recordVideo\` (iOS) or
|
|
1930
|
+
\`adb shell screenrecord\` (Android), writing an .mp4 of what was on
|
|
1931
|
+
screen during your XCUITest / Espresso / fastlane / appium run.
|
|
1932
|
+
|
|
1933
|
+
Options:
|
|
1934
|
+
--output <path> .mp4 path (default: .sweetlink/sim/<label>-<stamp>.mp4)
|
|
1935
|
+
--label <text> Embedded in filename
|
|
1936
|
+
--device <name|udid> Pick a specific simulator/emulator (default: first booted)
|
|
1937
|
+
--time-limit <sec> Android only — caps screen recording (max 180)
|
|
1938
|
+
--ignore-exit Don't propagate the recorded command's exit code
|
|
1939
|
+
|
|
1940
|
+
Requirements:
|
|
1941
|
+
iOS: Xcode + a booted Simulator (Simulator.app)
|
|
1942
|
+
Android: Android Platform Tools (\`adb\`) + a running emulator
|
|
1943
|
+
|
|
1944
|
+
Examples:
|
|
1945
|
+
pnpm sweetlink sim ios "fastlane scan" --device "iPhone 15"
|
|
1946
|
+
pnpm sweetlink sim android "./gradlew connectedAndroidTest"`,
|
|
1947
|
+
term: ` term <command...>
|
|
1948
|
+
Record a shell command's stdout/stderr into asciicast v2 + a self-contained
|
|
1949
|
+
HTML player. Captures real timing; the player has play/pause, 0.1×–4×
|
|
1950
|
+
speed, seek bar, and ANSI colour rendering.
|
|
1951
|
+
|
|
1952
|
+
Options:
|
|
1953
|
+
--output <path> .cast file path (default: .sweetlink/term/<label>-<stamp>.cast)
|
|
1954
|
+
--label <text> Label embedded in the .cast title + filename
|
|
1955
|
+
--app <name> Group artifacts under .sweetlink/<app>/<YYYYMMDD>/<run>/term/
|
|
1956
|
+
--run <id> Override the auto-generated run id (HHMM-SS or $SWEETLINK_RUN)
|
|
1957
|
+
--shell <path> Shell to invoke the command in (default: /bin/sh)
|
|
1958
|
+
--cols <n> Reported terminal width (default: 120)
|
|
1959
|
+
--rows <n> Reported terminal height (default: 30)
|
|
1960
|
+
--ignore-exit Don't propagate the recorded command's exit code
|
|
1961
|
+
|
|
1962
|
+
Examples:
|
|
1963
|
+
pnpm sweetlink term "pytest -v" --label api-tests
|
|
1964
|
+
pnpm sweetlink term "go test ./..." --app my-app --label go-tests
|
|
1965
|
+
pnpm sweetlink term "make build" --output .sweetlink/term/build.cast`,
|
|
1878
1966
|
sessions: ` sessions [list|open]
|
|
1879
1967
|
List or open all recorded sessions in this project.
|
|
1880
1968
|
|
|
@@ -1933,6 +2021,7 @@ const COMMAND_HELP = {
|
|
|
1933
2021
|
const COMMAND_ALIASES = {
|
|
1934
2022
|
measure: 'ruler',
|
|
1935
2023
|
accessibility: 'a11y',
|
|
2024
|
+
context: 'inspect',
|
|
1936
2025
|
};
|
|
1937
2026
|
const GLOBAL_HELP = `
|
|
1938
2027
|
Global Flags:
|
|
@@ -1973,9 +2062,12 @@ Commands:`);
|
|
|
1973
2062
|
// Each block looks like:
|
|
1974
2063
|
// " command [args]\n First-line description.\n..."
|
|
1975
2064
|
// We pick the first non-empty line after the signature.
|
|
1976
|
-
const lines = help
|
|
2065
|
+
const lines = help
|
|
2066
|
+
.split('\n')
|
|
2067
|
+
.map((l) => l.trim())
|
|
2068
|
+
.filter(Boolean);
|
|
1977
2069
|
const desc = lines[1] ?? '';
|
|
1978
|
-
const summary = desc.length > 70 ? desc.slice(0, 67)
|
|
2070
|
+
const summary = desc.length > 70 ? `${desc.slice(0, 67)}…` : desc;
|
|
1979
2071
|
console.log(` ${name.padEnd(14)} ${summary}`);
|
|
1980
2072
|
}
|
|
1981
2073
|
if (process.argv.includes('--all')) {
|
|
@@ -2002,12 +2094,161 @@ function showCommandHelp(command) {
|
|
|
2002
2094
|
}
|
|
2003
2095
|
// CLI argument parsing
|
|
2004
2096
|
const args = process.argv.slice(2);
|
|
2005
|
-
// Skip global flags to find the actual command
|
|
2006
|
-
|
|
2097
|
+
// Skip global flags to find the actual command. Falling back to args[0]
|
|
2098
|
+
// would surface flags like `--json` as the command — we want undefined
|
|
2099
|
+
// in that case so the no-command paths (help / batch mode) can fire.
|
|
2100
|
+
const commandType = args.find((a) => !a.startsWith('--'));
|
|
2007
2101
|
if (!commandType || commandType === '--help' || commandType === '-h') {
|
|
2102
|
+
// `sweetlink --json` with stdin → multi-capture batch mode.
|
|
2103
|
+
// Reads { action: "capture", captures: [...] } from stdin and runs
|
|
2104
|
+
// each entry, aggregating results into a single JSON envelope on stdout.
|
|
2105
|
+
if (args.includes('--json') && !process.stdin.isTTY) {
|
|
2106
|
+
// Top-level await keeps the rest of the dispatch from running while
|
|
2107
|
+
// the batch is in flight; we exit before fallthrough either way.
|
|
2108
|
+
try {
|
|
2109
|
+
await runBatchFromStdin();
|
|
2110
|
+
process.exit(0);
|
|
2111
|
+
}
|
|
2112
|
+
catch (err) {
|
|
2113
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2114
|
+
process.stdout.write(JSON.stringify({ ok: false, error: msg }) + '\n');
|
|
2115
|
+
process.exit(1);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2008
2118
|
showHelp();
|
|
2009
2119
|
process.exit(0);
|
|
2010
2120
|
}
|
|
2121
|
+
async function runBatchFromStdin() {
|
|
2122
|
+
const chunks = [];
|
|
2123
|
+
for await (const chunk of process.stdin)
|
|
2124
|
+
chunks.push(chunk);
|
|
2125
|
+
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
2126
|
+
if (!raw) {
|
|
2127
|
+
throw new Error('No input on stdin. Pipe a JSON document with {"action":"capture","captures":[...]}.');
|
|
2128
|
+
}
|
|
2129
|
+
let body;
|
|
2130
|
+
try {
|
|
2131
|
+
body = JSON.parse(raw);
|
|
2132
|
+
}
|
|
2133
|
+
catch (e) {
|
|
2134
|
+
throw new Error(`Could not parse stdin as JSON: ${e instanceof Error ? e.message : e}`);
|
|
2135
|
+
}
|
|
2136
|
+
if (body.action !== 'capture' || !Array.isArray(body.captures)) {
|
|
2137
|
+
throw new Error('Expected { "action": "capture", "captures": [...] }.');
|
|
2138
|
+
}
|
|
2139
|
+
const startTime = Date.now();
|
|
2140
|
+
const results = [];
|
|
2141
|
+
for (const cap of body.captures) {
|
|
2142
|
+
const t0 = Date.now();
|
|
2143
|
+
const mode = cap.mode;
|
|
2144
|
+
const label = cap.label;
|
|
2145
|
+
try {
|
|
2146
|
+
const data = await runOneBatchCapture(cap);
|
|
2147
|
+
results.push({ ok: true, mode, label, data, duration: Date.now() - t0 });
|
|
2148
|
+
}
|
|
2149
|
+
catch (err) {
|
|
2150
|
+
results.push({
|
|
2151
|
+
ok: false,
|
|
2152
|
+
mode,
|
|
2153
|
+
label,
|
|
2154
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2155
|
+
duration: Date.now() - t0,
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
const allOk = results.every((r) => r.ok);
|
|
2160
|
+
process.stdout.write(JSON.stringify({
|
|
2161
|
+
ok: allOk,
|
|
2162
|
+
duration: Date.now() - startTime,
|
|
2163
|
+
captures: results,
|
|
2164
|
+
}) + '\n');
|
|
2165
|
+
if (!allOk)
|
|
2166
|
+
process.exit(1);
|
|
2167
|
+
}
|
|
2168
|
+
async function runOneBatchCapture(cap) {
|
|
2169
|
+
const mode = cap.mode;
|
|
2170
|
+
if (!mode)
|
|
2171
|
+
throw new Error('Capture entry missing required "mode" field.');
|
|
2172
|
+
if (mode === 'term') {
|
|
2173
|
+
const command = cap.command;
|
|
2174
|
+
if (!command)
|
|
2175
|
+
throw new Error('term capture missing "command".');
|
|
2176
|
+
const label = cap.label ?? 'batch';
|
|
2177
|
+
const labelSlug = label
|
|
2178
|
+
.replace(/[^a-z0-9]/gi, '-')
|
|
2179
|
+
.toLowerCase()
|
|
2180
|
+
.slice(0, 40);
|
|
2181
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
2182
|
+
const output = cap.output ??
|
|
2183
|
+
path.join(findProjectRoot(), '.sweetlink', 'term', `${labelSlug}-${stamp}.cast`);
|
|
2184
|
+
ensureDir(output);
|
|
2185
|
+
const { captureTerminal } = await import('../term/recorder.js');
|
|
2186
|
+
const { generatePlayer } = await import('../term/player.js');
|
|
2187
|
+
const result = await captureTerminal({
|
|
2188
|
+
command,
|
|
2189
|
+
output,
|
|
2190
|
+
label,
|
|
2191
|
+
shell: cap.shell,
|
|
2192
|
+
cols: typeof cap.cols === 'number' ? cap.cols : undefined,
|
|
2193
|
+
rows: typeof cap.rows === 'number' ? cap.rows : undefined,
|
|
2194
|
+
});
|
|
2195
|
+
const playerPath = await generatePlayer({ castPath: output });
|
|
2196
|
+
return {
|
|
2197
|
+
castPath: output,
|
|
2198
|
+
playerPath,
|
|
2199
|
+
durationSec: result.durationSec,
|
|
2200
|
+
exitCode: result.exitCode,
|
|
2201
|
+
events: result.events,
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
if (mode === 'sim-ios' || mode === 'sim-android') {
|
|
2205
|
+
const command = cap.command;
|
|
2206
|
+
if (!command)
|
|
2207
|
+
throw new Error(`${mode} capture missing "command".`);
|
|
2208
|
+
const label = cap.label ?? 'batch';
|
|
2209
|
+
const labelSlug = label
|
|
2210
|
+
.replace(/[^a-z0-9]/gi, '-')
|
|
2211
|
+
.toLowerCase()
|
|
2212
|
+
.slice(0, 40);
|
|
2213
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
2214
|
+
const output = cap.output ??
|
|
2215
|
+
path.join(findProjectRoot(), '.sweetlink', 'sim', `${labelSlug}-${stamp}.mp4`);
|
|
2216
|
+
ensureDir(output);
|
|
2217
|
+
const device = cap.device;
|
|
2218
|
+
if (mode === 'sim-ios') {
|
|
2219
|
+
const { recordIosSimulator } = await import('../simulator/ios.js');
|
|
2220
|
+
return recordIosSimulator({ command, output, device });
|
|
2221
|
+
}
|
|
2222
|
+
const { recordAndroidEmulator } = await import('../simulator/android.js');
|
|
2223
|
+
return recordAndroidEmulator({
|
|
2224
|
+
command,
|
|
2225
|
+
output,
|
|
2226
|
+
device,
|
|
2227
|
+
timeLimit: typeof cap.timeLimit === 'number' ? cap.timeLimit : undefined,
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
if (mode === 'screenshot') {
|
|
2231
|
+
const targetUrl = cap.url ?? 'http://localhost:3000';
|
|
2232
|
+
const projRoot = findProjectRoot();
|
|
2233
|
+
const state = await ensureDaemon(projRoot, targetUrl);
|
|
2234
|
+
const resp = await daemonRequest(state, 'screenshot', {
|
|
2235
|
+
selector: cap.selector,
|
|
2236
|
+
fullPage: cap.fullPage,
|
|
2237
|
+
viewport: cap.viewport,
|
|
2238
|
+
padding: cap.padding,
|
|
2239
|
+
theme: cap.theme,
|
|
2240
|
+
});
|
|
2241
|
+
const data = resp.data;
|
|
2242
|
+
if (cap.output) {
|
|
2243
|
+
const outputPath = cap.output;
|
|
2244
|
+
ensureDir(outputPath);
|
|
2245
|
+
fs.writeFileSync(outputPath, Buffer.from(data.screenshot, 'base64'));
|
|
2246
|
+
return { path: outputPath, width: data.width, height: data.height };
|
|
2247
|
+
}
|
|
2248
|
+
return { width: data.width, height: data.height, base64Length: data.screenshot.length };
|
|
2249
|
+
}
|
|
2250
|
+
throw new Error(`Unknown capture mode: ${mode}. Allowed: term, sim-ios, sim-android, screenshot.`);
|
|
2251
|
+
}
|
|
2011
2252
|
// Helper function to get argument value
|
|
2012
2253
|
function getArg(flag) {
|
|
2013
2254
|
const index = args.indexOf(flag);
|
|
@@ -2017,6 +2258,7 @@ function hasFlag(flag) {
|
|
|
2017
2258
|
return args.includes(flag);
|
|
2018
2259
|
}
|
|
2019
2260
|
// Per-command --help: `pnpm sweetlink screenshot --help`
|
|
2261
|
+
// Past the early-exit at the top of dispatch, commandType is non-null.
|
|
2020
2262
|
if (hasFlag('--help') || hasFlag('-h')) {
|
|
2021
2263
|
showCommandHelp(commandType);
|
|
2022
2264
|
process.exit(0);
|
|
@@ -2026,6 +2268,8 @@ if (hasFlag('--output-schema')) {
|
|
|
2026
2268
|
// If commandType is a known command, print just that schema; otherwise print all
|
|
2027
2269
|
const knownCommands = [
|
|
2028
2270
|
'screenshot',
|
|
2271
|
+
'inspect',
|
|
2272
|
+
'context',
|
|
2029
2273
|
'query',
|
|
2030
2274
|
'logs',
|
|
2031
2275
|
'exec',
|
|
@@ -2047,6 +2291,8 @@ if (hasFlag('--output-schema')) {
|
|
|
2047
2291
|
'fill',
|
|
2048
2292
|
'console',
|
|
2049
2293
|
'record',
|
|
2294
|
+
'term',
|
|
2295
|
+
'sim',
|
|
2050
2296
|
'sessions',
|
|
2051
2297
|
'proof',
|
|
2052
2298
|
'report',
|
|
@@ -2057,7 +2303,9 @@ if (hasFlag('--output-schema')) {
|
|
|
2057
2303
|
? 'ruler'
|
|
2058
2304
|
: commandType === 'accessibility'
|
|
2059
2305
|
? 'a11y'
|
|
2060
|
-
: commandType
|
|
2306
|
+
: commandType === 'context'
|
|
2307
|
+
? 'inspect'
|
|
2308
|
+
: commandType
|
|
2061
2309
|
: undefined;
|
|
2062
2310
|
printOutputSchema(schemaCommand);
|
|
2063
2311
|
process.exit(0);
|
|
@@ -2091,6 +2339,24 @@ function setupJsonMode(command, startTime) {
|
|
|
2091
2339
|
});
|
|
2092
2340
|
return { origExit, getLastError: () => lastErrorMsg };
|
|
2093
2341
|
}
|
|
2342
|
+
function getErrorData(error) {
|
|
2343
|
+
if (error instanceof DaemonRequestError) {
|
|
2344
|
+
return {
|
|
2345
|
+
action: error.action,
|
|
2346
|
+
status: error.status,
|
|
2347
|
+
...(error.data ? error.data : {}),
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
return null;
|
|
2351
|
+
}
|
|
2352
|
+
function printErrorContext(error) {
|
|
2353
|
+
if (!(error instanceof DaemonRequestError) || !error.data)
|
|
2354
|
+
return;
|
|
2355
|
+
const failureScreenshot = error.data.failureScreenshot;
|
|
2356
|
+
if (typeof failureScreenshot === 'string') {
|
|
2357
|
+
console.error(`[Sweetlink] Failure screenshot: ${failureScreenshot}`);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2094
2360
|
/**
|
|
2095
2361
|
* Handle the `wait` command: wait for a server to be ready.
|
|
2096
2362
|
*/
|
|
@@ -2141,9 +2407,14 @@ async function handleStatusCommand() {
|
|
|
2141
2407
|
}
|
|
2142
2408
|
(async () => {
|
|
2143
2409
|
const startTime = Date.now();
|
|
2144
|
-
// Resolve --app flag:
|
|
2410
|
+
// Resolve --app flag: for WS-bridge commands, this discovers the matching
|
|
2411
|
+
// Sweetlink server by branch/app name. For commands that produce artifacts
|
|
2412
|
+
// (term/sim), --app is a *namespace* used in the artifact directory layout
|
|
2413
|
+
// (.sweetlink/<app>/<YYYYMMDD>/<run>/...) — those handlers read --app
|
|
2414
|
+
// themselves, so we skip discovery here.
|
|
2145
2415
|
const appTarget = getArg('--app');
|
|
2146
|
-
|
|
2416
|
+
const isArtifactCommand = commandType === 'term' || commandType === 'sim';
|
|
2417
|
+
if (appTarget && !isArtifactCommand) {
|
|
2147
2418
|
try {
|
|
2148
2419
|
resolvedWsUrl = await discoverServer(appTarget);
|
|
2149
2420
|
console.log(`[Sweetlink] Targeting server: ${resolvedWsUrl}`);
|
|
@@ -2184,6 +2455,42 @@ async function handleStatusCommand() {
|
|
|
2184
2455
|
: undefined,
|
|
2185
2456
|
});
|
|
2186
2457
|
break;
|
|
2458
|
+
case 'inspect':
|
|
2459
|
+
case 'context': {
|
|
2460
|
+
const projRoot = findProjectRoot();
|
|
2461
|
+
const targetUrl = getArg('--url') ?? 'http://localhost:3000';
|
|
2462
|
+
const state = await ensureDaemon(projRoot, targetUrl);
|
|
2463
|
+
const actionTranscript = [];
|
|
2464
|
+
args.forEach((arg, index) => {
|
|
2465
|
+
if (arg !== '--action')
|
|
2466
|
+
return;
|
|
2467
|
+
const value = args[index + 1];
|
|
2468
|
+
if (!value)
|
|
2469
|
+
return;
|
|
2470
|
+
actionTranscript.push({ action: value });
|
|
2471
|
+
});
|
|
2472
|
+
const resp = await daemonRequest(state, 'inspect', {
|
|
2473
|
+
last: getArg('--last') ? parseInt(getArg('--last'), 10) : undefined,
|
|
2474
|
+
label: getArg('--label'),
|
|
2475
|
+
expectedOutcome: getArg('--expected'),
|
|
2476
|
+
actionTranscript,
|
|
2477
|
+
includeA11y: !hasFlag('--no-a11y'),
|
|
2478
|
+
});
|
|
2479
|
+
const data = resp.data;
|
|
2480
|
+
const output = getArg('--output');
|
|
2481
|
+
if (output) {
|
|
2482
|
+
ensureDir(output);
|
|
2483
|
+
fs.writeFileSync(output, JSON.stringify(data, null, 2), 'utf-8');
|
|
2484
|
+
}
|
|
2485
|
+
if (getArg('--format') === 'json') {
|
|
2486
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2487
|
+
}
|
|
2488
|
+
else {
|
|
2489
|
+
printInspectSummary(data);
|
|
2490
|
+
}
|
|
2491
|
+
result = data;
|
|
2492
|
+
break;
|
|
2493
|
+
}
|
|
2187
2494
|
case 'query': {
|
|
2188
2495
|
const selector = getArg('--selector');
|
|
2189
2496
|
if (!selector) {
|
|
@@ -2279,7 +2586,9 @@ async function handleStatusCommand() {
|
|
|
2279
2586
|
}
|
|
2280
2587
|
}
|
|
2281
2588
|
}
|
|
2282
|
-
catch {
|
|
2589
|
+
catch {
|
|
2590
|
+
/* fall through to WS path */
|
|
2591
|
+
}
|
|
2283
2592
|
result = await click({
|
|
2284
2593
|
selector: clickTarget,
|
|
2285
2594
|
text: clickText,
|
|
@@ -2407,7 +2716,8 @@ async function handleStatusCommand() {
|
|
|
2407
2716
|
}
|
|
2408
2717
|
const sessionDirArg = getArg('--session') ?? '.sweetlink';
|
|
2409
2718
|
// Find latest session manifest
|
|
2410
|
-
const sessionFiles = fs
|
|
2719
|
+
const sessionFiles = fs
|
|
2720
|
+
.readdirSync(sessionDirArg)
|
|
2411
2721
|
.filter((f) => f.startsWith('session-'))
|
|
2412
2722
|
.sort()
|
|
2413
2723
|
.reverse();
|
|
@@ -2462,16 +2772,19 @@ async function handleStatusCommand() {
|
|
|
2462
2772
|
const data = resp.data;
|
|
2463
2773
|
const m = data.manifest;
|
|
2464
2774
|
console.log(`[Sweetlink] Recording stopped: ${m.sessionId}`);
|
|
2465
|
-
console.log(` Duration: ${m.duration.toFixed(1)}s | Actions: ${m.commands.length}${m.video ?
|
|
2775
|
+
console.log(` Duration: ${m.duration.toFixed(1)}s | Actions: ${m.commands.length}${m.video ? ` | Video: ${m.video}` : ''}`);
|
|
2466
2776
|
// Auto-open the viewer (cross-platform; --no-open to suppress)
|
|
2467
2777
|
if (data.viewerPath && !hasFlag('--no-open')) {
|
|
2468
2778
|
console.log(` Viewer: ${data.viewerPath}`);
|
|
2469
2779
|
const { execFile } = await import('child_process');
|
|
2470
2780
|
// `start` on Windows is a cmd builtin, not an exe — must invoke via cmd.
|
|
2471
|
-
const cmd = process.platform === 'darwin'
|
|
2472
|
-
|
|
2781
|
+
const cmd = process.platform === 'darwin'
|
|
2782
|
+
? 'open'
|
|
2783
|
+
: process.platform === 'win32'
|
|
2784
|
+
? 'cmd'
|
|
2473
2785
|
: 'xdg-open';
|
|
2474
|
-
const args = process.platform === 'win32'
|
|
2786
|
+
const args = process.platform === 'win32'
|
|
2787
|
+
? ['/c', 'start', '', data.viewerPath]
|
|
2475
2788
|
: [data.viewerPath];
|
|
2476
2789
|
execFile(cmd, args, (err) => {
|
|
2477
2790
|
if (err)
|
|
@@ -2514,7 +2827,10 @@ async function handleStatusCommand() {
|
|
|
2514
2827
|
const startResp = await daemonRequest(state, 'record-start', label ? { label } : {});
|
|
2515
2828
|
const startData = startResp.data;
|
|
2516
2829
|
console.log(`[Sweetlink] Recording: ${startData.sessionId}${label ? ` (${label})` : ''}`);
|
|
2517
|
-
const steps = script
|
|
2830
|
+
const steps = script
|
|
2831
|
+
.split(';')
|
|
2832
|
+
.map((s) => s.trim())
|
|
2833
|
+
.filter(Boolean);
|
|
2518
2834
|
// Snapshot once up-front so refs resolve.
|
|
2519
2835
|
await daemonRequest(state, 'snapshot', { interactive: true });
|
|
2520
2836
|
for (const step of steps) {
|
|
@@ -2598,7 +2914,8 @@ async function handleStatusCommand() {
|
|
|
2598
2914
|
console.error(`[Sweetlink] Session directory not found: ${sessionDirArg}`);
|
|
2599
2915
|
process.exit(1);
|
|
2600
2916
|
}
|
|
2601
|
-
const reportSessions = fs
|
|
2917
|
+
const reportSessions = fs
|
|
2918
|
+
.readdirSync(sessionDirArg)
|
|
2602
2919
|
.filter((f) => f.startsWith('session-'))
|
|
2603
2920
|
.sort()
|
|
2604
2921
|
.reverse();
|
|
@@ -2675,7 +2992,10 @@ async function handleStatusCommand() {
|
|
|
2675
2992
|
}
|
|
2676
2993
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
2677
2994
|
const summary = fs.existsSync(summaryPath) ? fs.readFileSync(summaryPath, 'utf-8') : '';
|
|
2678
|
-
const payload = {
|
|
2995
|
+
const payload = {
|
|
2996
|
+
summary,
|
|
2997
|
+
manifest,
|
|
2998
|
+
};
|
|
2679
2999
|
// Include viewer HTML for Slack/Discord webhooks
|
|
2680
3000
|
if (/slack|discord/i.test(webhookUrl)) {
|
|
2681
3001
|
const viewerPath = path.join(reportSessionDir, 'viewer.html');
|
|
@@ -2711,6 +3031,169 @@ async function handleStatusCommand() {
|
|
|
2711
3031
|
}
|
|
2712
3032
|
break;
|
|
2713
3033
|
}
|
|
3034
|
+
case 'sim': {
|
|
3035
|
+
// Record iOS Simulator or Android Emulator screen while running a command.
|
|
3036
|
+
// Example: sweetlink sim ios "fastlane scan" --device "iPhone 15"
|
|
3037
|
+
const platform = args[1];
|
|
3038
|
+
if (platform !== 'ios' && platform !== 'android') {
|
|
3039
|
+
console.error('[Sweetlink] Usage: sweetlink sim <ios|android> "<command>" [--output path] [--device <name>]');
|
|
3040
|
+
process.exit(1);
|
|
3041
|
+
}
|
|
3042
|
+
const flagsWithValues = new Set([
|
|
3043
|
+
'--output',
|
|
3044
|
+
'--label',
|
|
3045
|
+
'--device',
|
|
3046
|
+
'--time-limit',
|
|
3047
|
+
'--app',
|
|
3048
|
+
'--run',
|
|
3049
|
+
]);
|
|
3050
|
+
const positional = [];
|
|
3051
|
+
for (let i = 2; i < args.length; i++) {
|
|
3052
|
+
const a = args[i];
|
|
3053
|
+
if (a.startsWith('--')) {
|
|
3054
|
+
if (flagsWithValues.has(a))
|
|
3055
|
+
i++;
|
|
3056
|
+
continue;
|
|
3057
|
+
}
|
|
3058
|
+
positional.push(a);
|
|
3059
|
+
}
|
|
3060
|
+
const command = positional.join(' ').trim();
|
|
3061
|
+
if (!command) {
|
|
3062
|
+
console.error(`[Sweetlink] Error: sim ${platform} requires a command. Example: sweetlink sim ${platform} "fastlane scan"`);
|
|
3063
|
+
process.exit(1);
|
|
3064
|
+
}
|
|
3065
|
+
const label = getArg('--label');
|
|
3066
|
+
const labelSlug = label
|
|
3067
|
+
? label
|
|
3068
|
+
.replace(/[^a-z0-9]/gi, '-')
|
|
3069
|
+
.toLowerCase()
|
|
3070
|
+
.slice(0, 40)
|
|
3071
|
+
: `sim-${platform}`;
|
|
3072
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
3073
|
+
const { runSlot: simRunSlot } = await import('../runs.js');
|
|
3074
|
+
const defaultDir = simRunSlot({
|
|
3075
|
+
baseDir: findProjectRoot(),
|
|
3076
|
+
app: getArg('--app'),
|
|
3077
|
+
run: getArg('--run'),
|
|
3078
|
+
kind: 'sim',
|
|
3079
|
+
});
|
|
3080
|
+
const output = getArg('--output') ?? path.join(defaultDir, `${labelSlug}-${stamp}.mp4`);
|
|
3081
|
+
ensureDir(output);
|
|
3082
|
+
const device = getArg('--device');
|
|
3083
|
+
console.log(`[Sweetlink] Recording ${platform} simulator: ${command}`);
|
|
3084
|
+
let recResult;
|
|
3085
|
+
if (platform === 'ios') {
|
|
3086
|
+
const { recordIosSimulator } = await import('../simulator/ios.js');
|
|
3087
|
+
recResult = await recordIosSimulator({ command, output, device });
|
|
3088
|
+
}
|
|
3089
|
+
else {
|
|
3090
|
+
const { recordAndroidEmulator } = await import('../simulator/android.js');
|
|
3091
|
+
const tl = getArg('--time-limit');
|
|
3092
|
+
recResult = await recordAndroidEmulator({
|
|
3093
|
+
command,
|
|
3094
|
+
output,
|
|
3095
|
+
device,
|
|
3096
|
+
timeLimit: tl ? parseInt(tl, 10) : undefined,
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3099
|
+
let sizeKb = '?';
|
|
3100
|
+
try {
|
|
3101
|
+
sizeKb = String(Math.round(fs.statSync(output).size / 1024));
|
|
3102
|
+
}
|
|
3103
|
+
catch {
|
|
3104
|
+
/* file may not exist if recordingClosed is false */
|
|
3105
|
+
}
|
|
3106
|
+
console.log(`[Sweetlink] ${recResult.recordingClosed ? '✓' : '⚠'} ${getRelativePath(output)} · ` +
|
|
3107
|
+
`${recResult.durationSec.toFixed(1)}s · ${sizeKb}KB · ${recResult.device} · exit=${recResult.exitCode}` +
|
|
3108
|
+
(recResult.recordingClosed
|
|
3109
|
+
? ''
|
|
3110
|
+
: ' (recording was force-killed; mp4 may be incomplete)'));
|
|
3111
|
+
result = {
|
|
3112
|
+
path: output,
|
|
3113
|
+
device: recResult.device,
|
|
3114
|
+
durationSec: recResult.durationSec,
|
|
3115
|
+
exitCode: recResult.exitCode,
|
|
3116
|
+
recordingClosed: recResult.recordingClosed,
|
|
3117
|
+
};
|
|
3118
|
+
if (recResult.exitCode !== 0 && !hasFlag('--ignore-exit')) {
|
|
3119
|
+
process.exit(recResult.exitCode);
|
|
3120
|
+
}
|
|
3121
|
+
break;
|
|
3122
|
+
}
|
|
3123
|
+
case 'term': {
|
|
3124
|
+
// Record a shell command's stdout/stderr into asciicast v2 + HTML player.
|
|
3125
|
+
// Example: sweetlink term "pytest -v" --label api-tests --app my-app
|
|
3126
|
+
const flagsWithValues = new Set([
|
|
3127
|
+
'--output',
|
|
3128
|
+
'--label',
|
|
3129
|
+
'--shell',
|
|
3130
|
+
'--cols',
|
|
3131
|
+
'--rows',
|
|
3132
|
+
'--app',
|
|
3133
|
+
'--run',
|
|
3134
|
+
]);
|
|
3135
|
+
const positional = [];
|
|
3136
|
+
for (let i = 1; i < args.length; i++) {
|
|
3137
|
+
const a = args[i];
|
|
3138
|
+
if (a.startsWith('--')) {
|
|
3139
|
+
if (flagsWithValues.has(a))
|
|
3140
|
+
i++;
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
positional.push(a);
|
|
3144
|
+
}
|
|
3145
|
+
const command = positional.join(' ').trim();
|
|
3146
|
+
if (!command) {
|
|
3147
|
+
console.error('[Sweetlink] Error: term requires a command. Example: sweetlink term "pytest tests/"');
|
|
3148
|
+
process.exit(1);
|
|
3149
|
+
}
|
|
3150
|
+
const label = getArg('--label');
|
|
3151
|
+
const labelSlug = label
|
|
3152
|
+
? label
|
|
3153
|
+
.replace(/[^a-z0-9]/gi, '-')
|
|
3154
|
+
.toLowerCase()
|
|
3155
|
+
.slice(0, 40)
|
|
3156
|
+
: 'term';
|
|
3157
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
3158
|
+
const { runSlot } = await import('../runs.js');
|
|
3159
|
+
const defaultDir = runSlot({
|
|
3160
|
+
baseDir: findProjectRoot(),
|
|
3161
|
+
app: getArg('--app'),
|
|
3162
|
+
run: getArg('--run'),
|
|
3163
|
+
kind: 'term',
|
|
3164
|
+
});
|
|
3165
|
+
const output = getArg('--output') ?? path.join(defaultDir, `${labelSlug}-${stamp}.cast`);
|
|
3166
|
+
ensureDir(output);
|
|
3167
|
+
console.log(`[Sweetlink] Recording terminal: ${command}`);
|
|
3168
|
+
const { captureTerminal } = await import('../term/recorder.js');
|
|
3169
|
+
const { generatePlayer } = await import('../term/player.js');
|
|
3170
|
+
const cap = await captureTerminal({
|
|
3171
|
+
command,
|
|
3172
|
+
output,
|
|
3173
|
+
label,
|
|
3174
|
+
shell: getArg('--shell'),
|
|
3175
|
+
cols: getArg('--cols') ? parseInt(getArg('--cols'), 10) : undefined,
|
|
3176
|
+
rows: getArg('--rows') ? parseInt(getArg('--rows'), 10) : undefined,
|
|
3177
|
+
});
|
|
3178
|
+
const playerPath = await generatePlayer({ castPath: output });
|
|
3179
|
+
console.log(`[Sweetlink] ✓ ${getRelativePath(output)} · ${cap.durationSec.toFixed(1)}s · ` +
|
|
3180
|
+
`${cap.events} events · ${(cap.bytes / 1024).toFixed(0)}KB · exit=${cap.exitCode}`);
|
|
3181
|
+
console.log(`[Sweetlink] ▶ ${getRelativePath(playerPath)}`);
|
|
3182
|
+
result = {
|
|
3183
|
+
castPath: output,
|
|
3184
|
+
playerPath,
|
|
3185
|
+
durationSec: cap.durationSec,
|
|
3186
|
+
bytes: cap.bytes,
|
|
3187
|
+
events: cap.events,
|
|
3188
|
+
exitCode: cap.exitCode,
|
|
3189
|
+
};
|
|
3190
|
+
// Propagate the recorded command's exit code by default so CI fails
|
|
3191
|
+
// when the wrapped tests fail.
|
|
3192
|
+
if (cap.exitCode !== 0 && !hasFlag('--ignore-exit')) {
|
|
3193
|
+
process.exit(cap.exitCode);
|
|
3194
|
+
}
|
|
3195
|
+
break;
|
|
3196
|
+
}
|
|
2714
3197
|
case 'sessions': {
|
|
2715
3198
|
const sub = args[1];
|
|
2716
3199
|
const projRoot = findProjectRoot();
|
|
@@ -2777,8 +3260,20 @@ async function handleStatusCommand() {
|
|
|
2777
3260
|
console.log('\nAction sequences are identical.');
|
|
2778
3261
|
}
|
|
2779
3262
|
result = {
|
|
2780
|
-
a: {
|
|
2781
|
-
|
|
3263
|
+
a: {
|
|
3264
|
+
id: a.sessionId,
|
|
3265
|
+
label: a.label,
|
|
3266
|
+
duration: a.duration,
|
|
3267
|
+
actions: a.actionCount,
|
|
3268
|
+
errors: aErr,
|
|
3269
|
+
},
|
|
3270
|
+
b: {
|
|
3271
|
+
id: b.sessionId,
|
|
3272
|
+
label: b.label,
|
|
3273
|
+
duration: b.duration,
|
|
3274
|
+
actions: b.actionCount,
|
|
3275
|
+
errors: bErr,
|
|
3276
|
+
},
|
|
2782
3277
|
added,
|
|
2783
3278
|
removed,
|
|
2784
3279
|
};
|
|
@@ -2787,12 +3282,12 @@ async function handleStatusCommand() {
|
|
|
2787
3282
|
// Open the index.html in the browser
|
|
2788
3283
|
if (data.indexPath) {
|
|
2789
3284
|
const { execFile } = await import('child_process');
|
|
2790
|
-
const cmd = process.platform === 'darwin'
|
|
2791
|
-
|
|
3285
|
+
const cmd = process.platform === 'darwin'
|
|
3286
|
+
? 'open'
|
|
3287
|
+
: process.platform === 'win32'
|
|
3288
|
+
? 'cmd'
|
|
2792
3289
|
: 'xdg-open';
|
|
2793
|
-
const cmdArgs = process.platform === 'win32'
|
|
2794
|
-
? ['/c', 'start', '', data.indexPath]
|
|
2795
|
-
: [data.indexPath];
|
|
3290
|
+
const cmdArgs = process.platform === 'win32' ? ['/c', 'start', '', data.indexPath] : [data.indexPath];
|
|
2796
3291
|
execFile(cmd, cmdArgs, () => { });
|
|
2797
3292
|
console.log(`[Sweetlink] Opened ${data.indexPath}`);
|
|
2798
3293
|
}
|
|
@@ -2900,10 +3395,13 @@ async function handleStatusCommand() {
|
|
|
2900
3395
|
console.log(`[Sweetlink] Demo: "${state.title}" (${state.sections.length} sections)`);
|
|
2901
3396
|
console.log(` File: ${state.filePath}`);
|
|
2902
3397
|
for (const s of state.sections) {
|
|
2903
|
-
const preview = s.type === 'note'
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
3398
|
+
const preview = s.type === 'note'
|
|
3399
|
+
? s.content.substring(0, 60)
|
|
3400
|
+
: s.type === 'exec'
|
|
3401
|
+
? `$ ${s.command}`
|
|
3402
|
+
: s.type === 'screenshot'
|
|
3403
|
+
? `[image] ${s.screenshotFile}`
|
|
3404
|
+
: '[snapshot]';
|
|
2907
3405
|
console.log(` ${s.type.padEnd(12)} ${preview}`);
|
|
2908
3406
|
}
|
|
2909
3407
|
result = state;
|
|
@@ -3024,8 +3522,10 @@ async function handleStatusCommand() {
|
|
|
3024
3522
|
const msg = error instanceof Error ? error.message : String(error);
|
|
3025
3523
|
emitJson({
|
|
3026
3524
|
ok: false,
|
|
3525
|
+
// commandType is non-undefined here: the early-exit at the top of
|
|
3526
|
+
// CLI dispatch handles the bare `--json` (batch mode) case.
|
|
3027
3527
|
command: commandType,
|
|
3028
|
-
data:
|
|
3528
|
+
data: getErrorData(error),
|
|
3029
3529
|
error: msg,
|
|
3030
3530
|
duration: Date.now() - startTime,
|
|
3031
3531
|
});
|
|
@@ -3035,6 +3535,7 @@ async function handleStatusCommand() {
|
|
|
3035
3535
|
// to end users and clutters the output. Set SWEETLINK_DEBUG=1 to see it.
|
|
3036
3536
|
if (error instanceof Error) {
|
|
3037
3537
|
console.error(`[Sweetlink] ${error.message}`);
|
|
3538
|
+
printErrorContext(error);
|
|
3038
3539
|
if (process.env.SWEETLINK_DEBUG === '1' && error.stack) {
|
|
3039
3540
|
console.error(error.stack);
|
|
3040
3541
|
}
|