@ishlabs/cli 0.27.1 → 0.28.1
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/dist/commands/feedback.d.ts +22 -0
- package/dist/commands/feedback.js +259 -0
- package/dist/commands/study-run.js +21 -2
- package/dist/commands/workspace.js +1 -1
- package/dist/index.js +2 -0
- package/dist/lib/command-helpers.js +7 -3
- package/dist/lib/docs.js +108 -13
- package/dist/lib/local-sim/actions.d.ts +18 -2
- package/dist/lib/local-sim/actions.js +24 -1
- package/dist/lib/local-sim/android.js +7 -2
- package/dist/lib/local-sim/ios.js +10 -6
- package/dist/lib/local-sim/loop.js +5 -2
- package/dist/lib/local-sim/types.d.ts +27 -1
- package/dist/lib/local-sim/xcuitest.d.ts +24 -0
- package/dist/lib/local-sim/xcuitest.js +48 -0
- package/dist/lib/output.d.ts +11 -1
- package/dist/lib/output.js +56 -2
- package/dist/lib/skill-content.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ish feedback — report a bug, request a feature, or send any other feedback
|
|
3
|
+
* to the ish team straight from the terminal.
|
|
4
|
+
*
|
|
5
|
+
* Transport: posts to the ish backend (`POST /api/v1/product-feedback` via
|
|
6
|
+
* ApiClient) with the user's bearer token. The backend inserts into the
|
|
7
|
+
* `product_feedback` table — the same table the web app's "Send feedback"
|
|
8
|
+
* dialog writes to — using its privileged connection, so CLI reports show up
|
|
9
|
+
* alongside web feedback. Per ADR-0029 (ish-frontend), clients submit through
|
|
10
|
+
* api.ishlabs.io, NEVER via Supabase PostgREST directly: the `sb_publishable_*`
|
|
11
|
+
* key shipped in this binary is deliberately powerless against the database.
|
|
12
|
+
* (The web flow additionally mirrors to Notion + captures PostHog server-side;
|
|
13
|
+
* those are deferred for the CLI.)
|
|
14
|
+
*
|
|
15
|
+
* Diagnostics: to help whoever triages the report, the user's message is
|
|
16
|
+
* augmented with an environment + active-config block (opt out with
|
|
17
|
+
* --no-diagnostics), and optionally (--health) the `ish check` setup checklist
|
|
18
|
+
* plus the tail of ~/.ish/local-sim.log. The reporter's identity (user_id /
|
|
19
|
+
* user_email) is attached server-side from the JWT, not here.
|
|
20
|
+
*/
|
|
21
|
+
import type { Command } from "commander";
|
|
22
|
+
export declare function registerFeedbackCommands(program: Command): void;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ish feedback — report a bug, request a feature, or send any other feedback
|
|
3
|
+
* to the ish team straight from the terminal.
|
|
4
|
+
*
|
|
5
|
+
* Transport: posts to the ish backend (`POST /api/v1/product-feedback` via
|
|
6
|
+
* ApiClient) with the user's bearer token. The backend inserts into the
|
|
7
|
+
* `product_feedback` table — the same table the web app's "Send feedback"
|
|
8
|
+
* dialog writes to — using its privileged connection, so CLI reports show up
|
|
9
|
+
* alongside web feedback. Per ADR-0029 (ish-frontend), clients submit through
|
|
10
|
+
* api.ishlabs.io, NEVER via Supabase PostgREST directly: the `sb_publishable_*`
|
|
11
|
+
* key shipped in this binary is deliberately powerless against the database.
|
|
12
|
+
* (The web flow additionally mirrors to Notion + captures PostHog server-side;
|
|
13
|
+
* those are deferred for the CLI.)
|
|
14
|
+
*
|
|
15
|
+
* Diagnostics: to help whoever triages the report, the user's message is
|
|
16
|
+
* augmented with an environment + active-config block (opt out with
|
|
17
|
+
* --no-diagnostics), and optionally (--health) the `ish check` setup checklist
|
|
18
|
+
* plus the tail of ~/.ish/local-sim.log. The reporter's identity (user_id /
|
|
19
|
+
* user_email) is attached server-side from the JWT, not here.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
25
|
+
import { loadConfig } from "../config.js";
|
|
26
|
+
import { withClient } from "../lib/command-helpers.js";
|
|
27
|
+
import { resolveApiUrl } from "../lib/auth.js";
|
|
28
|
+
import { output } from "../lib/output.js";
|
|
29
|
+
import { c } from "../lib/colors.js";
|
|
30
|
+
const { version: CLI_VERSION } = pkg;
|
|
31
|
+
const FEEDBACK_TYPES = ["bug", "feature", "other"];
|
|
32
|
+
const DESCRIPTION_MIN = 10;
|
|
33
|
+
const DESCRIPTION_MAX = 4000;
|
|
34
|
+
const TITLE_MIN = 3;
|
|
35
|
+
const TITLE_MAX = 120;
|
|
36
|
+
const TYPE_NOUN = {
|
|
37
|
+
bug: "Bug report",
|
|
38
|
+
feature: "Feature request",
|
|
39
|
+
other: "Feedback",
|
|
40
|
+
};
|
|
41
|
+
function validationError(message) {
|
|
42
|
+
// `name === "ValidationError"` maps to exit code 2 in exitCodeFromError.
|
|
43
|
+
const err = new Error(message);
|
|
44
|
+
err.name = "ValidationError";
|
|
45
|
+
return err;
|
|
46
|
+
}
|
|
47
|
+
function readStdin() {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
let data = "";
|
|
50
|
+
process.stdin.setEncoding("utf-8");
|
|
51
|
+
process.stdin.on("data", (chunk) => {
|
|
52
|
+
data += chunk;
|
|
53
|
+
});
|
|
54
|
+
process.stdin.on("end", () => resolve(data));
|
|
55
|
+
process.stdin.on("error", reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the feedback body from the positional argument, an `@file`
|
|
60
|
+
* reference, or piped stdin — the same input idioms the rest of the CLI uses
|
|
61
|
+
* for text fields (no interactive prompt; the CLI is for autonomous agents).
|
|
62
|
+
*/
|
|
63
|
+
async function resolveDescription(message) {
|
|
64
|
+
if (message && message.startsWith("@")) {
|
|
65
|
+
const path = message.slice(1);
|
|
66
|
+
try {
|
|
67
|
+
return readFileSync(path, "utf-8");
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new Error(`Cannot read file: ${path}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Explicit stdin sentinel, or no argument with something piped in.
|
|
74
|
+
if (message === "-" || (message === undefined && !process.stdin.isTTY)) {
|
|
75
|
+
return readStdin();
|
|
76
|
+
}
|
|
77
|
+
if (message === undefined) {
|
|
78
|
+
throw validationError('No feedback message. Pass it as an argument, pipe it on stdin, or use @file:\n' +
|
|
79
|
+
' ish feedback "the save button on the profile page does nothing"\n' +
|
|
80
|
+
' ish feedback --type feature "let me export interactions as CSV"\n' +
|
|
81
|
+
" ish feedback @bug-report.md\n" +
|
|
82
|
+
' echo "..." | ish feedback -');
|
|
83
|
+
}
|
|
84
|
+
return message;
|
|
85
|
+
}
|
|
86
|
+
function deriveTitle(explicit, description, type) {
|
|
87
|
+
if (explicit !== undefined) {
|
|
88
|
+
const t = explicit.trim();
|
|
89
|
+
if (t.length < TITLE_MIN) {
|
|
90
|
+
throw validationError(`--title must be at least ${TITLE_MIN} characters.`);
|
|
91
|
+
}
|
|
92
|
+
return t.slice(0, TITLE_MAX);
|
|
93
|
+
}
|
|
94
|
+
// Mirror the web dialog: first non-empty line of the message becomes the
|
|
95
|
+
// title, capped to TITLE_MAX. Fall back to a generic title so the table's
|
|
96
|
+
// NOT NULL + min-length contract is always satisfied even for terse bodies.
|
|
97
|
+
const firstLine = description
|
|
98
|
+
.split("\n")
|
|
99
|
+
.map((l) => l.trim())
|
|
100
|
+
.find((l) => l.length > 0) ?? "";
|
|
101
|
+
const candidate = firstLine.slice(0, TITLE_MAX).trim();
|
|
102
|
+
return candidate.length >= TITLE_MIN ? candidate : `${TYPE_NOUN[type]} from CLI`;
|
|
103
|
+
}
|
|
104
|
+
// ── Diagnostics ────────────────────────────────────────────────────────────
|
|
105
|
+
//
|
|
106
|
+
// Appended to the message so whoever debugs the report has grounded context.
|
|
107
|
+
// None of this needs the JWT — the backend attaches the reporter's identity
|
|
108
|
+
// from the token. The aim is maximal useful signal without a schema change.
|
|
109
|
+
/** Always-on, instant one-liner: the bare minimum a debugger needs. */
|
|
110
|
+
function platformLine(dev) {
|
|
111
|
+
return `Sent via ish CLI v${CLI_VERSION} · ${process.platform} ${process.arch} · node ${process.versions.node}${dev ? " · dev" : ""}`;
|
|
112
|
+
}
|
|
113
|
+
/** Active-config context (instant; no subprocesses). Default-on. */
|
|
114
|
+
function diagnosticsBlock(apiUrl) {
|
|
115
|
+
const cfg = loadConfig();
|
|
116
|
+
const lines = [
|
|
117
|
+
"Diagnostics (auto-attached; pass --no-diagnostics to omit)",
|
|
118
|
+
`- Active workspace: ${cfg.workspace ?? "none"}`,
|
|
119
|
+
`- Active study: ${cfg.study ?? "none"}`,
|
|
120
|
+
`- Active ask: ${cfg.ask ?? "none"}`,
|
|
121
|
+
`- API: ${apiUrl}`,
|
|
122
|
+
];
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
}
|
|
125
|
+
/** Setup/health checklist, reusing `ish check`'s probes. Heavier (subprocess
|
|
126
|
+
* probes for xcrun/adb), so gated behind --health. */
|
|
127
|
+
async function healthBlock() {
|
|
128
|
+
try {
|
|
129
|
+
const { runChecks } = await import("./doctor.js");
|
|
130
|
+
const checks = await runChecks();
|
|
131
|
+
const MARK = {
|
|
132
|
+
pass: "OK",
|
|
133
|
+
warn: "WARN",
|
|
134
|
+
fail: "FAIL",
|
|
135
|
+
skip: "--",
|
|
136
|
+
};
|
|
137
|
+
const lines = checks.map((ch) => {
|
|
138
|
+
const fix = (ch.status === "warn" || ch.status === "fail") && ch.fix
|
|
139
|
+
? ` (fix: ${ch.fix})`
|
|
140
|
+
: "";
|
|
141
|
+
return `- [${MARK[ch.status] ?? ch.status}] ${ch.name}: ${ch.message ?? ""}${fix}`.trimEnd();
|
|
142
|
+
});
|
|
143
|
+
return ["Setup checks (ish check):", ...lines].join("\n");
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
return `Setup checks: could not run (${e instanceof Error ? e.message : String(e)}).`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Tail of the local-simulation debug log, when present. Written to
|
|
150
|
+
* ~/.ish/local-sim.log by `study run --local --debug`. */
|
|
151
|
+
function localSimLogTail(maxLines = 80) {
|
|
152
|
+
// Match the literal path debug.ts writes to (it uses homedir()/.ish, not the
|
|
153
|
+
// ISH_HOME-aware paths helper), so we read exactly where the log lands.
|
|
154
|
+
const logFile = join(homedir(), ".ish", "local-sim.log");
|
|
155
|
+
if (!existsSync(logFile))
|
|
156
|
+
return undefined;
|
|
157
|
+
try {
|
|
158
|
+
const tail = readFileSync(logFile, "utf-8").split("\n").slice(-maxLines).join("\n").trim();
|
|
159
|
+
if (!tail)
|
|
160
|
+
return undefined;
|
|
161
|
+
return [
|
|
162
|
+
`Recent local-sim log (last ${maxLines} lines of ~/.ish/local-sim.log):`,
|
|
163
|
+
"```",
|
|
164
|
+
tail,
|
|
165
|
+
"```",
|
|
166
|
+
].join("\n");
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Assemble everything appended to the user's message. */
|
|
173
|
+
async function buildAppendix(opts) {
|
|
174
|
+
const sections = [platformLine(opts.dev)];
|
|
175
|
+
if (opts.diagnostics)
|
|
176
|
+
sections.push(diagnosticsBlock(opts.apiUrl));
|
|
177
|
+
if (opts.health) {
|
|
178
|
+
sections.push(await healthBlock());
|
|
179
|
+
const log = localSimLogTail();
|
|
180
|
+
if (log)
|
|
181
|
+
sections.push(log);
|
|
182
|
+
}
|
|
183
|
+
return "\n\n—\n" + sections.join("\n\n");
|
|
184
|
+
}
|
|
185
|
+
export function registerFeedbackCommands(program) {
|
|
186
|
+
program
|
|
187
|
+
.command("feedback")
|
|
188
|
+
.description("Report a bug or send feedback to the ish team")
|
|
189
|
+
.argument("[message]", "Feedback text. Use @path to read from a file, or - to read from stdin.")
|
|
190
|
+
.option("--type <type>", "Feedback type: bug | feature | other", "bug")
|
|
191
|
+
.option("--title <title>", "Short title (default: first line of the message)")
|
|
192
|
+
.option("--no-diagnostics", "Don't attach environment/account diagnostics to the report")
|
|
193
|
+
.option("--health", "Also run setup checks (ish check) and attach recent local-sim logs — useful for simulation/setup bugs")
|
|
194
|
+
.option("--dry-run", "Print exactly what would be sent (including diagnostics) without submitting")
|
|
195
|
+
.addHelpText("after", `\nExamples:
|
|
196
|
+
$ ish feedback "the save button on the profile page does nothing"
|
|
197
|
+
$ ish feedback --type feature "let me export interactions as CSV"
|
|
198
|
+
$ ish feedback --type other "the guided setup was great"
|
|
199
|
+
$ ish feedback @bug-report.md
|
|
200
|
+
$ ish study run … --local --debug 2>err.txt; ish feedback @err.txt --health
|
|
201
|
+
$ git log -1 --oneline | ish feedback -
|
|
202
|
+
|
|
203
|
+
Reports go to the ish team and show up alongside web feedback. Sends as the
|
|
204
|
+
logged-in user (run \`ish login\` first). bug is the default type. To give
|
|
205
|
+
whoever debugs it the most signal, attach the failing command's output via
|
|
206
|
+
@file or stdin; environment + active-config diagnostics are attached
|
|
207
|
+
automatically (opt out with --no-diagnostics), and --health adds setup
|
|
208
|
+
checks + local-sim logs.`)
|
|
209
|
+
.action(async (message, opts, cmd) => {
|
|
210
|
+
await withClient(cmd, async (client, globals) => {
|
|
211
|
+
const type = opts.type;
|
|
212
|
+
if (!FEEDBACK_TYPES.includes(type)) {
|
|
213
|
+
throw validationError(`Invalid --type "${opts.type}". Must be one of: ${FEEDBACK_TYPES.join(", ")}.`);
|
|
214
|
+
}
|
|
215
|
+
const rawDescription = await resolveDescription(message);
|
|
216
|
+
const description = rawDescription.trim();
|
|
217
|
+
if (description.length === 0) {
|
|
218
|
+
throw validationError('No feedback message. Pass it as an argument, pipe it on stdin, or use @file:\n' +
|
|
219
|
+
' ish feedback "the save button on the profile page does nothing"\n' +
|
|
220
|
+
' ish feedback --type feature "let me export interactions as CSV"\n' +
|
|
221
|
+
" ish feedback @bug-report.md");
|
|
222
|
+
}
|
|
223
|
+
if (description.length < DESCRIPTION_MIN) {
|
|
224
|
+
throw validationError(`Please add a bit more detail (at least ${DESCRIPTION_MIN} characters).`);
|
|
225
|
+
}
|
|
226
|
+
if (description.length > DESCRIPTION_MAX) {
|
|
227
|
+
throw validationError(`Feedback is too long (${description.length} chars; max ${DESCRIPTION_MAX}).`);
|
|
228
|
+
}
|
|
229
|
+
const title = deriveTitle(opts.title, description, type);
|
|
230
|
+
const apiUrl = resolveApiUrl(globals.apiUrl, globals.dev);
|
|
231
|
+
const appendix = await buildAppendix({
|
|
232
|
+
apiUrl,
|
|
233
|
+
dev: globals.dev,
|
|
234
|
+
diagnostics: opts.diagnostics,
|
|
235
|
+
health: opts.health ?? false,
|
|
236
|
+
});
|
|
237
|
+
const body = {
|
|
238
|
+
type,
|
|
239
|
+
title,
|
|
240
|
+
description: description + appendix,
|
|
241
|
+
};
|
|
242
|
+
if (opts.dryRun) {
|
|
243
|
+
if (!globals.quiet) {
|
|
244
|
+
console.error(`${c.dim}Dry run — nothing sent. This is what would be submitted:${c.reset}`);
|
|
245
|
+
}
|
|
246
|
+
output(body, globals.json);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!globals.quiet) {
|
|
250
|
+
console.error(`Sending ${type} report…`);
|
|
251
|
+
}
|
|
252
|
+
const data = await client.post("/product-feedback", body);
|
|
253
|
+
if (!globals.quiet) {
|
|
254
|
+
console.error(`${c.green}✓${c.reset} Thanks! Your ${type} report was sent to the ish team.`);
|
|
255
|
+
}
|
|
256
|
+
output(data, globals.json, { writePath: true });
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -493,9 +493,28 @@ Examples:
|
|
|
493
493
|
chatMode = readChatMode(iteration.details);
|
|
494
494
|
isPair = chatMode === "participant_pair";
|
|
495
495
|
}
|
|
496
|
-
|
|
497
|
-
|
|
496
|
+
// Native (ios/android) interactive iterations name their target via
|
|
497
|
+
// `app_artifact`, which is OPTIONAL at create time: `ish iteration create
|
|
498
|
+
// --platform ios` with no --app stores it as "chosen at run time" (no
|
|
499
|
+
// app_artifact) and the app is supplied here via --app. The web app's
|
|
500
|
+
// "Run on your device" panel generates exactly that command
|
|
501
|
+
// (`ish study run --local --platform ios --app ./Build.app …`), so a
|
|
502
|
+
// run-time --app must satisfy the content requirement even though the
|
|
503
|
+
// iteration itself carries no stored target. `iterationHasContent` only
|
|
504
|
+
// sees the persisted details, so layer the run-time flag on top here.
|
|
505
|
+
const iterationPlatform = normalizePlatform(readIterationDetails(iteration.details).platform);
|
|
506
|
+
const isNativeIteration = iterationPlatform === "ios" || iterationPlatform === "android";
|
|
507
|
+
const runtimeAppProvided = isNativeIteration && typeof opts.app === "string" && opts.app.trim().length > 0;
|
|
508
|
+
if (!iterationHasContent(iteration.details, modality) && !runtimeAppProvided) {
|
|
498
509
|
const iterAlias = tagAlias(ALIAS_PREFIX.iteration, iterationId);
|
|
510
|
+
if (isNativeIteration) {
|
|
511
|
+
// The target is an app, not a URL — point at --app, not --url.
|
|
512
|
+
const ext = iterationPlatform === "ios" ? ".app" : ".apk";
|
|
513
|
+
throw new Error(`Iteration "${iterationLabel}" (${iterAlias}) is a native ${iterationPlatform} iteration with no app set. ` +
|
|
514
|
+
`Pass \`--app <path-to-${ext}-or-bundle-id>\` on this run, ` +
|
|
515
|
+
`or store one on the iteration via \`ish iteration update ${iterAlias} --details-json '{"app_artifact":"<bundle-id-or-path>"}'\`, then retry.`);
|
|
516
|
+
}
|
|
517
|
+
const flagHint = describeRequiredContentFlag(modality, isPair ? "participant_pair" : undefined);
|
|
499
518
|
throw new Error(`Iteration "${iterationLabel}" (${iterAlias}) has no ${isMedia ? "content" : isPair ? "people/scenarios" : isChat ? "endpoint" : "URL"} configured yet. ` +
|
|
500
519
|
`Add ${isMedia ? "content" : isPair ? "the pair-mode payload" : isChat ? "an endpoint" : "a URL"} with ` +
|
|
501
520
|
`\`ish iteration create --study ${resolvedStudy} ${flagHint}\` ` +
|
|
@@ -296,7 +296,7 @@ async function collectWorkspaceUsage(client, workspaceId) {
|
|
|
296
296
|
people_used: typeof participants.total === "number" ? participants.total : 0,
|
|
297
297
|
people_max: lookupLimit("maxCustomPersons"),
|
|
298
298
|
concurrent_participants_max: lookupLimit("maxConcurrentParticipants"),
|
|
299
|
-
workspace_members_max: lookupLimit("
|
|
299
|
+
workspace_members_max: lookupLimit("maxSeats"),
|
|
300
300
|
};
|
|
301
301
|
}
|
|
302
302
|
// ---------------------------------------------------------------------------
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { registerInitCommands } from "./commands/init.js";
|
|
|
20
20
|
import { registerMcpCommands } from "./commands/mcp.js";
|
|
21
21
|
import { registerSecretCommands } from "./commands/secret.js";
|
|
22
22
|
import { registerDoctorCommands } from "./commands/doctor.js";
|
|
23
|
+
import { registerFeedbackCommands } from "./commands/feedback.js";
|
|
23
24
|
import { AGENT_HELP_FOOTER } from "./lib/docs.js";
|
|
24
25
|
import { runInline, EXIT_USAGE, injectGlobalWorkspaceOption } from "./lib/command-helpers.js";
|
|
25
26
|
import { resolveApiUrl, resolveToken, verifyToken } from "./lib/auth.js";
|
|
@@ -512,6 +513,7 @@ registerInitCommands(program);
|
|
|
512
513
|
registerMcpCommands(program);
|
|
513
514
|
registerSecretCommands(program);
|
|
514
515
|
registerDoctorCommands(program);
|
|
516
|
+
registerFeedbackCommands(program);
|
|
515
517
|
program
|
|
516
518
|
.command("upgrade")
|
|
517
519
|
.description("Update ish to the latest version")
|
|
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import { resolveApiUrl, resolveToken } from "./auth.js";
|
|
7
7
|
import { getAppUrl } from "../auth.js";
|
|
8
8
|
import { ApiClient, ApiError } from "./api-client.js";
|
|
9
|
-
import { outputError, setVerbose, setFields, setGetField } from "./output.js";
|
|
9
|
+
import { outputError, shouldSuggestReport, setVerbose, setFields, setGetField } from "./output.js";
|
|
10
10
|
import { setColorsEnabled, colorsEnabled } from "./colors.js";
|
|
11
11
|
import { loadConfig } from "../config.js";
|
|
12
12
|
import { resolveId } from "./alias-store.js";
|
|
@@ -513,7 +513,10 @@ export async function withClient(cmd, fn) {
|
|
|
513
513
|
await fn(client, globals);
|
|
514
514
|
}
|
|
515
515
|
catch (err) {
|
|
516
|
-
|
|
516
|
+
// Nudge to report genuine faults — but never when the failing command
|
|
517
|
+
// IS `ish feedback` (don't suggest reporting via the thing that broke).
|
|
518
|
+
const suggestReport = shouldSuggestReport(err) && commandPath(cmd).split(".")[0] !== "feedback";
|
|
519
|
+
outputError(err, globals.json, { suggestReport });
|
|
517
520
|
await exitWithFlush(exitCodeFromError(err));
|
|
518
521
|
}
|
|
519
522
|
});
|
|
@@ -530,7 +533,8 @@ export async function runInline(cmd, fn) {
|
|
|
530
533
|
await fn(globals);
|
|
531
534
|
}
|
|
532
535
|
catch (err) {
|
|
533
|
-
|
|
536
|
+
const suggestReport = shouldSuggestReport(err) && commandPath(cmd).split(".")[0] !== "feedback";
|
|
537
|
+
outputError(err, globals.json, { suggestReport });
|
|
534
538
|
await exitWithFlush(exitCodeFromError(err));
|
|
535
539
|
}
|
|
536
540
|
});
|
package/dist/lib/docs.js
CHANGED
|
@@ -2549,6 +2549,15 @@ from envelopes that don't originate from an HTTP response (Commander
|
|
|
2549
2549
|
parse errors, local validation failures, alias-resolution errors). Do
|
|
2550
2550
|
not branch on \`status: 0\` — that value is never emitted as of 0.20.
|
|
2551
2551
|
|
|
2552
|
+
A **\`report\`** field appears on *genuine faults* — uncategorized
|
|
2553
|
+
client errors, \`server\`/5xx, network failures, and unknown throws —
|
|
2554
|
+
carrying the suggested \`ish feedback "…"\` invocation to report the
|
|
2555
|
+
bug. It is deliberately **absent** on user-actionable failures (usage /
|
|
2556
|
+
validation exit 2, auth exit 3, not-found exit 4, \`usage_limit_reached\`)
|
|
2557
|
+
so agents don't report their own input mistakes. In human mode the same
|
|
2558
|
+
nudge prints as a \`→ Looks like a bug? Report it: …\` line on stderr.
|
|
2559
|
+
See \`guides/feedback\`.
|
|
2560
|
+
|
|
2552
2561
|
## Conventions
|
|
2553
2562
|
|
|
2554
2563
|
- Successful commands exit 0 and print one JSON object/array on stdout.
|
|
@@ -3224,23 +3233,26 @@ request time, for any client, is the backend's \`TIER_LIMITS\` dict in
|
|
|
3224
3233
|
|
|
3225
3234
|
## Limits enforced
|
|
3226
3235
|
|
|
3227
|
-
| Limit | Free |
|
|
3228
|
-
|
|
3229
|
-
| \`maxProducts\` | 1 |
|
|
3230
|
-
| \`maxStudiesPerProduct\` | 3 |
|
|
3231
|
-
| \`
|
|
3232
|
-
| \`
|
|
3233
|
-
| \`
|
|
3234
|
-
| \`
|
|
3235
|
-
| \`maxSeats\` | 1 | 1
|
|
3236
|
+
| Limit | Free | Starter | Pro | Enterprise |
|
|
3237
|
+
|-----------------------------|------|---------|-----|------------|
|
|
3238
|
+
| \`maxProducts\` | 1 | 3 | ∞ | ∞ |
|
|
3239
|
+
| \`maxStudiesPerProduct\` | 3 | 15 | ∞ | ∞ |
|
|
3240
|
+
| \`maxAsksPerProduct\` | 3 | 15 | ∞ | ∞ |
|
|
3241
|
+
| \`maxIterationsPerStudy\` | 2 | 5 | ∞ | ∞ |
|
|
3242
|
+
| \`maxCustomPersons\` | 3 | 10 | ∞ | ∞ |
|
|
3243
|
+
| \`maxConcurrentParticipants\` | 3 | 10 | 50 | ∞ |
|
|
3244
|
+
| \`maxSeats\` | 1 | 1 | 10 | ∞ |
|
|
3236
3245
|
|
|
3237
3246
|
Commands that may hit a limit: \`ish workspace create\`,
|
|
3238
|
-
\`ish study create\`, \`ish study generate\`, \`ish
|
|
3247
|
+
\`ish study create\`, \`ish study generate\`, \`ish ask create\`
|
|
3248
|
+
(and \`ish ask run --new\`), \`ish iteration create\`,
|
|
3239
3249
|
\`ish person create\`, \`ish person generate\`.
|
|
3240
3250
|
|
|
3241
|
-
\`maxConcurrentParticipants\`
|
|
3242
|
-
|
|
3243
|
-
|
|
3251
|
+
\`maxConcurrentParticipants\` caps how many participants a single run can
|
|
3252
|
+
include — cumulative per iteration for studies, per ask for asks — and is
|
|
3253
|
+
enforced when participants are created/added, not at dispatch. \`maxSeats\`
|
|
3254
|
+
gates workspace membership (active members + pending invitations). Both are
|
|
3255
|
+
enforced server-side.
|
|
3244
3256
|
|
|
3245
3257
|
## What you see when a limit is hit
|
|
3246
3258
|
|
|
@@ -3292,6 +3304,7 @@ upgrade or delete an existing resource to free up headroom.
|
|
|
3292
3304
|
that page is about *how many credits each run draws*).
|
|
3293
3305
|
- \`concepts/workspace\` — \`maxProducts\` is per-account.
|
|
3294
3306
|
- \`concepts/study\` — \`maxStudiesPerProduct\` gates study creation.
|
|
3307
|
+
- \`concepts/ask\` — \`maxAsksPerProduct\` gates ask creation.
|
|
3295
3308
|
- \`concepts/iteration\` — \`maxIterationsPerStudy\` gates iteration creation.
|
|
3296
3309
|
- \`concepts/person\` — \`maxCustomPersons\` gates person creation.
|
|
3297
3310
|
- \`reference/json-mode\` — full error envelope shape and exit codes.
|
|
@@ -4410,6 +4423,14 @@ The platform defaults to the iteration's; \`--app\` on \`study run\` overrides t
|
|
|
4410
4423
|
stored target with a fresh local build. The WebDriverAgent runner cold-starts
|
|
4411
4424
|
slowly the first time (~30-60s) and is then reused across participants.
|
|
4412
4425
|
|
|
4426
|
+
If the iteration was created **without** \`--app\` (its target is "chosen at run
|
|
4427
|
+
time"), supply the app on the run instead — there's nothing stored to default
|
|
4428
|
+
from:
|
|
4429
|
+
|
|
4430
|
+
\`\`\`
|
|
4431
|
+
ish study run --local --platform ios --app ./Build.app --all --wait
|
|
4432
|
+
\`\`\`
|
|
4433
|
+
|
|
4413
4434
|
## 3b. Parallel runs — \`--parallel N\` (iOS + Android)
|
|
4414
4435
|
|
|
4415
4436
|
\`\`\`
|
|
@@ -4466,6 +4487,74 @@ reproducible runs.
|
|
|
4466
4487
|
- \`reference/screenshots\` — grouped index vs per-interaction frames.
|
|
4467
4488
|
- \`guides/first-study\` — the browser-URL version of this flow.
|
|
4468
4489
|
`;
|
|
4490
|
+
const GUIDE_FEEDBACK = `# guide: report a bug or send feedback
|
|
4491
|
+
|
|
4492
|
+
\`ish feedback\` files a bug report, feature request, or general note
|
|
4493
|
+
straight from the terminal — to the same place the web app's "Send
|
|
4494
|
+
feedback" dialog goes. As the agent operating the CLI, you usually see
|
|
4495
|
+
a failure first; this is how you hand it to the team without leaving
|
|
4496
|
+
the shell.
|
|
4497
|
+
|
|
4498
|
+
\`\`\`bash
|
|
4499
|
+
ish feedback "the save button on the profile page does nothing"
|
|
4500
|
+
ish feedback --type feature "let me export interactions as CSV"
|
|
4501
|
+
ish feedback --type other "the guided setup was great"
|
|
4502
|
+
\`\`\`
|
|
4503
|
+
|
|
4504
|
+
## The message
|
|
4505
|
+
|
|
4506
|
+
Pass the body as the positional argument, or read it from elsewhere —
|
|
4507
|
+
the same idioms as other text fields:
|
|
4508
|
+
|
|
4509
|
+
- \`ish feedback @bug-report.md\` — read the body from a file.
|
|
4510
|
+
- \`… 2>err.txt; ish feedback @err.txt\` — attach a failing command's output.
|
|
4511
|
+
- \`some-cmd 2>&1 | ish feedback -\` — read the body from stdin.
|
|
4512
|
+
|
|
4513
|
+
The body must be at least 10 characters. \`--title\` is optional; by
|
|
4514
|
+
default the first line of the message becomes the title.
|
|
4515
|
+
|
|
4516
|
+
## Type
|
|
4517
|
+
|
|
4518
|
+
\`--type\` is one of \`bug\` (default), \`feature\`, or \`other\` — the same
|
|
4519
|
+
three the web dialog offers.
|
|
4520
|
+
|
|
4521
|
+
## Diagnostics (help whoever debugs it)
|
|
4522
|
+
|
|
4523
|
+
To give the debugger as much grounded signal as possible, the report
|
|
4524
|
+
auto-attaches an environment + account block: ish CLI version, OS,
|
|
4525
|
+
node, the logged-in account, and your active workspace/study/ask. Opt
|
|
4526
|
+
out with \`--no-diagnostics\`.
|
|
4527
|
+
|
|
4528
|
+
\`--health\` adds more for simulation/setup bugs: it runs the \`ish check\`
|
|
4529
|
+
setup checklist (Xcode, simulators, adb, Chromium, account) and
|
|
4530
|
+
attaches the tail of \`~/.ish/local-sim.log\` when present. Use it
|
|
4531
|
+
whenever the bug involves \`study run --local\` or native device setup.
|
|
4532
|
+
|
|
4533
|
+
\`--dry-run\` prints exactly what would be sent (message + diagnostics)
|
|
4534
|
+
without submitting — preview it first if you're unsure.
|
|
4535
|
+
|
|
4536
|
+
## When the CLI nudges you to report
|
|
4537
|
+
|
|
4538
|
+
On a *genuine fault* (uncategorized client error, 5xx/\`server\`, network
|
|
4539
|
+
failure, unknown throw) the CLI appends a hint: a \`report\` field in the
|
|
4540
|
+
\`--json\` error envelope, and a \`→ Looks like a bug? Report it: …\` line
|
|
4541
|
+
on stderr in human mode. It is intentionally **silent** on
|
|
4542
|
+
user-actionable failures (usage, validation, auth, not-found,
|
|
4543
|
+
usage-limit) so the nudge stays high-signal. When you see it, that
|
|
4544
|
+
error is worth an \`ish feedback\` — paste what you were doing and add
|
|
4545
|
+
\`--health\` if it was a local/native run.
|
|
4546
|
+
|
|
4547
|
+
## Where it lands
|
|
4548
|
+
|
|
4549
|
+
Sends as the logged-in user (run \`ish login\` first) through the ish
|
|
4550
|
+
backend, so reports show up alongside web feedback in the team's review
|
|
4551
|
+
surface. Output is the created row \`{id, type, title, status}\`.
|
|
4552
|
+
|
|
4553
|
+
## Related
|
|
4554
|
+
|
|
4555
|
+
- \`reference/json-mode\` — the \`report\` field + error envelope.
|
|
4556
|
+
- \`guides/native-app\` — when to reach for \`--health\`.
|
|
4557
|
+
`;
|
|
4469
4558
|
const PAGES = [
|
|
4470
4559
|
{
|
|
4471
4560
|
slug: "overview",
|
|
@@ -4641,6 +4730,12 @@ const PAGES = [
|
|
|
4641
4730
|
description: "One command wires the hosted ish MCP server into Cursor, VS Code, Claude Code, Claude Desktop, and Windsurf. Idempotent, atomic, preserves unrelated keys, no tokens written.",
|
|
4642
4731
|
body: GUIDE_MCP_ADD,
|
|
4643
4732
|
},
|
|
4733
|
+
{
|
|
4734
|
+
slug: "guides/feedback",
|
|
4735
|
+
title: "guide: report a bug or send feedback (`ish feedback`)",
|
|
4736
|
+
description: "File a bug/feature/other report from the terminal: message via arg/@file/stdin, --type, auto-attached environment+account diagnostics (--no-diagnostics to opt out), --health for setup checks + local-sim logs, --dry-run preview. Also: the `report` hint the CLI appends to genuine-fault errors.",
|
|
4737
|
+
body: GUIDE_FEEDBACK,
|
|
4738
|
+
},
|
|
4644
4739
|
];
|
|
4645
4740
|
const PAGES_BY_SLUG = new Map(PAGES.map((p) => [p.slug, p]));
|
|
4646
4741
|
export function listPages() {
|
|
@@ -10,15 +10,31 @@
|
|
|
10
10
|
* lives there, not here.
|
|
11
11
|
*/
|
|
12
12
|
import type { Page } from "playwright-core";
|
|
13
|
-
import type { LocalStepAction, ActionResult, ContextValue, TreeData } from "./types.js";
|
|
13
|
+
import type { LocalStepAction, ActionResult, ContextValue, WireContextValue, TreeData } from "./types.js";
|
|
14
14
|
import type { TabManager } from "./tabs.js";
|
|
15
15
|
/**
|
|
16
16
|
* Execute a single action on the page.
|
|
17
17
|
*/
|
|
18
18
|
export declare function executeAction(page: Page, action: LocalStepAction, treeData: TreeData, contextValues: ContextValue[], tabs?: TabManager): Promise<ActionResult>;
|
|
19
|
+
/**
|
|
20
|
+
* Normalize the backend's wire context-value shape ({@link WireContextValue}:
|
|
21
|
+
* `{key, requires_resolution, …}`) into the internal {@link ContextValue}
|
|
22
|
+
* ({name, type, …}) the executors and {@link resolveTextValue} consume.
|
|
23
|
+
*
|
|
24
|
+
* This mapping is the fix for a long-standing silent break: the backend has
|
|
25
|
+
* always sent `key`/`requires_resolution`, the CLI has always read `name`/`type`,
|
|
26
|
+
* and nothing mapped between them — so var/secret lookups never matched and the
|
|
27
|
+
* agent typed the literal key (e.g. `LOGIN_USERNAME`) into login fields on web,
|
|
28
|
+
* iOS and Android alike. `requires_resolution` is true for secrets (resolved at
|
|
29
|
+
* runtime) and false for preloaded variables.
|
|
30
|
+
*/
|
|
31
|
+
export declare function normalizeContextValues(wire: WireContextValue[]): ContextValue[];
|
|
19
32
|
/**
|
|
20
33
|
* Resolve the actual text to type from an action, handling var/secret value types.
|
|
21
|
-
* Exported so the native (Android)
|
|
34
|
+
* Exported so the native (Android/iOS) executors can resolve values the same way.
|
|
35
|
+
*
|
|
36
|
+
* `contextValues` must already be normalized via {@link normalizeContextValues}
|
|
37
|
+
* — the lookup matches on `name`, which is the backend's `key` post-mapping.
|
|
22
38
|
*/
|
|
23
39
|
export declare function resolveTextValue(action: LocalStepAction, contextValues: ContextValue[]): string;
|
|
24
40
|
/**
|
|
@@ -376,9 +376,32 @@ async function executeKeyboardShortcut(page, action) {
|
|
|
376
376
|
await page.keyboard.press(combo);
|
|
377
377
|
}
|
|
378
378
|
// --- Helpers ---
|
|
379
|
+
/**
|
|
380
|
+
* Normalize the backend's wire context-value shape ({@link WireContextValue}:
|
|
381
|
+
* `{key, requires_resolution, …}`) into the internal {@link ContextValue}
|
|
382
|
+
* ({name, type, …}) the executors and {@link resolveTextValue} consume.
|
|
383
|
+
*
|
|
384
|
+
* This mapping is the fix for a long-standing silent break: the backend has
|
|
385
|
+
* always sent `key`/`requires_resolution`, the CLI has always read `name`/`type`,
|
|
386
|
+
* and nothing mapped between them — so var/secret lookups never matched and the
|
|
387
|
+
* agent typed the literal key (e.g. `LOGIN_USERNAME`) into login fields on web,
|
|
388
|
+
* iOS and Android alike. `requires_resolution` is true for secrets (resolved at
|
|
389
|
+
* runtime) and false for preloaded variables.
|
|
390
|
+
*/
|
|
391
|
+
export function normalizeContextValues(wire) {
|
|
392
|
+
return wire.map(cv => ({
|
|
393
|
+
name: cv.key,
|
|
394
|
+
type: cv.requires_resolution ? "secret" : "var",
|
|
395
|
+
value: cv.value,
|
|
396
|
+
...(cv.description != null && { description: cv.description }),
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
379
399
|
/**
|
|
380
400
|
* Resolve the actual text to type from an action, handling var/secret value types.
|
|
381
|
-
* Exported so the native (Android)
|
|
401
|
+
* Exported so the native (Android/iOS) executors can resolve values the same way.
|
|
402
|
+
*
|
|
403
|
+
* `contextValues` must already be normalized via {@link normalizeContextValues}
|
|
404
|
+
* — the lookup matches on `name`, which is the backend's `key` post-mapping.
|
|
382
405
|
*/
|
|
383
406
|
export function resolveTextValue(action, contextValues) {
|
|
384
407
|
if (action.value_type === "var" || action.value_type === "secret") {
|
|
@@ -403,10 +403,15 @@ export class AndroidDevice {
|
|
|
403
403
|
await settle(250);
|
|
404
404
|
}
|
|
405
405
|
const text = resolveTextValue(action, this.contextValues);
|
|
406
|
-
|
|
406
|
+
// Clear before typing by default — ADBKeyboard's type broadcast APPENDS, so
|
|
407
|
+
// a retried or app-pre-filled field would otherwise accumulate text. This is
|
|
408
|
+
// the replace-on-type semantic the browser (select-all+type) and iOS paths
|
|
409
|
+
// also use; previously only click_type cleared. Skip the clear+type entirely
|
|
410
|
+
// when there's nothing to type so an empty no-op can't wipe the field.
|
|
411
|
+
if (text) {
|
|
407
412
|
await adbKeyboardClear();
|
|
413
|
+
await adbKeyboardType(text);
|
|
408
414
|
}
|
|
409
|
-
await adbKeyboardType(text);
|
|
410
415
|
if (action.submit) {
|
|
411
416
|
await settle(150);
|
|
412
417
|
await pressKeyEvent("KEYCODE_ENTER");
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
import { resolveTextValue } from "./actions.js";
|
|
34
34
|
import { requireOneBootedSimulator, screenshotPng, terminateApp, launchApp, installApp, uninstallApp, isAppInstalled, bundleIdFromApp, appBuildFromSimulator, } from "./simctl.js";
|
|
35
35
|
// iOS UI interaction + a11y run through WebDriverAgent (XCUITest), not idb.
|
|
36
|
-
import { ensureWda, closeWda, describeScreen, describeAll, activeBundleId, uiTap, uiLongPress, uiSwipe, uiText, uiKey, HID_KEY_RETURN, } from "./xcuitest.js";
|
|
36
|
+
import { ensureWda, closeWda, describeScreen, describeAll, activeBundleId, uiTap, uiLongPress, uiSwipe, uiText, uiClearActiveField, uiKey, HID_KEY_RETURN, } from "./xcuitest.js";
|
|
37
37
|
import { isLocalPath } from "../upload.js";
|
|
38
38
|
import { deNormalizePoint, deNormalizeDrag, pointToPixel } from "./coordinates.js";
|
|
39
39
|
import { parseXcuiHierarchy, serializeNativeTree, boundsCenter } from "./native-a11y.js";
|
|
@@ -456,12 +456,16 @@ export class IOSDevice {
|
|
|
456
456
|
await settle(250);
|
|
457
457
|
}
|
|
458
458
|
const text = resolveTextValue(action, this.contextValues);
|
|
459
|
-
// WDA
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
|
|
459
|
+
// WDA's /wda/keys APPENDS to the focused field, so clear it first — typing
|
|
460
|
+
// replaces by default (matching the browser select-all+type and Android's
|
|
461
|
+
// ADB_CLEAR_TEXT). uiClearActiveField uses WDA's native element clear on the
|
|
462
|
+
// focused element; it's best-effort, so a non-editable focus simply no-ops
|
|
463
|
+
// and we fall back to appending. Only clear when we actually have text to
|
|
464
|
+
// type, so an empty/no-op type never wipes the field.
|
|
465
|
+
if (text) {
|
|
466
|
+
await uiClearActiveField(this.udid);
|
|
464
467
|
await uiText(this.udid, text);
|
|
468
|
+
}
|
|
465
469
|
if (action.submit) {
|
|
466
470
|
await settle(150);
|
|
467
471
|
await uiKey(this.udid, HID_KEY_RETURN);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { appendFileSync } from "node:fs";
|
|
9
9
|
import { launchSharedBrowser, FULL_PAGE_HEIGHT_CAP_PX_MOBILE, FULL_PAGE_HEIGHT_CAP_PX_DESKTOP, } from "./browser.js";
|
|
10
10
|
import { uploadScreenshot } from "./upload.js";
|
|
11
|
-
import { detectNoVisibleChange, describeAction, classifyStepKind } from "./actions.js";
|
|
11
|
+
import { detectNoVisibleChange, describeAction, classifyStepKind, normalizeContextValues } from "./actions.js";
|
|
12
12
|
import { createDevice } from "./device.js";
|
|
13
13
|
import { nativeStateResetWarning } from "./ios.js";
|
|
14
14
|
import { provisionDevicePool, maxConcurrentDevices, totalMemBytes, PER_DEVICE_MB, } from "./device-pool.js";
|
|
@@ -427,7 +427,10 @@ async function runSingleSimulation(client, participantId, participantName, opts,
|
|
|
427
427
|
assignments: initResponse.assignments,
|
|
428
428
|
participant_background: initResponse.participant_background,
|
|
429
429
|
participant_language: initResponse.participant_language,
|
|
430
|
-
|
|
430
|
+
// Map the backend wire shape ({key, requires_resolution}) into the internal
|
|
431
|
+
// {name, type} ContextValue the devices/resolveTextValue expect. Without
|
|
432
|
+
// this every var/secret lookup misses and the literal key gets typed.
|
|
433
|
+
context_values: normalizeContextValues(initResponse.context_values),
|
|
431
434
|
max_interactions: initResponse.max_interactions,
|
|
432
435
|
agent_model: initResponse.agent_model,
|
|
433
436
|
dom_model: initResponse.dom_model,
|
|
@@ -27,7 +27,7 @@ export interface LocalSimInitResponse {
|
|
|
27
27
|
participant_background: Record<string, unknown> | null;
|
|
28
28
|
participant_language: string | null;
|
|
29
29
|
config: Record<string, unknown>;
|
|
30
|
-
context_values:
|
|
30
|
+
context_values: WireContextValue[];
|
|
31
31
|
max_interactions: number;
|
|
32
32
|
agent_model: string;
|
|
33
33
|
dom_model: string;
|
|
@@ -40,6 +40,32 @@ export interface LocalSimAssignment {
|
|
|
40
40
|
instructions: string;
|
|
41
41
|
sequence: number;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Wire shape of a context value exactly as the backend serializes it
|
|
45
|
+
* (ish-backend `app/runs/values.py` `ContextValue` → `/simulation/local/init`):
|
|
46
|
+
* the identifier field is `key` (NOT `name`), and `requires_resolution` is the
|
|
47
|
+
* secret-vs-variable discriminator (NO `type` field). The CLI normalizes this
|
|
48
|
+
* into the internal {@link ContextValue} shape at ingestion via
|
|
49
|
+
* {@link normalizeContextValues}.
|
|
50
|
+
*
|
|
51
|
+
* History: the internal shape below has used `name`/`type` since v0.7.0 while the
|
|
52
|
+
* backend has always sent `key`/`requires_resolution`. Because the two were
|
|
53
|
+
* assumed identical and consumed without mapping, every `resolveTextValue`
|
|
54
|
+
* lookup (`find(v => v.name === action.value)`) silently missed and the agent
|
|
55
|
+
* typed the literal key (e.g. `LOGIN_USERNAME`) into the field on every
|
|
56
|
+
* platform. Keep this interface faithful to the wire so the mismatch can't
|
|
57
|
+
* recur unnoticed.
|
|
58
|
+
*/
|
|
59
|
+
export interface WireContextValue {
|
|
60
|
+
key: string;
|
|
61
|
+
description?: string | null;
|
|
62
|
+
value: string | null;
|
|
63
|
+
requires_resolution: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Internal context-value shape used by the executors / {@link resolveTextValue}.
|
|
67
|
+
* Produced from {@link WireContextValue} by {@link normalizeContextValues}.
|
|
68
|
+
*/
|
|
43
69
|
export interface ContextValue {
|
|
44
70
|
name: string;
|
|
45
71
|
type: "var" | "secret";
|
|
@@ -76,6 +76,30 @@ export declare function uiText(udid: string, text: string): Promise<void>;
|
|
|
76
76
|
* map it to a newline (see WDA_RETURN). Unknown codes are a no-op-safe error.
|
|
77
77
|
*/
|
|
78
78
|
export declare function uiKey(udid: string, keycode: number): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Extract the element id from a WDA active-element payload, accepting both the
|
|
81
|
+
* W3C key and the legacy `ELEMENT` alias. Pure + exported for unit testing the
|
|
82
|
+
* (fiddly) key handling without a live runner. Returns null when the shape has
|
|
83
|
+
* no usable id (nothing focused / non-element response).
|
|
84
|
+
*/
|
|
85
|
+
export declare function parseActiveElementId(value: unknown): string | null;
|
|
86
|
+
/**
|
|
87
|
+
* Clear the currently-focused text field via WDA's native element clear — the
|
|
88
|
+
* same select-all+delete Appium's `clear()` performs, so it handles multiline
|
|
89
|
+
* and arbitrary cursor positions correctly.
|
|
90
|
+
*
|
|
91
|
+
* Why this exists: `uiText` posts to `/wda/keys`, which APPENDS to the focused
|
|
92
|
+
* field. Without a clear, re-typing a field (a retry, or a field the app
|
|
93
|
+
* pre-filled) accumulates text (e.g. `LOGIN_USERNAMELOGIN_USERNAME…`). The
|
|
94
|
+
* coordinate/vision typing path has no element uuid up front, so we resolve the
|
|
95
|
+
* focused element with `GET …/element/active`, then `POST …/element/:uuid/clear`.
|
|
96
|
+
*
|
|
97
|
+
* Best-effort by design: a non-editable focus (or any WDA hiccup) just returns
|
|
98
|
+
* false and the caller falls back to the prior append behavior — clearing is an
|
|
99
|
+
* improvement, never a hard precondition for typing. Returns true when a clear
|
|
100
|
+
* was actually issued.
|
|
101
|
+
*/
|
|
102
|
+
export declare function uiClearActiveField(udid: string): Promise<boolean>;
|
|
79
103
|
/** Re-export so a future ios.ts can drop the simctl HID constant. */
|
|
80
104
|
export declare const HID_KEY_RETURN = 40;
|
|
81
105
|
export {};
|
|
@@ -331,5 +331,53 @@ export async function uiKey(udid, keycode) {
|
|
|
331
331
|
const s = await getSession(udid);
|
|
332
332
|
await wdaCall(s.port, "POST", `/session/${s.sessionId}/wda/keys`, { value: [WDA_RETURN] });
|
|
333
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* The W3C WebDriver element-reference key. `GET …/element/active` returns the
|
|
336
|
+
* focused element under this key (WDA also historically mirrors it as `ELEMENT`).
|
|
337
|
+
*/
|
|
338
|
+
const W3C_ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf";
|
|
339
|
+
/**
|
|
340
|
+
* Extract the element id from a WDA active-element payload, accepting both the
|
|
341
|
+
* W3C key and the legacy `ELEMENT` alias. Pure + exported for unit testing the
|
|
342
|
+
* (fiddly) key handling without a live runner. Returns null when the shape has
|
|
343
|
+
* no usable id (nothing focused / non-element response).
|
|
344
|
+
*/
|
|
345
|
+
export function parseActiveElementId(value) {
|
|
346
|
+
if (!value || typeof value !== "object")
|
|
347
|
+
return null;
|
|
348
|
+
const obj = value;
|
|
349
|
+
const id = obj[W3C_ELEMENT_KEY] ?? obj.ELEMENT;
|
|
350
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Clear the currently-focused text field via WDA's native element clear — the
|
|
354
|
+
* same select-all+delete Appium's `clear()` performs, so it handles multiline
|
|
355
|
+
* and arbitrary cursor positions correctly.
|
|
356
|
+
*
|
|
357
|
+
* Why this exists: `uiText` posts to `/wda/keys`, which APPENDS to the focused
|
|
358
|
+
* field. Without a clear, re-typing a field (a retry, or a field the app
|
|
359
|
+
* pre-filled) accumulates text (e.g. `LOGIN_USERNAMELOGIN_USERNAME…`). The
|
|
360
|
+
* coordinate/vision typing path has no element uuid up front, so we resolve the
|
|
361
|
+
* focused element with `GET …/element/active`, then `POST …/element/:uuid/clear`.
|
|
362
|
+
*
|
|
363
|
+
* Best-effort by design: a non-editable focus (or any WDA hiccup) just returns
|
|
364
|
+
* false and the caller falls back to the prior append behavior — clearing is an
|
|
365
|
+
* improvement, never a hard precondition for typing. Returns true when a clear
|
|
366
|
+
* was actually issued.
|
|
367
|
+
*/
|
|
368
|
+
export async function uiClearActiveField(udid) {
|
|
369
|
+
try {
|
|
370
|
+
const s = await getSession(udid);
|
|
371
|
+
const active = unwrap(await wdaCall(s.port, "GET", `/session/${s.sessionId}/element/active`));
|
|
372
|
+
const uuid = parseActiveElementId(active);
|
|
373
|
+
if (!uuid)
|
|
374
|
+
return false;
|
|
375
|
+
await wdaCall(s.port, "POST", `/session/${s.sessionId}/element/${uuid}/clear`, {});
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
334
382
|
/** Re-export so a future ios.ts can drop the simctl HID constant. */
|
|
335
383
|
export const HID_KEY_RETURN = 40;
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -46,7 +46,17 @@ export declare class ValidationError extends Error {
|
|
|
46
46
|
hint?: string | undefined;
|
|
47
47
|
constructor(message: string, valid_options: string[], hint?: string | undefined);
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Whether an error is worth nudging the user to report via `ish feedback`.
|
|
51
|
+
* Excludes user-actionable failures — usage/validation, auth (login),
|
|
52
|
+
* not-found, and usage-limit (upgrade) — so the hint stays high-signal and
|
|
53
|
+
* agents don't reflexively report their own input mistakes. Genuine faults
|
|
54
|
+
* (5xx, unexpected client errors, unknown throws) get the nudge.
|
|
55
|
+
*/
|
|
56
|
+
export declare function shouldSuggestReport(err: unknown): boolean;
|
|
57
|
+
export declare function outputError(err: unknown, json: boolean, opts?: {
|
|
58
|
+
suggestReport?: boolean;
|
|
59
|
+
}): void;
|
|
50
60
|
export declare function printTable(headers: string[], rows: string[][]): void;
|
|
51
61
|
export declare function printKeyValue(obj: Record<string, unknown>, indent?: string): void;
|
|
52
62
|
export declare function formatWorkspaceList(workspaces: Record<string, unknown>[], json: boolean): void;
|
package/dist/lib/output.js
CHANGED
|
@@ -562,8 +562,48 @@ function remapEntityName(message) {
|
|
|
562
562
|
.replace(/\bAttachment not found\b/g, "Source not found")
|
|
563
563
|
.replace(/\battachment not found\b/g, "source not found");
|
|
564
564
|
}
|
|
565
|
-
|
|
565
|
+
// Shown after a genuine fault so the operating agent (the CLI's primary user)
|
|
566
|
+
// knows it can hand the failure straight to the ish team. Kept short.
|
|
567
|
+
const REPORT_HINT_HUMAN = 'Looks like a bug? Report it: ish feedback "what you were trying to do" (add --health for setup/sim issues)';
|
|
568
|
+
const REPORT_HINT_JSON = 'ish feedback "<what you were trying to do>" — add --health for setup/sim issues';
|
|
569
|
+
/**
|
|
570
|
+
* Whether an error is worth nudging the user to report via `ish feedback`.
|
|
571
|
+
* Excludes user-actionable failures — usage/validation, auth (login),
|
|
572
|
+
* not-found, and usage-limit (upgrade) — so the hint stays high-signal and
|
|
573
|
+
* agents don't reflexively report their own input mistakes. Genuine faults
|
|
574
|
+
* (5xx, unexpected client errors, unknown throws) get the nudge.
|
|
575
|
+
*/
|
|
576
|
+
export function shouldSuggestReport(err) {
|
|
577
|
+
if (err instanceof ValidationError)
|
|
578
|
+
return false;
|
|
579
|
+
if (err instanceof ApiError) {
|
|
580
|
+
if (err.status === 400 ||
|
|
581
|
+
err.status === 401 ||
|
|
582
|
+
err.status === 402 || // insufficient credits — buy credits, not a bug
|
|
583
|
+
err.status === 403 ||
|
|
584
|
+
err.status === 404 ||
|
|
585
|
+
err.status === 422) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
if (err.error_code === "usage_limit_reached")
|
|
589
|
+
return false;
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
if (err instanceof Error) {
|
|
593
|
+
const tagged = err;
|
|
594
|
+
if (err.name === "ValidationError")
|
|
595
|
+
return false;
|
|
596
|
+
if (tagged.error_code === "auth_failed")
|
|
597
|
+
return false;
|
|
598
|
+
if (tagged.error_kind === "ConfirmationRequired")
|
|
599
|
+
return false;
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
export function outputError(err, json, opts = {}) {
|
|
566
605
|
const suggestions = suggestionsForError(err);
|
|
606
|
+
const report = opts.suggestReport === true;
|
|
567
607
|
if (err instanceof ApiError) {
|
|
568
608
|
// Surface backend-structured fields when present in the response body
|
|
569
609
|
// (e.g. 422 errors return `errors: [{loc, msg, type, input, allowed_values}]`,
|
|
@@ -621,6 +661,7 @@ export function outputError(err, json) {
|
|
|
621
661
|
...(seededAliases && { seeded_but_not_dispatched_aliases: seededAliases }),
|
|
622
662
|
...(bodyErrors !== undefined && { errors: bodyErrors }),
|
|
623
663
|
...(mergedSuggestions.length > 0 && { suggestions: mergedSuggestions }),
|
|
664
|
+
...(report && { report: REPORT_HINT_JSON }),
|
|
624
665
|
}));
|
|
625
666
|
}
|
|
626
667
|
else {
|
|
@@ -724,6 +765,7 @@ export function outputError(err, json) {
|
|
|
724
765
|
...(seededIds && { seeded_but_not_dispatched_ids: seededIds }),
|
|
725
766
|
...(seededAliases && { seeded_but_not_dispatched_aliases: seededAliases }),
|
|
726
767
|
...(mergedSuggestions.length > 0 && { suggestions: mergedSuggestions }),
|
|
768
|
+
...(report && { report: REPORT_HINT_JSON }),
|
|
727
769
|
}));
|
|
728
770
|
}
|
|
729
771
|
else {
|
|
@@ -736,12 +778,24 @@ export function outputError(err, json) {
|
|
|
736
778
|
}
|
|
737
779
|
else {
|
|
738
780
|
if (json) {
|
|
739
|
-
console.error(JSON.stringify({
|
|
781
|
+
console.error(JSON.stringify({
|
|
782
|
+
error: String(err),
|
|
783
|
+
error_code: "unknown_error",
|
|
784
|
+
retryable: false,
|
|
785
|
+
...(report && { report: REPORT_HINT_JSON }),
|
|
786
|
+
}));
|
|
740
787
|
}
|
|
741
788
|
else {
|
|
742
789
|
console.error(`Error: ${err}`);
|
|
743
790
|
}
|
|
744
791
|
}
|
|
792
|
+
// A single human-mode nudge for genuine faults (gated by the caller via
|
|
793
|
+
// shouldSuggestReport). The JSON branches above carry the same hint as a
|
|
794
|
+
// `report` field. ValidationError's own JSON branch intentionally omits it
|
|
795
|
+
// (usage errors aren't bugs); the caller never sets suggestReport there.
|
|
796
|
+
if (!json && report) {
|
|
797
|
+
console.error(` ${c.dim}→ ${REPORT_HINT_HUMAN}${c.reset}`);
|
|
798
|
+
}
|
|
745
799
|
}
|
|
746
800
|
// --- Entity-specific formatters (human mode) ---
|
|
747
801
|
export function printTable(headers, rows) {
|
|
@@ -1143,6 +1143,14 @@ table, projection shapes, and the defensive null-handling rules.
|
|
|
1143
1143
|
confirmed. The orphan-tunnel-on-startup-404 bug is fixed.
|
|
1144
1144
|
- The \`Warning: Could not verify token (network error). Proceeding
|
|
1145
1145
|
anyway.\` stderr line is gone on green runs.
|
|
1146
|
+
- On a **genuine fault** (uncategorized client error, 5xx/\`server\`,
|
|
1147
|
+
network, unknown throw) the error envelope adds a \`report\` field and
|
|
1148
|
+
human mode prints a \`→ Looks like a bug? Report it: …\` line. File it
|
|
1149
|
+
with \`ish feedback "what you were doing"\` (add \`--health\` for
|
|
1150
|
+
local/native-run bugs). The nudge is deliberately silent on
|
|
1151
|
+
user-actionable errors (usage, validation, auth, not-found,
|
|
1152
|
+
usage-limit), so when you see it, it's worth reporting. See
|
|
1153
|
+
guides/feedback.
|
|
1146
1154
|
|
|
1147
1155
|
## Common reshaping → use the CLI, not jq/python
|
|
1148
1156
|
|
|
@@ -1211,6 +1219,7 @@ ish <command> --help
|
|
|
1211
1219
|
| | study, ask, token validity) — alias \`whoami\` | |
|
|
1212
1220
|
| \`connect\` | Cloudflare tunnel exposing localhost | — |
|
|
1213
1221
|
| \`upgrade\` | Self-update | — |
|
|
1222
|
+
| \`feedback\` | Report a bug / feature request / note to the ish team. \`--health\` attaches setup checks + local-sim logs. | guides/feedback |
|
|
1214
1223
|
|
|
1215
1224
|
## Discovering flags safely
|
|
1216
1225
|
|