@ytspar/sweetlink 1.19.0 → 1.21.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 +400 -48
- 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 +6 -6
- 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 +8 -6
- 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 +38 -15
- package/dist/daemon/recording.js.map +1 -1
- package/dist/daemon/refs.d.ts.map +1 -1
- package/dist/daemon/refs.js +1 -1
- package/dist/daemon/refs.js.map +1 -1
- package/dist/daemon/ringBuffer.d.ts +8 -0
- package/dist/daemon/ringBuffer.d.ts.map +1 -1
- package/dist/daemon/ringBuffer.js +17 -0
- package/dist/daemon/ringBuffer.js.map +1 -1
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +490 -86
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/stateFile.js +2 -2
- package/dist/daemon/stateFile.js.map +1 -1
- package/dist/daemon/summary.d.ts +1 -1
- package/dist/daemon/summary.d.ts.map +1 -1
- package/dist/daemon/summary.js +2 -2
- package/dist/daemon/summary.js.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 +21 -13
- package/dist/daemon/viewer.js.map +1 -1
- package/dist/daemon/visualDiff.js +1 -1
- package/dist/daemon/visualDiff.js.map +1 -1
- package/dist/next.js +3 -3
- package/dist/next.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 +13 -0
- package/dist/simulator/android.d.ts.map +1 -1
- package/dist/simulator/android.js +75 -5
- package/dist/simulator/android.js.map +1 -1
- package/dist/simulator/androidTaps.d.ts +99 -0
- package/dist/simulator/androidTaps.d.ts.map +1 -0
- package/dist/simulator/androidTaps.js +162 -0
- package/dist/simulator/androidTaps.js.map +1 -0
- package/dist/simulator/ios.d.ts.map +1 -1
- package/dist/simulator/ios.js +13 -5
- package/dist/simulator/ios.js.map +1 -1
- package/dist/simulator/overlay.d.ts +41 -0
- package/dist/simulator/overlay.d.ts.map +1 -0
- package/dist/simulator/overlay.js +78 -0
- package/dist/simulator/overlay.js.map +1 -0
- package/dist/term/ansi.d.ts.map +1 -1
- package/dist/term/ansi.js +49 -14
- package/dist/term/ansi.js.map +1 -1
- package/dist/term/player.d.ts.map +1 -1
- package/dist/term/player.js +5 -5
- package/dist/term/player.js.map +1 -1
- package/dist/term/recorder.js +1 -1
- package/dist/term/recorder.js.map +1 -1
- 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
|
|
|
@@ -1886,15 +1935,21 @@ const COMMAND_HELP = {
|
|
|
1886
1935
|
--label <text> Embedded in filename
|
|
1887
1936
|
--device <name|udid> Pick a specific simulator/emulator (default: first booted)
|
|
1888
1937
|
--time-limit <sec> Android only — caps screen recording (max 180)
|
|
1938
|
+
--app <name> Group artifacts under .sweetlink/<app>/<YYYYMMDD>/<run>/sim/
|
|
1939
|
+
--run <id> Override the auto-generated run id
|
|
1940
|
+
--no-overlays Android only — skip tap-event capture and ffmpeg overlay
|
|
1889
1941
|
--ignore-exit Don't propagate the recorded command's exit code
|
|
1890
1942
|
|
|
1891
1943
|
Requirements:
|
|
1892
1944
|
iOS: Xcode + a booted Simulator (Simulator.app)
|
|
1893
1945
|
Android: Android Platform Tools (\`adb\`) + a running emulator
|
|
1946
|
+
Tap-indicator overlays additionally need \`ffmpeg\` on PATH;
|
|
1947
|
+
without it, taps are still captured to a sidecar .taps.json.
|
|
1894
1948
|
|
|
1895
1949
|
Examples:
|
|
1896
1950
|
pnpm sweetlink sim ios "fastlane scan" --device "iPhone 15"
|
|
1897
|
-
pnpm sweetlink sim android "./gradlew connectedAndroidTest"
|
|
1951
|
+
pnpm sweetlink sim android "./gradlew connectedAndroidTest"
|
|
1952
|
+
pnpm sweetlink sim android "appium run" --no-overlays`,
|
|
1898
1953
|
term: ` term <command...>
|
|
1899
1954
|
Record a shell command's stdout/stderr into asciicast v2 + a self-contained
|
|
1900
1955
|
HTML player. Captures real timing; the player has play/pause, 0.1×–4×
|
|
@@ -1903,6 +1958,8 @@ const COMMAND_HELP = {
|
|
|
1903
1958
|
Options:
|
|
1904
1959
|
--output <path> .cast file path (default: .sweetlink/term/<label>-<stamp>.cast)
|
|
1905
1960
|
--label <text> Label embedded in the .cast title + filename
|
|
1961
|
+
--app <name> Group artifacts under .sweetlink/<app>/<YYYYMMDD>/<run>/term/
|
|
1962
|
+
--run <id> Override the auto-generated run id (HHMM-SS or $SWEETLINK_RUN)
|
|
1906
1963
|
--shell <path> Shell to invoke the command in (default: /bin/sh)
|
|
1907
1964
|
--cols <n> Reported terminal width (default: 120)
|
|
1908
1965
|
--rows <n> Reported terminal height (default: 30)
|
|
@@ -1910,7 +1967,7 @@ const COMMAND_HELP = {
|
|
|
1910
1967
|
|
|
1911
1968
|
Examples:
|
|
1912
1969
|
pnpm sweetlink term "pytest -v" --label api-tests
|
|
1913
|
-
pnpm sweetlink term "go test ./..." --label go-tests
|
|
1970
|
+
pnpm sweetlink term "go test ./..." --app my-app --label go-tests
|
|
1914
1971
|
pnpm sweetlink term "make build" --output .sweetlink/term/build.cast`,
|
|
1915
1972
|
sessions: ` sessions [list|open]
|
|
1916
1973
|
List or open all recorded sessions in this project.
|
|
@@ -1970,6 +2027,7 @@ const COMMAND_HELP = {
|
|
|
1970
2027
|
const COMMAND_ALIASES = {
|
|
1971
2028
|
measure: 'ruler',
|
|
1972
2029
|
accessibility: 'a11y',
|
|
2030
|
+
context: 'inspect',
|
|
1973
2031
|
};
|
|
1974
2032
|
const GLOBAL_HELP = `
|
|
1975
2033
|
Global Flags:
|
|
@@ -2010,9 +2068,12 @@ Commands:`);
|
|
|
2010
2068
|
// Each block looks like:
|
|
2011
2069
|
// " command [args]\n First-line description.\n..."
|
|
2012
2070
|
// We pick the first non-empty line after the signature.
|
|
2013
|
-
const lines = help
|
|
2071
|
+
const lines = help
|
|
2072
|
+
.split('\n')
|
|
2073
|
+
.map((l) => l.trim())
|
|
2074
|
+
.filter(Boolean);
|
|
2014
2075
|
const desc = lines[1] ?? '';
|
|
2015
|
-
const summary = desc.length > 70 ? desc.slice(0, 67)
|
|
2076
|
+
const summary = desc.length > 70 ? `${desc.slice(0, 67)}…` : desc;
|
|
2016
2077
|
console.log(` ${name.padEnd(14)} ${summary}`);
|
|
2017
2078
|
}
|
|
2018
2079
|
if (process.argv.includes('--all')) {
|
|
@@ -2039,12 +2100,161 @@ function showCommandHelp(command) {
|
|
|
2039
2100
|
}
|
|
2040
2101
|
// CLI argument parsing
|
|
2041
2102
|
const args = process.argv.slice(2);
|
|
2042
|
-
// Skip global flags to find the actual command
|
|
2043
|
-
|
|
2103
|
+
// Skip global flags to find the actual command. Falling back to args[0]
|
|
2104
|
+
// would surface flags like `--json` as the command — we want undefined
|
|
2105
|
+
// in that case so the no-command paths (help / batch mode) can fire.
|
|
2106
|
+
const commandType = args.find((a) => !a.startsWith('--'));
|
|
2044
2107
|
if (!commandType || commandType === '--help' || commandType === '-h') {
|
|
2108
|
+
// `sweetlink --json` with stdin → multi-capture batch mode.
|
|
2109
|
+
// Reads { action: "capture", captures: [...] } from stdin and runs
|
|
2110
|
+
// each entry, aggregating results into a single JSON envelope on stdout.
|
|
2111
|
+
if (args.includes('--json') && !process.stdin.isTTY) {
|
|
2112
|
+
// Top-level await keeps the rest of the dispatch from running while
|
|
2113
|
+
// the batch is in flight; we exit before fallthrough either way.
|
|
2114
|
+
try {
|
|
2115
|
+
await runBatchFromStdin();
|
|
2116
|
+
process.exit(0);
|
|
2117
|
+
}
|
|
2118
|
+
catch (err) {
|
|
2119
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2120
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: msg })}\n`);
|
|
2121
|
+
process.exit(1);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2045
2124
|
showHelp();
|
|
2046
2125
|
process.exit(0);
|
|
2047
2126
|
}
|
|
2127
|
+
async function runBatchFromStdin() {
|
|
2128
|
+
const chunks = [];
|
|
2129
|
+
for await (const chunk of process.stdin)
|
|
2130
|
+
chunks.push(chunk);
|
|
2131
|
+
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
2132
|
+
if (!raw) {
|
|
2133
|
+
throw new Error('No input on stdin. Pipe a JSON document with {"action":"capture","captures":[...]}.');
|
|
2134
|
+
}
|
|
2135
|
+
let body;
|
|
2136
|
+
try {
|
|
2137
|
+
body = JSON.parse(raw);
|
|
2138
|
+
}
|
|
2139
|
+
catch (e) {
|
|
2140
|
+
throw new Error(`Could not parse stdin as JSON: ${e instanceof Error ? e.message : e}`);
|
|
2141
|
+
}
|
|
2142
|
+
if (body.action !== 'capture' || !Array.isArray(body.captures)) {
|
|
2143
|
+
throw new Error('Expected { "action": "capture", "captures": [...] }.');
|
|
2144
|
+
}
|
|
2145
|
+
const startTime = Date.now();
|
|
2146
|
+
const results = [];
|
|
2147
|
+
for (const cap of body.captures) {
|
|
2148
|
+
const t0 = Date.now();
|
|
2149
|
+
const mode = cap.mode;
|
|
2150
|
+
const label = cap.label;
|
|
2151
|
+
try {
|
|
2152
|
+
const data = await runOneBatchCapture(cap);
|
|
2153
|
+
results.push({ ok: true, mode, label, data, duration: Date.now() - t0 });
|
|
2154
|
+
}
|
|
2155
|
+
catch (err) {
|
|
2156
|
+
results.push({
|
|
2157
|
+
ok: false,
|
|
2158
|
+
mode,
|
|
2159
|
+
label,
|
|
2160
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2161
|
+
duration: Date.now() - t0,
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
const allOk = results.every((r) => r.ok);
|
|
2166
|
+
process.stdout.write(`${JSON.stringify({
|
|
2167
|
+
ok: allOk,
|
|
2168
|
+
duration: Date.now() - startTime,
|
|
2169
|
+
captures: results,
|
|
2170
|
+
})}\n`);
|
|
2171
|
+
if (!allOk)
|
|
2172
|
+
process.exit(1);
|
|
2173
|
+
}
|
|
2174
|
+
async function runOneBatchCapture(cap) {
|
|
2175
|
+
const mode = cap.mode;
|
|
2176
|
+
if (!mode)
|
|
2177
|
+
throw new Error('Capture entry missing required "mode" field.');
|
|
2178
|
+
if (mode === 'term') {
|
|
2179
|
+
const command = cap.command;
|
|
2180
|
+
if (!command)
|
|
2181
|
+
throw new Error('term capture missing "command".');
|
|
2182
|
+
const label = cap.label ?? 'batch';
|
|
2183
|
+
const labelSlug = label
|
|
2184
|
+
.replace(/[^a-z0-9]/gi, '-')
|
|
2185
|
+
.toLowerCase()
|
|
2186
|
+
.slice(0, 40);
|
|
2187
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
2188
|
+
const output = cap.output ??
|
|
2189
|
+
path.join(findProjectRoot(), '.sweetlink', 'term', `${labelSlug}-${stamp}.cast`);
|
|
2190
|
+
ensureDir(output);
|
|
2191
|
+
const { captureTerminal } = await import('../term/recorder.js');
|
|
2192
|
+
const { generatePlayer } = await import('../term/player.js');
|
|
2193
|
+
const result = await captureTerminal({
|
|
2194
|
+
command,
|
|
2195
|
+
output,
|
|
2196
|
+
label,
|
|
2197
|
+
shell: cap.shell,
|
|
2198
|
+
cols: typeof cap.cols === 'number' ? cap.cols : undefined,
|
|
2199
|
+
rows: typeof cap.rows === 'number' ? cap.rows : undefined,
|
|
2200
|
+
});
|
|
2201
|
+
const playerPath = await generatePlayer({ castPath: output });
|
|
2202
|
+
return {
|
|
2203
|
+
castPath: output,
|
|
2204
|
+
playerPath,
|
|
2205
|
+
durationSec: result.durationSec,
|
|
2206
|
+
exitCode: result.exitCode,
|
|
2207
|
+
events: result.events,
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
if (mode === 'sim-ios' || mode === 'sim-android') {
|
|
2211
|
+
const command = cap.command;
|
|
2212
|
+
if (!command)
|
|
2213
|
+
throw new Error(`${mode} capture missing "command".`);
|
|
2214
|
+
const label = cap.label ?? 'batch';
|
|
2215
|
+
const labelSlug = label
|
|
2216
|
+
.replace(/[^a-z0-9]/gi, '-')
|
|
2217
|
+
.toLowerCase()
|
|
2218
|
+
.slice(0, 40);
|
|
2219
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
2220
|
+
const output = cap.output ??
|
|
2221
|
+
path.join(findProjectRoot(), '.sweetlink', 'sim', `${labelSlug}-${stamp}.mp4`);
|
|
2222
|
+
ensureDir(output);
|
|
2223
|
+
const device = cap.device;
|
|
2224
|
+
if (mode === 'sim-ios') {
|
|
2225
|
+
const { recordIosSimulator } = await import('../simulator/ios.js');
|
|
2226
|
+
return recordIosSimulator({ command, output, device });
|
|
2227
|
+
}
|
|
2228
|
+
const { recordAndroidEmulator } = await import('../simulator/android.js');
|
|
2229
|
+
return recordAndroidEmulator({
|
|
2230
|
+
command,
|
|
2231
|
+
output,
|
|
2232
|
+
device,
|
|
2233
|
+
timeLimit: typeof cap.timeLimit === 'number' ? cap.timeLimit : undefined,
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
if (mode === 'screenshot') {
|
|
2237
|
+
const targetUrl = cap.url ?? 'http://localhost:3000';
|
|
2238
|
+
const projRoot = findProjectRoot();
|
|
2239
|
+
const state = await ensureDaemon(projRoot, targetUrl);
|
|
2240
|
+
const resp = await daemonRequest(state, 'screenshot', {
|
|
2241
|
+
selector: cap.selector,
|
|
2242
|
+
fullPage: cap.fullPage,
|
|
2243
|
+
viewport: cap.viewport,
|
|
2244
|
+
padding: cap.padding,
|
|
2245
|
+
theme: cap.theme,
|
|
2246
|
+
});
|
|
2247
|
+
const data = resp.data;
|
|
2248
|
+
if (cap.output) {
|
|
2249
|
+
const outputPath = cap.output;
|
|
2250
|
+
ensureDir(outputPath);
|
|
2251
|
+
fs.writeFileSync(outputPath, Buffer.from(data.screenshot, 'base64'));
|
|
2252
|
+
return { path: outputPath, width: data.width, height: data.height };
|
|
2253
|
+
}
|
|
2254
|
+
return { width: data.width, height: data.height, base64Length: data.screenshot.length };
|
|
2255
|
+
}
|
|
2256
|
+
throw new Error(`Unknown capture mode: ${mode}. Allowed: term, sim-ios, sim-android, screenshot.`);
|
|
2257
|
+
}
|
|
2048
2258
|
// Helper function to get argument value
|
|
2049
2259
|
function getArg(flag) {
|
|
2050
2260
|
const index = args.indexOf(flag);
|
|
@@ -2054,6 +2264,7 @@ function hasFlag(flag) {
|
|
|
2054
2264
|
return args.includes(flag);
|
|
2055
2265
|
}
|
|
2056
2266
|
// Per-command --help: `pnpm sweetlink screenshot --help`
|
|
2267
|
+
// Past the early-exit at the top of dispatch, commandType is non-null.
|
|
2057
2268
|
if (hasFlag('--help') || hasFlag('-h')) {
|
|
2058
2269
|
showCommandHelp(commandType);
|
|
2059
2270
|
process.exit(0);
|
|
@@ -2063,6 +2274,8 @@ if (hasFlag('--output-schema')) {
|
|
|
2063
2274
|
// If commandType is a known command, print just that schema; otherwise print all
|
|
2064
2275
|
const knownCommands = [
|
|
2065
2276
|
'screenshot',
|
|
2277
|
+
'inspect',
|
|
2278
|
+
'context',
|
|
2066
2279
|
'query',
|
|
2067
2280
|
'logs',
|
|
2068
2281
|
'exec',
|
|
@@ -2096,7 +2309,9 @@ if (hasFlag('--output-schema')) {
|
|
|
2096
2309
|
? 'ruler'
|
|
2097
2310
|
: commandType === 'accessibility'
|
|
2098
2311
|
? 'a11y'
|
|
2099
|
-
: commandType
|
|
2312
|
+
: commandType === 'context'
|
|
2313
|
+
? 'inspect'
|
|
2314
|
+
: commandType
|
|
2100
2315
|
: undefined;
|
|
2101
2316
|
printOutputSchema(schemaCommand);
|
|
2102
2317
|
process.exit(0);
|
|
@@ -2130,6 +2345,24 @@ function setupJsonMode(command, startTime) {
|
|
|
2130
2345
|
});
|
|
2131
2346
|
return { origExit, getLastError: () => lastErrorMsg };
|
|
2132
2347
|
}
|
|
2348
|
+
function getErrorData(error) {
|
|
2349
|
+
if (error instanceof DaemonRequestError) {
|
|
2350
|
+
return {
|
|
2351
|
+
action: error.action,
|
|
2352
|
+
status: error.status,
|
|
2353
|
+
...(error.data ? error.data : {}),
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
return null;
|
|
2357
|
+
}
|
|
2358
|
+
function printErrorContext(error) {
|
|
2359
|
+
if (!(error instanceof DaemonRequestError) || !error.data)
|
|
2360
|
+
return;
|
|
2361
|
+
const failureScreenshot = error.data.failureScreenshot;
|
|
2362
|
+
if (typeof failureScreenshot === 'string') {
|
|
2363
|
+
console.error(`[Sweetlink] Failure screenshot: ${failureScreenshot}`);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2133
2366
|
/**
|
|
2134
2367
|
* Handle the `wait` command: wait for a server to be ready.
|
|
2135
2368
|
*/
|
|
@@ -2180,9 +2413,14 @@ async function handleStatusCommand() {
|
|
|
2180
2413
|
}
|
|
2181
2414
|
(async () => {
|
|
2182
2415
|
const startTime = Date.now();
|
|
2183
|
-
// Resolve --app flag:
|
|
2416
|
+
// Resolve --app flag: for WS-bridge commands, this discovers the matching
|
|
2417
|
+
// Sweetlink server by branch/app name. For commands that produce artifacts
|
|
2418
|
+
// (term/sim), --app is a *namespace* used in the artifact directory layout
|
|
2419
|
+
// (.sweetlink/<app>/<YYYYMMDD>/<run>/...) — those handlers read --app
|
|
2420
|
+
// themselves, so we skip discovery here.
|
|
2184
2421
|
const appTarget = getArg('--app');
|
|
2185
|
-
|
|
2422
|
+
const isArtifactCommand = commandType === 'term' || commandType === 'sim';
|
|
2423
|
+
if (appTarget && !isArtifactCommand) {
|
|
2186
2424
|
try {
|
|
2187
2425
|
resolvedWsUrl = await discoverServer(appTarget);
|
|
2188
2426
|
console.log(`[Sweetlink] Targeting server: ${resolvedWsUrl}`);
|
|
@@ -2223,6 +2461,42 @@ async function handleStatusCommand() {
|
|
|
2223
2461
|
: undefined,
|
|
2224
2462
|
});
|
|
2225
2463
|
break;
|
|
2464
|
+
case 'inspect':
|
|
2465
|
+
case 'context': {
|
|
2466
|
+
const projRoot = findProjectRoot();
|
|
2467
|
+
const targetUrl = getArg('--url') ?? 'http://localhost:3000';
|
|
2468
|
+
const state = await ensureDaemon(projRoot, targetUrl);
|
|
2469
|
+
const actionTranscript = [];
|
|
2470
|
+
args.forEach((arg, index) => {
|
|
2471
|
+
if (arg !== '--action')
|
|
2472
|
+
return;
|
|
2473
|
+
const value = args[index + 1];
|
|
2474
|
+
if (!value)
|
|
2475
|
+
return;
|
|
2476
|
+
actionTranscript.push({ action: value });
|
|
2477
|
+
});
|
|
2478
|
+
const resp = await daemonRequest(state, 'inspect', {
|
|
2479
|
+
last: getArg('--last') ? parseInt(getArg('--last'), 10) : undefined,
|
|
2480
|
+
label: getArg('--label'),
|
|
2481
|
+
expectedOutcome: getArg('--expected'),
|
|
2482
|
+
actionTranscript,
|
|
2483
|
+
includeA11y: !hasFlag('--no-a11y'),
|
|
2484
|
+
});
|
|
2485
|
+
const data = resp.data;
|
|
2486
|
+
const output = getArg('--output');
|
|
2487
|
+
if (output) {
|
|
2488
|
+
ensureDir(output);
|
|
2489
|
+
fs.writeFileSync(output, JSON.stringify(data, null, 2), 'utf-8');
|
|
2490
|
+
}
|
|
2491
|
+
if (getArg('--format') === 'json') {
|
|
2492
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2493
|
+
}
|
|
2494
|
+
else {
|
|
2495
|
+
printInspectSummary(data);
|
|
2496
|
+
}
|
|
2497
|
+
result = data;
|
|
2498
|
+
break;
|
|
2499
|
+
}
|
|
2226
2500
|
case 'query': {
|
|
2227
2501
|
const selector = getArg('--selector');
|
|
2228
2502
|
if (!selector) {
|
|
@@ -2318,7 +2592,9 @@ async function handleStatusCommand() {
|
|
|
2318
2592
|
}
|
|
2319
2593
|
}
|
|
2320
2594
|
}
|
|
2321
|
-
catch {
|
|
2595
|
+
catch {
|
|
2596
|
+
/* fall through to WS path */
|
|
2597
|
+
}
|
|
2322
2598
|
result = await click({
|
|
2323
2599
|
selector: clickTarget,
|
|
2324
2600
|
text: clickText,
|
|
@@ -2446,7 +2722,8 @@ async function handleStatusCommand() {
|
|
|
2446
2722
|
}
|
|
2447
2723
|
const sessionDirArg = getArg('--session') ?? '.sweetlink';
|
|
2448
2724
|
// Find latest session manifest
|
|
2449
|
-
const sessionFiles = fs
|
|
2725
|
+
const sessionFiles = fs
|
|
2726
|
+
.readdirSync(sessionDirArg)
|
|
2450
2727
|
.filter((f) => f.startsWith('session-'))
|
|
2451
2728
|
.sort()
|
|
2452
2729
|
.reverse();
|
|
@@ -2501,16 +2778,19 @@ async function handleStatusCommand() {
|
|
|
2501
2778
|
const data = resp.data;
|
|
2502
2779
|
const m = data.manifest;
|
|
2503
2780
|
console.log(`[Sweetlink] Recording stopped: ${m.sessionId}`);
|
|
2504
|
-
console.log(` Duration: ${m.duration.toFixed(1)}s | Actions: ${m.commands.length}${m.video ?
|
|
2781
|
+
console.log(` Duration: ${m.duration.toFixed(1)}s | Actions: ${m.commands.length}${m.video ? ` | Video: ${m.video}` : ''}`);
|
|
2505
2782
|
// Auto-open the viewer (cross-platform; --no-open to suppress)
|
|
2506
2783
|
if (data.viewerPath && !hasFlag('--no-open')) {
|
|
2507
2784
|
console.log(` Viewer: ${data.viewerPath}`);
|
|
2508
2785
|
const { execFile } = await import('child_process');
|
|
2509
2786
|
// `start` on Windows is a cmd builtin, not an exe — must invoke via cmd.
|
|
2510
|
-
const cmd = process.platform === 'darwin'
|
|
2511
|
-
|
|
2787
|
+
const cmd = process.platform === 'darwin'
|
|
2788
|
+
? 'open'
|
|
2789
|
+
: process.platform === 'win32'
|
|
2790
|
+
? 'cmd'
|
|
2512
2791
|
: 'xdg-open';
|
|
2513
|
-
const args = process.platform === 'win32'
|
|
2792
|
+
const args = process.platform === 'win32'
|
|
2793
|
+
? ['/c', 'start', '', data.viewerPath]
|
|
2514
2794
|
: [data.viewerPath];
|
|
2515
2795
|
execFile(cmd, args, (err) => {
|
|
2516
2796
|
if (err)
|
|
@@ -2553,7 +2833,10 @@ async function handleStatusCommand() {
|
|
|
2553
2833
|
const startResp = await daemonRequest(state, 'record-start', label ? { label } : {});
|
|
2554
2834
|
const startData = startResp.data;
|
|
2555
2835
|
console.log(`[Sweetlink] Recording: ${startData.sessionId}${label ? ` (${label})` : ''}`);
|
|
2556
|
-
const steps = script
|
|
2836
|
+
const steps = script
|
|
2837
|
+
.split(';')
|
|
2838
|
+
.map((s) => s.trim())
|
|
2839
|
+
.filter(Boolean);
|
|
2557
2840
|
// Snapshot once up-front so refs resolve.
|
|
2558
2841
|
await daemonRequest(state, 'snapshot', { interactive: true });
|
|
2559
2842
|
for (const step of steps) {
|
|
@@ -2637,7 +2920,8 @@ async function handleStatusCommand() {
|
|
|
2637
2920
|
console.error(`[Sweetlink] Session directory not found: ${sessionDirArg}`);
|
|
2638
2921
|
process.exit(1);
|
|
2639
2922
|
}
|
|
2640
|
-
const reportSessions = fs
|
|
2923
|
+
const reportSessions = fs
|
|
2924
|
+
.readdirSync(sessionDirArg)
|
|
2641
2925
|
.filter((f) => f.startsWith('session-'))
|
|
2642
2926
|
.sort()
|
|
2643
2927
|
.reverse();
|
|
@@ -2714,7 +2998,10 @@ async function handleStatusCommand() {
|
|
|
2714
2998
|
}
|
|
2715
2999
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
2716
3000
|
const summary = fs.existsSync(summaryPath) ? fs.readFileSync(summaryPath, 'utf-8') : '';
|
|
2717
|
-
const payload = {
|
|
3001
|
+
const payload = {
|
|
3002
|
+
summary,
|
|
3003
|
+
manifest,
|
|
3004
|
+
};
|
|
2718
3005
|
// Include viewer HTML for Slack/Discord webhooks
|
|
2719
3006
|
if (/slack|discord/i.test(webhookUrl)) {
|
|
2720
3007
|
const viewerPath = path.join(reportSessionDir, 'viewer.html');
|
|
@@ -2758,7 +3045,14 @@ async function handleStatusCommand() {
|
|
|
2758
3045
|
console.error('[Sweetlink] Usage: sweetlink sim <ios|android> "<command>" [--output path] [--device <name>]');
|
|
2759
3046
|
process.exit(1);
|
|
2760
3047
|
}
|
|
2761
|
-
const flagsWithValues = new Set([
|
|
3048
|
+
const flagsWithValues = new Set([
|
|
3049
|
+
'--output',
|
|
3050
|
+
'--label',
|
|
3051
|
+
'--device',
|
|
3052
|
+
'--time-limit',
|
|
3053
|
+
'--app',
|
|
3054
|
+
'--run',
|
|
3055
|
+
]);
|
|
2762
3056
|
const positional = [];
|
|
2763
3057
|
for (let i = 2; i < args.length; i++) {
|
|
2764
3058
|
const a = args[i];
|
|
@@ -2776,10 +3070,19 @@ async function handleStatusCommand() {
|
|
|
2776
3070
|
}
|
|
2777
3071
|
const label = getArg('--label');
|
|
2778
3072
|
const labelSlug = label
|
|
2779
|
-
? label
|
|
3073
|
+
? label
|
|
3074
|
+
.replace(/[^a-z0-9]/gi, '-')
|
|
3075
|
+
.toLowerCase()
|
|
3076
|
+
.slice(0, 40)
|
|
2780
3077
|
: `sim-${platform}`;
|
|
2781
3078
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
2782
|
-
const
|
|
3079
|
+
const { runSlot: simRunSlot } = await import('../runs.js');
|
|
3080
|
+
const defaultDir = simRunSlot({
|
|
3081
|
+
baseDir: findProjectRoot(),
|
|
3082
|
+
app: getArg('--app'),
|
|
3083
|
+
run: getArg('--run'),
|
|
3084
|
+
kind: 'sim',
|
|
3085
|
+
});
|
|
2783
3086
|
const output = getArg('--output') ?? path.join(defaultDir, `${labelSlug}-${stamp}.mp4`);
|
|
2784
3087
|
ensureDir(output);
|
|
2785
3088
|
const device = getArg('--device');
|
|
@@ -2793,24 +3096,38 @@ async function handleStatusCommand() {
|
|
|
2793
3096
|
const { recordAndroidEmulator } = await import('../simulator/android.js');
|
|
2794
3097
|
const tl = getArg('--time-limit');
|
|
2795
3098
|
recResult = await recordAndroidEmulator({
|
|
2796
|
-
command,
|
|
3099
|
+
command,
|
|
3100
|
+
output,
|
|
3101
|
+
device,
|
|
2797
3102
|
timeLimit: tl ? parseInt(tl, 10) : undefined,
|
|
3103
|
+
overlays: !hasFlag('--no-overlays'),
|
|
2798
3104
|
});
|
|
2799
3105
|
}
|
|
2800
3106
|
let sizeKb = '?';
|
|
2801
3107
|
try {
|
|
2802
3108
|
sizeKb = String(Math.round(fs.statSync(output).size / 1024));
|
|
2803
3109
|
}
|
|
2804
|
-
catch {
|
|
3110
|
+
catch {
|
|
3111
|
+
/* file may not exist if recordingClosed is false */
|
|
3112
|
+
}
|
|
3113
|
+
const tapSuffix = (recResult.tapCount ?? 0) > 0
|
|
3114
|
+
? ` · ${recResult.tapCount} taps${recResult.overlaysApplied ? ' (overlaid)' : ' (sidecar only — install ffmpeg for overlays)'}`
|
|
3115
|
+
: '';
|
|
2805
3116
|
console.log(`[Sweetlink] ${recResult.recordingClosed ? '✓' : '⚠'} ${getRelativePath(output)} · ` +
|
|
2806
3117
|
`${recResult.durationSec.toFixed(1)}s · ${sizeKb}KB · ${recResult.device} · exit=${recResult.exitCode}` +
|
|
2807
|
-
|
|
3118
|
+
tapSuffix +
|
|
3119
|
+
(recResult.recordingClosed
|
|
3120
|
+
? ''
|
|
3121
|
+
: ' (recording was force-killed; mp4 may be incomplete)'));
|
|
2808
3122
|
result = {
|
|
2809
3123
|
path: output,
|
|
2810
3124
|
device: recResult.device,
|
|
2811
3125
|
durationSec: recResult.durationSec,
|
|
2812
3126
|
exitCode: recResult.exitCode,
|
|
2813
3127
|
recordingClosed: recResult.recordingClosed,
|
|
3128
|
+
tapCount: recResult.tapCount,
|
|
3129
|
+
tapsJsonPath: recResult.tapsJsonPath,
|
|
3130
|
+
overlaysApplied: recResult.overlaysApplied,
|
|
2814
3131
|
};
|
|
2815
3132
|
if (recResult.exitCode !== 0 && !hasFlag('--ignore-exit')) {
|
|
2816
3133
|
process.exit(recResult.exitCode);
|
|
@@ -2819,8 +3136,16 @@ async function handleStatusCommand() {
|
|
|
2819
3136
|
}
|
|
2820
3137
|
case 'term': {
|
|
2821
3138
|
// Record a shell command's stdout/stderr into asciicast v2 + HTML player.
|
|
2822
|
-
// Example: sweetlink term "pytest -v" --label api-tests
|
|
2823
|
-
const flagsWithValues = new Set([
|
|
3139
|
+
// Example: sweetlink term "pytest -v" --label api-tests --app my-app
|
|
3140
|
+
const flagsWithValues = new Set([
|
|
3141
|
+
'--output',
|
|
3142
|
+
'--label',
|
|
3143
|
+
'--shell',
|
|
3144
|
+
'--cols',
|
|
3145
|
+
'--rows',
|
|
3146
|
+
'--app',
|
|
3147
|
+
'--run',
|
|
3148
|
+
]);
|
|
2824
3149
|
const positional = [];
|
|
2825
3150
|
for (let i = 1; i < args.length; i++) {
|
|
2826
3151
|
const a = args[i];
|
|
@@ -2838,10 +3163,19 @@ async function handleStatusCommand() {
|
|
|
2838
3163
|
}
|
|
2839
3164
|
const label = getArg('--label');
|
|
2840
3165
|
const labelSlug = label
|
|
2841
|
-
? label
|
|
3166
|
+
? label
|
|
3167
|
+
.replace(/[^a-z0-9]/gi, '-')
|
|
3168
|
+
.toLowerCase()
|
|
3169
|
+
.slice(0, 40)
|
|
2842
3170
|
: 'term';
|
|
2843
3171
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
2844
|
-
const
|
|
3172
|
+
const { runSlot } = await import('../runs.js');
|
|
3173
|
+
const defaultDir = runSlot({
|
|
3174
|
+
baseDir: findProjectRoot(),
|
|
3175
|
+
app: getArg('--app'),
|
|
3176
|
+
run: getArg('--run'),
|
|
3177
|
+
kind: 'term',
|
|
3178
|
+
});
|
|
2845
3179
|
const output = getArg('--output') ?? path.join(defaultDir, `${labelSlug}-${stamp}.cast`);
|
|
2846
3180
|
ensureDir(output);
|
|
2847
3181
|
console.log(`[Sweetlink] Recording terminal: ${command}`);
|
|
@@ -2940,8 +3274,20 @@ async function handleStatusCommand() {
|
|
|
2940
3274
|
console.log('\nAction sequences are identical.');
|
|
2941
3275
|
}
|
|
2942
3276
|
result = {
|
|
2943
|
-
a: {
|
|
2944
|
-
|
|
3277
|
+
a: {
|
|
3278
|
+
id: a.sessionId,
|
|
3279
|
+
label: a.label,
|
|
3280
|
+
duration: a.duration,
|
|
3281
|
+
actions: a.actionCount,
|
|
3282
|
+
errors: aErr,
|
|
3283
|
+
},
|
|
3284
|
+
b: {
|
|
3285
|
+
id: b.sessionId,
|
|
3286
|
+
label: b.label,
|
|
3287
|
+
duration: b.duration,
|
|
3288
|
+
actions: b.actionCount,
|
|
3289
|
+
errors: bErr,
|
|
3290
|
+
},
|
|
2945
3291
|
added,
|
|
2946
3292
|
removed,
|
|
2947
3293
|
};
|
|
@@ -2950,12 +3296,12 @@ async function handleStatusCommand() {
|
|
|
2950
3296
|
// Open the index.html in the browser
|
|
2951
3297
|
if (data.indexPath) {
|
|
2952
3298
|
const { execFile } = await import('child_process');
|
|
2953
|
-
const cmd = process.platform === 'darwin'
|
|
2954
|
-
|
|
3299
|
+
const cmd = process.platform === 'darwin'
|
|
3300
|
+
? 'open'
|
|
3301
|
+
: process.platform === 'win32'
|
|
3302
|
+
? 'cmd'
|
|
2955
3303
|
: 'xdg-open';
|
|
2956
|
-
const cmdArgs = process.platform === 'win32'
|
|
2957
|
-
? ['/c', 'start', '', data.indexPath]
|
|
2958
|
-
: [data.indexPath];
|
|
3304
|
+
const cmdArgs = process.platform === 'win32' ? ['/c', 'start', '', data.indexPath] : [data.indexPath];
|
|
2959
3305
|
execFile(cmd, cmdArgs, () => { });
|
|
2960
3306
|
console.log(`[Sweetlink] Opened ${data.indexPath}`);
|
|
2961
3307
|
}
|
|
@@ -3063,10 +3409,13 @@ async function handleStatusCommand() {
|
|
|
3063
3409
|
console.log(`[Sweetlink] Demo: "${state.title}" (${state.sections.length} sections)`);
|
|
3064
3410
|
console.log(` File: ${state.filePath}`);
|
|
3065
3411
|
for (const s of state.sections) {
|
|
3066
|
-
const preview = s.type === 'note'
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3412
|
+
const preview = s.type === 'note'
|
|
3413
|
+
? s.content.substring(0, 60)
|
|
3414
|
+
: s.type === 'exec'
|
|
3415
|
+
? `$ ${s.command}`
|
|
3416
|
+
: s.type === 'screenshot'
|
|
3417
|
+
? `[image] ${s.screenshotFile}`
|
|
3418
|
+
: '[snapshot]';
|
|
3070
3419
|
console.log(` ${s.type.padEnd(12)} ${preview}`);
|
|
3071
3420
|
}
|
|
3072
3421
|
result = state;
|
|
@@ -3187,8 +3536,10 @@ async function handleStatusCommand() {
|
|
|
3187
3536
|
const msg = error instanceof Error ? error.message : String(error);
|
|
3188
3537
|
emitJson({
|
|
3189
3538
|
ok: false,
|
|
3539
|
+
// commandType is non-undefined here: the early-exit at the top of
|
|
3540
|
+
// CLI dispatch handles the bare `--json` (batch mode) case.
|
|
3190
3541
|
command: commandType,
|
|
3191
|
-
data:
|
|
3542
|
+
data: getErrorData(error),
|
|
3192
3543
|
error: msg,
|
|
3193
3544
|
duration: Date.now() - startTime,
|
|
3194
3545
|
});
|
|
@@ -3198,6 +3549,7 @@ async function handleStatusCommand() {
|
|
|
3198
3549
|
// to end users and clutters the output. Set SWEETLINK_DEBUG=1 to see it.
|
|
3199
3550
|
if (error instanceof Error) {
|
|
3200
3551
|
console.error(`[Sweetlink] ${error.message}`);
|
|
3552
|
+
printErrorContext(error);
|
|
3201
3553
|
if (process.env.SWEETLINK_DEBUG === '1' && error.stack) {
|
|
3202
3554
|
console.error(error.stack);
|
|
3203
3555
|
}
|