taphound 0.2.0-dev.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/LICENSE +202 -0
- package/README.md +200 -0
- package/README.zh-CN.md +200 -0
- package/assets/brand/taphound-mark.svg +9 -0
- package/assets/skills/taphound-ai-journey/GUIDE.md +862 -0
- package/assets/skills/taphound-ai-journey/SKILL.md +377 -0
- package/assets/skills/taphound-ai-journey/prompts/analyze-project.md +526 -0
- package/assets/skills/taphound-ai-journey/prompts/check-completion.md +52 -0
- package/assets/skills/taphound-ai-journey/prompts/generate-step.md +109 -0
- package/assets/skills/taphound-ai-journey/schemas/observe-output.json +42 -0
- package/assets/skills/taphound-ai-journey/schemas/project-context.json +81 -0
- package/assets/skills/taphound-ai-journey/schemas/proposed-step-envelope.json +267 -0
- package/assets/skills/taphound-ai-journey/templates/project-context.example.json +49 -0
- package/dist/adapters/adb/adb-adapter.d.ts +23 -0
- package/dist/adapters/adb/adb-adapter.js +218 -0
- package/dist/adapters/android-cli/android-cli-adapter.d.ts +11 -0
- package/dist/adapters/android-cli/android-cli-adapter.js +64 -0
- package/dist/adapters/android-cli/layout-parser.d.ts +3 -0
- package/dist/adapters/android-cli/layout-parser.js +174 -0
- package/dist/adapters/clock/system-clock.d.ts +5 -0
- package/dist/adapters/clock/system-clock.js +13 -0
- package/dist/adapters/filesystem/artifact-store.d.ts +4 -0
- package/dist/adapters/filesystem/artifact-store.js +73 -0
- package/dist/adapters/filesystem/generation-meta-writer.d.ts +16 -0
- package/dist/adapters/filesystem/generation-meta-writer.js +34 -0
- package/dist/adapters/filesystem/generation-session-store.d.ts +68 -0
- package/dist/adapters/filesystem/generation-session-store.js +1603 -0
- package/dist/adapters/filesystem/journey-writer.d.ts +16 -0
- package/dist/adapters/filesystem/journey-writer.js +34 -0
- package/dist/adapters/filesystem/project-bound-file.d.ts +6 -0
- package/dist/adapters/filesystem/project-bound-file.js +240 -0
- package/dist/adapters/filesystem/project-file-inspector.d.ts +8 -0
- package/dist/adapters/filesystem/project-file-inspector.js +198 -0
- package/dist/adapters/filesystem/skill-installer.d.ts +10 -0
- package/dist/adapters/filesystem/skill-installer.js +39 -0
- package/dist/adapters/process/node-process-runner.d.ts +8 -0
- package/dist/adapters/process/node-process-runner.js +155 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.d.ts +55 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.js +213 -0
- package/dist/adapters/prompt/inquirer-init-prompt.d.ts +32 -0
- package/dist/adapters/prompt/inquirer-init-prompt.js +55 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.d.ts +47 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.js +157 -0
- package/dist/application/assertion/expectation-evaluator.d.ts +61 -0
- package/dist/application/assertion/expectation-evaluator.js +207 -0
- package/dist/application/collector/logcat-collector.d.ts +39 -0
- package/dist/application/collector/logcat-collector.js +107 -0
- package/dist/application/context/context-validator.d.ts +27 -0
- package/dist/application/context/context-validator.js +120 -0
- package/dist/application/doctor/doctor-service.d.ts +35 -0
- package/dist/application/doctor/doctor-service.js +178 -0
- package/dist/application/generation/focused-input.d.ts +2 -0
- package/dist/application/generation/focused-input.js +9 -0
- package/dist/application/generation/generation-confirmation-service.d.ts +76 -0
- package/dist/application/generation/generation-confirmation-service.js +383 -0
- package/dist/application/generation/generation-finalizer.d.ts +67 -0
- package/dist/application/generation/generation-finalizer.js +646 -0
- package/dist/application/generation/generation-publisher.d.ts +46 -0
- package/dist/application/generation/generation-publisher.js +178 -0
- package/dist/application/generation/generation-starter.d.ts +31 -0
- package/dist/application/generation/generation-starter.js +102 -0
- package/dist/application/generation/generation-step-executor.d.ts +52 -0
- package/dist/application/generation/generation-step-executor.js +668 -0
- package/dist/application/generation/proposed-step-validator.d.ts +11 -0
- package/dist/application/generation/proposed-step-validator.js +88 -0
- package/dist/application/generation/risk-evaluator.d.ts +9 -0
- package/dist/application/generation/risk-evaluator.js +14 -0
- package/dist/application/generation/runtime-observer.d.ts +39 -0
- package/dist/application/generation/runtime-observer.js +157 -0
- package/dist/application/init/init-service.d.ts +17 -0
- package/dist/application/init/init-service.js +44 -0
- package/dist/application/interaction/action-executor.d.ts +25 -0
- package/dist/application/interaction/action-executor.js +103 -0
- package/dist/application/interaction/fallback-resolver.d.ts +24 -0
- package/dist/application/interaction/fallback-resolver.js +48 -0
- package/dist/application/interaction/scroll-to-executor.d.ts +40 -0
- package/dist/application/interaction/scroll-to-executor.js +191 -0
- package/dist/application/locator/locator-resolver.d.ts +20 -0
- package/dist/application/locator/locator-resolver.js +89 -0
- package/dist/application/project/project-describer.d.ts +14 -0
- package/dist/application/project/project-describer.js +8 -0
- package/dist/application/recorder/locator-selector.d.ts +10 -0
- package/dist/application/recorder/locator-selector.js +80 -0
- package/dist/application/recorder/recorder-service.d.ts +41 -0
- package/dist/application/recorder/recorder-service.js +331 -0
- package/dist/application/report/report-writer.d.ts +10 -0
- package/dist/application/report/report-writer.js +39 -0
- package/dist/application/runtime/activity-waiter.d.ts +33 -0
- package/dist/application/runtime/activity-waiter.js +73 -0
- package/dist/application/runtime/launch-failure.d.ts +2 -0
- package/dist/application/runtime/launch-failure.js +20 -0
- package/dist/application/runtime/process-waiter.d.ts +27 -0
- package/dist/application/runtime/process-waiter.js +73 -0
- package/dist/application/runtime/step-runner.d.ts +50 -0
- package/dist/application/runtime/step-runner.js +429 -0
- package/dist/application/runtime/verify-runtime.d.ts +44 -0
- package/dist/application/runtime/verify-runtime.js +350 -0
- package/dist/application/wait/idle-waiter.d.ts +30 -0
- package/dist/application/wait/idle-waiter.js +90 -0
- package/dist/cli/commands/context.d.ts +3 -0
- package/dist/cli/commands/context.js +94 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +51 -0
- package/dist/cli/commands/generation.d.ts +3 -0
- package/dist/cli/commands/generation.js +518 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +104 -0
- package/dist/cli/commands/project.d.ts +3 -0
- package/dist/cli/commands/project.js +58 -0
- package/dist/cli/commands/record.d.ts +3 -0
- package/dist/cli/commands/record.js +88 -0
- package/dist/cli/commands/verify.d.ts +3 -0
- package/dist/cli/commands/verify.js +106 -0
- package/dist/cli/dependencies.d.ts +66 -0
- package/dist/cli/dependencies.js +204 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +73 -0
- package/dist/cli/output.d.ts +16 -0
- package/dist/cli/output.js +22 -0
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +43 -0
- package/dist/domain/activity.d.ts +7 -0
- package/dist/domain/activity.js +65 -0
- package/dist/domain/app-process.d.ts +7 -0
- package/dist/domain/app-process.js +10 -0
- package/dist/domain/config.d.ts +15 -0
- package/dist/domain/config.js +17 -0
- package/dist/domain/failure.d.ts +4 -0
- package/dist/domain/failure.js +43 -0
- package/dist/domain/generation.d.ts +912 -0
- package/dist/domain/generation.js +342 -0
- package/dist/domain/init.d.ts +42 -0
- package/dist/domain/init.js +114 -0
- package/dist/domain/journey.d.ts +673 -0
- package/dist/domain/journey.js +109 -0
- package/dist/domain/layout.d.ts +35 -0
- package/dist/domain/layout.js +33 -0
- package/dist/domain/locator.d.ts +8 -0
- package/dist/domain/locator.js +5 -0
- package/dist/domain/project-context.d.ts +101 -0
- package/dist/domain/project-context.js +64 -0
- package/dist/domain/proposed-step.d.ts +341 -0
- package/dist/domain/proposed-step.js +87 -0
- package/dist/domain/report.d.ts +349 -0
- package/dist/domain/report.js +135 -0
- package/dist/domain/runtime-snapshot.d.ts +16 -0
- package/dist/domain/runtime-snapshot.js +46 -0
- package/dist/ports/adb.d.ts +42 -0
- package/dist/ports/adb.js +1 -0
- package/dist/ports/android-cli.d.ts +21 -0
- package/dist/ports/android-cli.js +1 -0
- package/dist/ports/artifact-store.d.ts +12 -0
- package/dist/ports/artifact-store.js +1 -0
- package/dist/ports/clock.d.ts +4 -0
- package/dist/ports/clock.js +1 -0
- package/dist/ports/generation-meta-writer.d.ts +10 -0
- package/dist/ports/generation-meta-writer.js +1 -0
- package/dist/ports/generation-prompt.d.ts +19 -0
- package/dist/ports/generation-prompt.js +6 -0
- package/dist/ports/generation-session-store.d.ts +32 -0
- package/dist/ports/generation-session-store.js +26 -0
- package/dist/ports/init-prompt.d.ts +8 -0
- package/dist/ports/init-prompt.js +6 -0
- package/dist/ports/journey-writer.d.ts +10 -0
- package/dist/ports/journey-writer.js +1 -0
- package/dist/ports/process-runner.d.ts +32 -0
- package/dist/ports/process-runner.js +1 -0
- package/dist/ports/project-bound-file.d.ts +8 -0
- package/dist/ports/project-bound-file.js +1 -0
- package/dist/ports/project-file-inspector.d.ts +33 -0
- package/dist/ports/project-file-inspector.js +1 -0
- package/dist/ports/recorder-prompt.d.ts +29 -0
- package/dist/ports/recorder-prompt.js +1 -0
- package/dist/ports/skill-installer.d.ts +8 -0
- package/dist/ports/skill-installer.js +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
function consumeLines(buffer, chunk) {
|
|
4
|
+
const content = buffer.pending + chunk;
|
|
5
|
+
const lines = content.split(/\r?\n/);
|
|
6
|
+
buffer.pending = lines.pop() ?? "";
|
|
7
|
+
for (const line of lines) {
|
|
8
|
+
buffer.emit?.(line);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function flushLine(buffer) {
|
|
12
|
+
if (buffer.pending.length > 0) {
|
|
13
|
+
buffer.emit?.(buffer.pending);
|
|
14
|
+
buffer.pending = "";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class NodeProcessRunner {
|
|
18
|
+
defaultTimeoutMs;
|
|
19
|
+
streamStartupGraceMs;
|
|
20
|
+
constructor(defaultTimeoutMs = 15 * 60 * 1000, streamStartupGraceMs = 250) {
|
|
21
|
+
this.defaultTimeoutMs = defaultTimeoutMs;
|
|
22
|
+
this.streamStartupGraceMs = streamStartupGraceMs;
|
|
23
|
+
}
|
|
24
|
+
run(spec) {
|
|
25
|
+
return this.start({
|
|
26
|
+
...spec,
|
|
27
|
+
timeoutMs: spec.timeoutMs ?? this.defaultTimeoutMs
|
|
28
|
+
}).completion;
|
|
29
|
+
}
|
|
30
|
+
start(spec, handlers = {}) {
|
|
31
|
+
const startedAt = performance.now();
|
|
32
|
+
let stdout = "";
|
|
33
|
+
let stderr = "";
|
|
34
|
+
let timedOut = false;
|
|
35
|
+
let cancelled = false;
|
|
36
|
+
let spawnError;
|
|
37
|
+
let settled = false;
|
|
38
|
+
let timeout;
|
|
39
|
+
let forceKillTimeout;
|
|
40
|
+
let startupTimeout;
|
|
41
|
+
let startupSettled = false;
|
|
42
|
+
let resolveStarted = () => undefined;
|
|
43
|
+
const started = new Promise((resolve) => {
|
|
44
|
+
resolveStarted = resolve;
|
|
45
|
+
});
|
|
46
|
+
const settleStarted = (result) => {
|
|
47
|
+
if (startupSettled) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
startupSettled = true;
|
|
51
|
+
if (startupTimeout !== undefined) {
|
|
52
|
+
clearTimeout(startupTimeout);
|
|
53
|
+
}
|
|
54
|
+
resolveStarted(result);
|
|
55
|
+
};
|
|
56
|
+
const stdoutLines = {
|
|
57
|
+
pending: "",
|
|
58
|
+
emit: handlers.onStdoutLine === undefined
|
|
59
|
+
? undefined
|
|
60
|
+
: (line) => handlers.onStdoutLine?.(line)
|
|
61
|
+
};
|
|
62
|
+
const stderrLines = {
|
|
63
|
+
pending: "",
|
|
64
|
+
emit: handlers.onStderrLine === undefined
|
|
65
|
+
? undefined
|
|
66
|
+
: (line) => handlers.onStderrLine?.(line)
|
|
67
|
+
};
|
|
68
|
+
const child = spawn(spec.executable, [...spec.args], {
|
|
69
|
+
cwd: spec.cwd,
|
|
70
|
+
env: {
|
|
71
|
+
...process.env,
|
|
72
|
+
...spec.env
|
|
73
|
+
},
|
|
74
|
+
shell: false,
|
|
75
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
76
|
+
});
|
|
77
|
+
child.stdout.setEncoding("utf8");
|
|
78
|
+
child.stderr.setEncoding("utf8");
|
|
79
|
+
child.stdout.on("data", (chunk) => {
|
|
80
|
+
stdout += chunk;
|
|
81
|
+
consumeLines(stdoutLines, chunk);
|
|
82
|
+
});
|
|
83
|
+
child.stderr.on("data", (chunk) => {
|
|
84
|
+
stderr += chunk;
|
|
85
|
+
consumeLines(stderrLines, chunk);
|
|
86
|
+
});
|
|
87
|
+
child.on("spawn", () => {
|
|
88
|
+
startupTimeout = setTimeout(() => {
|
|
89
|
+
settleStarted(undefined);
|
|
90
|
+
}, this.streamStartupGraceMs);
|
|
91
|
+
startupTimeout.unref();
|
|
92
|
+
});
|
|
93
|
+
child.on("error", (error) => {
|
|
94
|
+
spawnError = error.message;
|
|
95
|
+
});
|
|
96
|
+
const terminate = () => {
|
|
97
|
+
if (settled || child.killed) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
child.kill("SIGTERM");
|
|
101
|
+
forceKillTimeout = setTimeout(() => {
|
|
102
|
+
if (!settled) {
|
|
103
|
+
child.kill("SIGKILL");
|
|
104
|
+
}
|
|
105
|
+
}, 1000);
|
|
106
|
+
forceKillTimeout.unref();
|
|
107
|
+
};
|
|
108
|
+
const abort = () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
terminate();
|
|
111
|
+
};
|
|
112
|
+
spec.signal?.addEventListener("abort", abort, { once: true });
|
|
113
|
+
if (spec.timeoutMs !== undefined) {
|
|
114
|
+
timeout = setTimeout(() => {
|
|
115
|
+
timedOut = true;
|
|
116
|
+
terminate();
|
|
117
|
+
}, spec.timeoutMs);
|
|
118
|
+
timeout.unref();
|
|
119
|
+
}
|
|
120
|
+
const completion = new Promise((resolve) => {
|
|
121
|
+
child.on("close", (exitCode, signal) => {
|
|
122
|
+
settled = true;
|
|
123
|
+
if (timeout !== undefined) {
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
}
|
|
126
|
+
if (forceKillTimeout !== undefined) {
|
|
127
|
+
clearTimeout(forceKillTimeout);
|
|
128
|
+
}
|
|
129
|
+
spec.signal?.removeEventListener("abort", abort);
|
|
130
|
+
flushLine(stdoutLines);
|
|
131
|
+
flushLine(stderrLines);
|
|
132
|
+
const result = {
|
|
133
|
+
exitCode,
|
|
134
|
+
signal,
|
|
135
|
+
stdout,
|
|
136
|
+
stderr,
|
|
137
|
+
durationMs: performance.now() - startedAt,
|
|
138
|
+
timedOut,
|
|
139
|
+
cancelled,
|
|
140
|
+
...(spawnError === undefined ? {} : { spawnError })
|
|
141
|
+
};
|
|
142
|
+
settleStarted(result);
|
|
143
|
+
resolve(result);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
const stop = () => {
|
|
147
|
+
terminate();
|
|
148
|
+
return completion;
|
|
149
|
+
};
|
|
150
|
+
if (spec.signal?.aborted === true) {
|
|
151
|
+
abort();
|
|
152
|
+
}
|
|
153
|
+
return { started, completion, stop };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { confirm } from "@inquirer/prompts";
|
|
2
|
+
import type { PendingConfirmation } from "../../domain/generation.js";
|
|
3
|
+
import { type ProposedStep } from "../../domain/proposed-step.js";
|
|
4
|
+
import type { GenerationPromptPort, ManualProposalInput } from "../../ports/generation-prompt.js";
|
|
5
|
+
interface SelectConfig {
|
|
6
|
+
message: string;
|
|
7
|
+
choices: readonly {
|
|
8
|
+
name: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}[];
|
|
11
|
+
}
|
|
12
|
+
interface InputConfig {
|
|
13
|
+
message: string;
|
|
14
|
+
validate?: ((value: string) => boolean | string) | undefined;
|
|
15
|
+
}
|
|
16
|
+
interface ConfirmConfig {
|
|
17
|
+
message: string;
|
|
18
|
+
default: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface NumberConfig {
|
|
21
|
+
message: string;
|
|
22
|
+
default: number;
|
|
23
|
+
min?: number | undefined;
|
|
24
|
+
max?: number | undefined;
|
|
25
|
+
}
|
|
26
|
+
export type GenerationPromptContext = NonNullable<Parameters<typeof confirm>[1]>;
|
|
27
|
+
export interface GenerationPromptFunctions {
|
|
28
|
+
select: (config: SelectConfig, context: GenerationPromptContext) => Promise<unknown>;
|
|
29
|
+
input: (config: InputConfig, context: GenerationPromptContext) => Promise<unknown>;
|
|
30
|
+
confirm: (config: ConfirmConfig, context: GenerationPromptContext) => Promise<unknown>;
|
|
31
|
+
number: (config: NumberConfig, context: GenerationPromptContext) => Promise<unknown>;
|
|
32
|
+
}
|
|
33
|
+
export type GenerationInputStream = NodeJS.ReadableStream & {
|
|
34
|
+
isTTY?: boolean | undefined;
|
|
35
|
+
};
|
|
36
|
+
export type GenerationDiagnosticStream = NodeJS.WritableStream & {
|
|
37
|
+
isTTY?: boolean | undefined;
|
|
38
|
+
};
|
|
39
|
+
export declare class InquirerGenerationPrompt implements GenerationPromptPort {
|
|
40
|
+
private readonly prompts;
|
|
41
|
+
private readonly promptInput;
|
|
42
|
+
private readonly diagnostics;
|
|
43
|
+
constructor(prompts?: GenerationPromptFunctions, promptInput?: GenerationInputStream, diagnostics?: GenerationDiagnosticStream);
|
|
44
|
+
confirm(challenge: PendingConfirmation, signal?: AbortSignal): Promise<boolean>;
|
|
45
|
+
buildManualProposal(input: ManualProposalInput, signal?: AbortSignal): Promise<ProposedStep>;
|
|
46
|
+
private buildScrollTo;
|
|
47
|
+
private selectTarget;
|
|
48
|
+
private selectDirection;
|
|
49
|
+
private swipeOptions;
|
|
50
|
+
private assertLocalTty;
|
|
51
|
+
private ask;
|
|
52
|
+
private throwIfCancelled;
|
|
53
|
+
private context;
|
|
54
|
+
}
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { confirm, input, number as numberPrompt, select } from "@inquirer/prompts";
|
|
2
|
+
import { listLocatableTargets, listRecorderTargets } from "../../application/recorder/locator-selector.js";
|
|
3
|
+
import { ProposedStepSchema } from "../../domain/proposed-step.js";
|
|
4
|
+
import { GenerationPromptCancelledError } from "../../ports/generation-prompt.js";
|
|
5
|
+
const defaultPrompts = {
|
|
6
|
+
select: async (config, context) => select({
|
|
7
|
+
message: config.message,
|
|
8
|
+
choices: [...config.choices]
|
|
9
|
+
}, context),
|
|
10
|
+
input: async (config, context) => input({
|
|
11
|
+
message: config.message,
|
|
12
|
+
...(config.validate === undefined ? {} : { validate: config.validate })
|
|
13
|
+
}, context),
|
|
14
|
+
confirm: async (config, context) => confirm(config, context),
|
|
15
|
+
number: async (config, context) => numberPrompt(config, context)
|
|
16
|
+
};
|
|
17
|
+
function selectedString(value, allowed) {
|
|
18
|
+
if (typeof value !== "string"
|
|
19
|
+
|| (allowed !== undefined && !allowed.includes(value))) {
|
|
20
|
+
throw new Error("Prompt returned an invalid selection");
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function selectedNumber(value, minimum, maximum = Number.POSITIVE_INFINITY) {
|
|
25
|
+
if (typeof value !== "number"
|
|
26
|
+
|| !Number.isFinite(value)
|
|
27
|
+
|| value < minimum
|
|
28
|
+
|| value > maximum) {
|
|
29
|
+
throw new Error("Prompt returned an invalid number");
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function common(input) {
|
|
34
|
+
return {
|
|
35
|
+
binding: input.binding,
|
|
36
|
+
activity: { before: input.before },
|
|
37
|
+
...(input.expect === undefined ? {} : { expect: input.expect })
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function requireTargets(targets) {
|
|
41
|
+
if (targets.length === 0) {
|
|
42
|
+
throw new Error("No deterministic Layout targets are available");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export class InquirerGenerationPrompt {
|
|
46
|
+
prompts;
|
|
47
|
+
promptInput;
|
|
48
|
+
diagnostics;
|
|
49
|
+
constructor(prompts = defaultPrompts, promptInput = process.stdin, diagnostics = process.stderr) {
|
|
50
|
+
this.prompts = prompts;
|
|
51
|
+
this.promptInput = promptInput;
|
|
52
|
+
this.diagnostics = diagnostics;
|
|
53
|
+
}
|
|
54
|
+
async confirm(challenge, signal) {
|
|
55
|
+
this.throwIfCancelled(signal);
|
|
56
|
+
this.assertLocalTty("Generation confirmation");
|
|
57
|
+
if (challenge.status !== "pending") {
|
|
58
|
+
throw new Error("Generation confirmation challenge is not pending");
|
|
59
|
+
}
|
|
60
|
+
this.diagnostics.write(`TapHound confirmation: ${challenge.actionSummary}\n`);
|
|
61
|
+
const answer = await this.ask((context) => this.prompts.confirm({
|
|
62
|
+
message: `Approve action at step ${String(challenge.stepIndex)}?`,
|
|
63
|
+
default: false
|
|
64
|
+
}, context), signal);
|
|
65
|
+
if (typeof answer !== "boolean") {
|
|
66
|
+
throw new Error("Prompt returned an invalid confirmation");
|
|
67
|
+
}
|
|
68
|
+
return answer;
|
|
69
|
+
}
|
|
70
|
+
async buildManualProposal(input, signal) {
|
|
71
|
+
this.throwIfCancelled(signal);
|
|
72
|
+
this.assertLocalTty("Manual generation takeover");
|
|
73
|
+
const shared = common(input);
|
|
74
|
+
if (input.action === "back" || input.action === "wait") {
|
|
75
|
+
return ProposedStepSchema.parse({ action: input.action, ...shared });
|
|
76
|
+
}
|
|
77
|
+
if (input.action === "inputText") {
|
|
78
|
+
const text = selectedString(await this.ask((context) => this.prompts.input({
|
|
79
|
+
message: "Text to enter",
|
|
80
|
+
validate: (answer) => answer.length > 0 || "Text must not be empty"
|
|
81
|
+
}, context), signal));
|
|
82
|
+
return ProposedStepSchema.parse({ action: input.action, text, ...shared });
|
|
83
|
+
}
|
|
84
|
+
if (input.action === "scrollTo") {
|
|
85
|
+
return this.buildScrollTo(input, shared, signal);
|
|
86
|
+
}
|
|
87
|
+
const targets = listRecorderTargets(input.layout, input.action);
|
|
88
|
+
const target = await this.selectTarget(targets, "Choose a deterministic Layout target", signal);
|
|
89
|
+
if (input.action === "click") {
|
|
90
|
+
return ProposedStepSchema.parse({
|
|
91
|
+
action: input.action,
|
|
92
|
+
locator: target.locator,
|
|
93
|
+
...shared
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (input.action === "longClick") {
|
|
97
|
+
const durationMs = selectedNumber(await this.ask((context) => this.prompts.number({
|
|
98
|
+
message: "Long-click duration (ms)",
|
|
99
|
+
default: 800,
|
|
100
|
+
min: 1
|
|
101
|
+
}, context), signal), 1);
|
|
102
|
+
return ProposedStepSchema.parse({
|
|
103
|
+
action: input.action,
|
|
104
|
+
locator: target.locator,
|
|
105
|
+
durationMs,
|
|
106
|
+
...shared
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const direction = await this.selectDirection(signal);
|
|
110
|
+
const options = await this.swipeOptions(signal);
|
|
111
|
+
return ProposedStepSchema.parse({
|
|
112
|
+
action: input.action,
|
|
113
|
+
locator: target.locator,
|
|
114
|
+
direction,
|
|
115
|
+
...options,
|
|
116
|
+
...shared
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async buildScrollTo(input, shared, signal) {
|
|
120
|
+
const container = await this.selectTarget(listRecorderTargets(input.layout, "swipe"), "Choose the scrollable container", signal);
|
|
121
|
+
const target = await this.selectTarget(listLocatableTargets(input.layout), "Choose the scroll target", signal);
|
|
122
|
+
const direction = await this.selectDirection(signal);
|
|
123
|
+
const options = await this.swipeOptions(signal);
|
|
124
|
+
const maxSwipes = selectedNumber(await this.ask((context) => this.prompts.number({
|
|
125
|
+
message: "Maximum scroll swipes",
|
|
126
|
+
default: 20,
|
|
127
|
+
min: 1,
|
|
128
|
+
max: 30
|
|
129
|
+
}, context), signal), 1, 30);
|
|
130
|
+
return ProposedStepSchema.parse({
|
|
131
|
+
action: "scrollTo",
|
|
132
|
+
locator: target.locator,
|
|
133
|
+
container: container.locator,
|
|
134
|
+
direction,
|
|
135
|
+
maxSwipes,
|
|
136
|
+
...options,
|
|
137
|
+
...shared
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async selectTarget(targets, message, signal) {
|
|
141
|
+
requireTargets(targets);
|
|
142
|
+
const id = selectedString(await this.ask((context) => this.prompts.select({
|
|
143
|
+
message,
|
|
144
|
+
choices: targets.map((target) => ({
|
|
145
|
+
name: target.label,
|
|
146
|
+
value: target.element.id
|
|
147
|
+
}))
|
|
148
|
+
}, context), signal), targets.map((target) => target.element.id));
|
|
149
|
+
const target = targets.find((candidate) => candidate.element.id === id);
|
|
150
|
+
if (target === undefined) {
|
|
151
|
+
throw new Error("Prompt returned an invalid selection");
|
|
152
|
+
}
|
|
153
|
+
return target;
|
|
154
|
+
}
|
|
155
|
+
async selectDirection(signal) {
|
|
156
|
+
const directions = ["up", "down", "left", "right"];
|
|
157
|
+
const value = await this.ask((context) => this.prompts.select({
|
|
158
|
+
message: "Swipe direction",
|
|
159
|
+
choices: directions.map((direction) => ({
|
|
160
|
+
name: direction,
|
|
161
|
+
value: direction
|
|
162
|
+
}))
|
|
163
|
+
}, context), signal);
|
|
164
|
+
return selectedString(value, directions);
|
|
165
|
+
}
|
|
166
|
+
async swipeOptions(signal) {
|
|
167
|
+
const distancePercent = selectedNumber(await this.ask((context) => this.prompts.number({
|
|
168
|
+
message: "Swipe distance (0–1)",
|
|
169
|
+
default: 0.6,
|
|
170
|
+
min: 0.01,
|
|
171
|
+
max: 1
|
|
172
|
+
}, context), signal), 0.01, 1);
|
|
173
|
+
const durationMs = selectedNumber(await this.ask((context) => this.prompts.number({
|
|
174
|
+
message: "Swipe duration (ms)",
|
|
175
|
+
default: 300,
|
|
176
|
+
min: 1
|
|
177
|
+
}, context), signal), 1);
|
|
178
|
+
return { distancePercent, durationMs };
|
|
179
|
+
}
|
|
180
|
+
assertLocalTty(operation) {
|
|
181
|
+
if (this.promptInput.isTTY !== true
|
|
182
|
+
|| this.diagnostics.isTTY !== true) {
|
|
183
|
+
throw new Error(`${operation} requires local TTY input and diagnostics`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async ask(operation, signal) {
|
|
187
|
+
this.throwIfCancelled(signal);
|
|
188
|
+
try {
|
|
189
|
+
return await operation(this.context(signal));
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (signal?.aborted === true
|
|
193
|
+
|| (error instanceof Error
|
|
194
|
+
&& (error.name === "AbortPromptError"
|
|
195
|
+
|| error.name === "ExitPromptError"))) {
|
|
196
|
+
throw new GenerationPromptCancelledError();
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
throwIfCancelled(signal) {
|
|
202
|
+
if (signal?.aborted === true) {
|
|
203
|
+
throw new GenerationPromptCancelledError();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
context(signal) {
|
|
207
|
+
return {
|
|
208
|
+
input: this.promptInput,
|
|
209
|
+
output: this.diagnostics,
|
|
210
|
+
...(signal === undefined ? {} : { signal })
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { checkbox } from "@inquirer/prompts";
|
|
2
|
+
import { type AgentId } from "../../domain/init.js";
|
|
3
|
+
import type { InitPromptPort } from "../../ports/init-prompt.js";
|
|
4
|
+
export type InitInputStream = NodeJS.ReadableStream & {
|
|
5
|
+
isTTY?: boolean | undefined;
|
|
6
|
+
};
|
|
7
|
+
export type InitDiagnosticStream = NodeJS.WritableStream & {
|
|
8
|
+
isTTY?: boolean | undefined;
|
|
9
|
+
};
|
|
10
|
+
export type InitPromptContext = NonNullable<Parameters<typeof checkbox>[1]>;
|
|
11
|
+
export interface CheckboxConfig {
|
|
12
|
+
message: string;
|
|
13
|
+
choices: readonly {
|
|
14
|
+
name: string;
|
|
15
|
+
value: string;
|
|
16
|
+
checked?: boolean;
|
|
17
|
+
}[];
|
|
18
|
+
validate?: ((choices: readonly {
|
|
19
|
+
value: string;
|
|
20
|
+
}[]) => boolean | string) | undefined;
|
|
21
|
+
}
|
|
22
|
+
export interface InitPromptFunctions {
|
|
23
|
+
checkbox: (config: CheckboxConfig, context: InitPromptContext) => Promise<string[]>;
|
|
24
|
+
}
|
|
25
|
+
export declare class InquirerInitPrompt implements InitPromptPort {
|
|
26
|
+
private readonly prompts;
|
|
27
|
+
private readonly promptInput;
|
|
28
|
+
private readonly diagnostics;
|
|
29
|
+
constructor(prompts?: InitPromptFunctions, promptInput?: InitInputStream, diagnostics?: InitDiagnosticStream);
|
|
30
|
+
selectAgents(): Promise<AgentId[]>;
|
|
31
|
+
private assertLocalTty;
|
|
32
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { checkbox } from "@inquirer/prompts";
|
|
2
|
+
import { AGENT_REGISTRY } from "../../domain/init.js";
|
|
3
|
+
import { InitPromptCancelledError } from "../../ports/init-prompt.js";
|
|
4
|
+
const defaultPrompts = {
|
|
5
|
+
checkbox: async (config, context) => checkbox({
|
|
6
|
+
message: config.message,
|
|
7
|
+
choices: [...config.choices],
|
|
8
|
+
...(config.validate === undefined
|
|
9
|
+
? {}
|
|
10
|
+
: { validate: config.validate })
|
|
11
|
+
}, context)
|
|
12
|
+
};
|
|
13
|
+
export class InquirerInitPrompt {
|
|
14
|
+
prompts;
|
|
15
|
+
promptInput;
|
|
16
|
+
diagnostics;
|
|
17
|
+
constructor(prompts = defaultPrompts, promptInput = process.stdin, diagnostics = process.stderr) {
|
|
18
|
+
this.prompts = prompts;
|
|
19
|
+
this.promptInput = promptInput;
|
|
20
|
+
this.diagnostics = diagnostics;
|
|
21
|
+
}
|
|
22
|
+
async selectAgents() {
|
|
23
|
+
this.assertLocalTty();
|
|
24
|
+
try {
|
|
25
|
+
const selected = await this.prompts.checkbox({
|
|
26
|
+
message: "Select AI agents to install the TapHound Skill for:",
|
|
27
|
+
choices: AGENT_REGISTRY.map((agent) => ({
|
|
28
|
+
name: agent.label,
|
|
29
|
+
value: agent.id
|
|
30
|
+
})),
|
|
31
|
+
validate: (choices) => (choices.length > 0
|
|
32
|
+
? true
|
|
33
|
+
: "Select at least one agent")
|
|
34
|
+
}, {
|
|
35
|
+
input: this.promptInput,
|
|
36
|
+
output: this.diagnostics
|
|
37
|
+
});
|
|
38
|
+
return selected;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof Error
|
|
42
|
+
&& (error.name === "AbortPromptError"
|
|
43
|
+
|| error.name === "ExitPromptError")) {
|
|
44
|
+
throw new InitPromptCancelledError();
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
assertLocalTty() {
|
|
50
|
+
if (this.promptInput.isTTY !== true
|
|
51
|
+
|| this.diagnostics.isTTY !== true) {
|
|
52
|
+
throw new Error("Agent selection requires local TTY input and output");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { RecorderAction, RecorderPromptPort, RecorderTargetChoice, ScrollDecision, SwipeOptions } from "../../ports/recorder-prompt.js";
|
|
2
|
+
interface SelectConfig {
|
|
3
|
+
message: string;
|
|
4
|
+
choices: readonly {
|
|
5
|
+
name: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}[];
|
|
8
|
+
}
|
|
9
|
+
interface InputConfig {
|
|
10
|
+
message: string;
|
|
11
|
+
validate?: ((value: string) => boolean | string) | undefined;
|
|
12
|
+
}
|
|
13
|
+
interface ConfirmConfig {
|
|
14
|
+
message: string;
|
|
15
|
+
default: boolean;
|
|
16
|
+
}
|
|
17
|
+
interface NumberConfig {
|
|
18
|
+
message: string;
|
|
19
|
+
default: number;
|
|
20
|
+
min?: number | undefined;
|
|
21
|
+
max?: number | undefined;
|
|
22
|
+
}
|
|
23
|
+
export interface PromptFunctions {
|
|
24
|
+
select: (config: SelectConfig) => Promise<unknown>;
|
|
25
|
+
input: (config: InputConfig) => Promise<unknown>;
|
|
26
|
+
confirm: (config: ConfirmConfig) => Promise<unknown>;
|
|
27
|
+
number: (config: NumberConfig) => Promise<unknown>;
|
|
28
|
+
}
|
|
29
|
+
export interface DiagnosticStream {
|
|
30
|
+
write: (content: string) => unknown;
|
|
31
|
+
}
|
|
32
|
+
export declare class InquirerRecorderPrompt implements RecorderPromptPort {
|
|
33
|
+
private readonly prompts;
|
|
34
|
+
private readonly diagnostics;
|
|
35
|
+
constructor(prompts?: PromptFunctions, diagnostics?: DiagnosticStream);
|
|
36
|
+
selectAction(): Promise<RecorderAction>;
|
|
37
|
+
selectTarget(choices: readonly RecorderTargetChoice[]): Promise<string>;
|
|
38
|
+
inputText(): Promise<string>;
|
|
39
|
+
selectSwipeDirection(): Promise<"up" | "down" | "left" | "right">;
|
|
40
|
+
longClickDuration(): Promise<number>;
|
|
41
|
+
swipeOptions(): Promise<SwipeOptions>;
|
|
42
|
+
selectFallbackLabel(annotatedScreenshotPath: string): Promise<string | undefined>;
|
|
43
|
+
notifyFailure(message: string): Promise<void>;
|
|
44
|
+
selectScrollContainer(choices: readonly RecorderTargetChoice[]): Promise<string>;
|
|
45
|
+
scrollTargetDecision(choices: readonly RecorderTargetChoice[]): Promise<ScrollDecision>;
|
|
46
|
+
}
|
|
47
|
+
export {};
|