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.
Files changed (176) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +200 -0
  3. package/README.zh-CN.md +200 -0
  4. package/assets/brand/taphound-mark.svg +9 -0
  5. package/assets/skills/taphound-ai-journey/GUIDE.md +862 -0
  6. package/assets/skills/taphound-ai-journey/SKILL.md +377 -0
  7. package/assets/skills/taphound-ai-journey/prompts/analyze-project.md +526 -0
  8. package/assets/skills/taphound-ai-journey/prompts/check-completion.md +52 -0
  9. package/assets/skills/taphound-ai-journey/prompts/generate-step.md +109 -0
  10. package/assets/skills/taphound-ai-journey/schemas/observe-output.json +42 -0
  11. package/assets/skills/taphound-ai-journey/schemas/project-context.json +81 -0
  12. package/assets/skills/taphound-ai-journey/schemas/proposed-step-envelope.json +267 -0
  13. package/assets/skills/taphound-ai-journey/templates/project-context.example.json +49 -0
  14. package/dist/adapters/adb/adb-adapter.d.ts +23 -0
  15. package/dist/adapters/adb/adb-adapter.js +218 -0
  16. package/dist/adapters/android-cli/android-cli-adapter.d.ts +11 -0
  17. package/dist/adapters/android-cli/android-cli-adapter.js +64 -0
  18. package/dist/adapters/android-cli/layout-parser.d.ts +3 -0
  19. package/dist/adapters/android-cli/layout-parser.js +174 -0
  20. package/dist/adapters/clock/system-clock.d.ts +5 -0
  21. package/dist/adapters/clock/system-clock.js +13 -0
  22. package/dist/adapters/filesystem/artifact-store.d.ts +4 -0
  23. package/dist/adapters/filesystem/artifact-store.js +73 -0
  24. package/dist/adapters/filesystem/generation-meta-writer.d.ts +16 -0
  25. package/dist/adapters/filesystem/generation-meta-writer.js +34 -0
  26. package/dist/adapters/filesystem/generation-session-store.d.ts +68 -0
  27. package/dist/adapters/filesystem/generation-session-store.js +1603 -0
  28. package/dist/adapters/filesystem/journey-writer.d.ts +16 -0
  29. package/dist/adapters/filesystem/journey-writer.js +34 -0
  30. package/dist/adapters/filesystem/project-bound-file.d.ts +6 -0
  31. package/dist/adapters/filesystem/project-bound-file.js +240 -0
  32. package/dist/adapters/filesystem/project-file-inspector.d.ts +8 -0
  33. package/dist/adapters/filesystem/project-file-inspector.js +198 -0
  34. package/dist/adapters/filesystem/skill-installer.d.ts +10 -0
  35. package/dist/adapters/filesystem/skill-installer.js +39 -0
  36. package/dist/adapters/process/node-process-runner.d.ts +8 -0
  37. package/dist/adapters/process/node-process-runner.js +155 -0
  38. package/dist/adapters/prompt/inquirer-generation-prompt.d.ts +55 -0
  39. package/dist/adapters/prompt/inquirer-generation-prompt.js +213 -0
  40. package/dist/adapters/prompt/inquirer-init-prompt.d.ts +32 -0
  41. package/dist/adapters/prompt/inquirer-init-prompt.js +55 -0
  42. package/dist/adapters/prompt/inquirer-recorder-prompt.d.ts +47 -0
  43. package/dist/adapters/prompt/inquirer-recorder-prompt.js +157 -0
  44. package/dist/application/assertion/expectation-evaluator.d.ts +61 -0
  45. package/dist/application/assertion/expectation-evaluator.js +207 -0
  46. package/dist/application/collector/logcat-collector.d.ts +39 -0
  47. package/dist/application/collector/logcat-collector.js +107 -0
  48. package/dist/application/context/context-validator.d.ts +27 -0
  49. package/dist/application/context/context-validator.js +120 -0
  50. package/dist/application/doctor/doctor-service.d.ts +35 -0
  51. package/dist/application/doctor/doctor-service.js +178 -0
  52. package/dist/application/generation/focused-input.d.ts +2 -0
  53. package/dist/application/generation/focused-input.js +9 -0
  54. package/dist/application/generation/generation-confirmation-service.d.ts +76 -0
  55. package/dist/application/generation/generation-confirmation-service.js +383 -0
  56. package/dist/application/generation/generation-finalizer.d.ts +67 -0
  57. package/dist/application/generation/generation-finalizer.js +646 -0
  58. package/dist/application/generation/generation-publisher.d.ts +46 -0
  59. package/dist/application/generation/generation-publisher.js +178 -0
  60. package/dist/application/generation/generation-starter.d.ts +31 -0
  61. package/dist/application/generation/generation-starter.js +102 -0
  62. package/dist/application/generation/generation-step-executor.d.ts +52 -0
  63. package/dist/application/generation/generation-step-executor.js +668 -0
  64. package/dist/application/generation/proposed-step-validator.d.ts +11 -0
  65. package/dist/application/generation/proposed-step-validator.js +88 -0
  66. package/dist/application/generation/risk-evaluator.d.ts +9 -0
  67. package/dist/application/generation/risk-evaluator.js +14 -0
  68. package/dist/application/generation/runtime-observer.d.ts +39 -0
  69. package/dist/application/generation/runtime-observer.js +157 -0
  70. package/dist/application/init/init-service.d.ts +17 -0
  71. package/dist/application/init/init-service.js +44 -0
  72. package/dist/application/interaction/action-executor.d.ts +25 -0
  73. package/dist/application/interaction/action-executor.js +103 -0
  74. package/dist/application/interaction/fallback-resolver.d.ts +24 -0
  75. package/dist/application/interaction/fallback-resolver.js +48 -0
  76. package/dist/application/interaction/scroll-to-executor.d.ts +40 -0
  77. package/dist/application/interaction/scroll-to-executor.js +191 -0
  78. package/dist/application/locator/locator-resolver.d.ts +20 -0
  79. package/dist/application/locator/locator-resolver.js +89 -0
  80. package/dist/application/project/project-describer.d.ts +14 -0
  81. package/dist/application/project/project-describer.js +8 -0
  82. package/dist/application/recorder/locator-selector.d.ts +10 -0
  83. package/dist/application/recorder/locator-selector.js +80 -0
  84. package/dist/application/recorder/recorder-service.d.ts +41 -0
  85. package/dist/application/recorder/recorder-service.js +331 -0
  86. package/dist/application/report/report-writer.d.ts +10 -0
  87. package/dist/application/report/report-writer.js +39 -0
  88. package/dist/application/runtime/activity-waiter.d.ts +33 -0
  89. package/dist/application/runtime/activity-waiter.js +73 -0
  90. package/dist/application/runtime/launch-failure.d.ts +2 -0
  91. package/dist/application/runtime/launch-failure.js +20 -0
  92. package/dist/application/runtime/process-waiter.d.ts +27 -0
  93. package/dist/application/runtime/process-waiter.js +73 -0
  94. package/dist/application/runtime/step-runner.d.ts +50 -0
  95. package/dist/application/runtime/step-runner.js +429 -0
  96. package/dist/application/runtime/verify-runtime.d.ts +44 -0
  97. package/dist/application/runtime/verify-runtime.js +350 -0
  98. package/dist/application/wait/idle-waiter.d.ts +30 -0
  99. package/dist/application/wait/idle-waiter.js +90 -0
  100. package/dist/cli/commands/context.d.ts +3 -0
  101. package/dist/cli/commands/context.js +94 -0
  102. package/dist/cli/commands/doctor.d.ts +3 -0
  103. package/dist/cli/commands/doctor.js +51 -0
  104. package/dist/cli/commands/generation.d.ts +3 -0
  105. package/dist/cli/commands/generation.js +518 -0
  106. package/dist/cli/commands/init.d.ts +3 -0
  107. package/dist/cli/commands/init.js +104 -0
  108. package/dist/cli/commands/project.d.ts +3 -0
  109. package/dist/cli/commands/project.js +58 -0
  110. package/dist/cli/commands/record.d.ts +3 -0
  111. package/dist/cli/commands/record.js +88 -0
  112. package/dist/cli/commands/verify.d.ts +3 -0
  113. package/dist/cli/commands/verify.js +106 -0
  114. package/dist/cli/dependencies.d.ts +66 -0
  115. package/dist/cli/dependencies.js +204 -0
  116. package/dist/cli/main.d.ts +8 -0
  117. package/dist/cli/main.js +73 -0
  118. package/dist/cli/output.d.ts +16 -0
  119. package/dist/cli/output.js +22 -0
  120. package/dist/cli/program.d.ts +3 -0
  121. package/dist/cli/program.js +43 -0
  122. package/dist/domain/activity.d.ts +7 -0
  123. package/dist/domain/activity.js +65 -0
  124. package/dist/domain/app-process.d.ts +7 -0
  125. package/dist/domain/app-process.js +10 -0
  126. package/dist/domain/config.d.ts +15 -0
  127. package/dist/domain/config.js +17 -0
  128. package/dist/domain/failure.d.ts +4 -0
  129. package/dist/domain/failure.js +43 -0
  130. package/dist/domain/generation.d.ts +912 -0
  131. package/dist/domain/generation.js +342 -0
  132. package/dist/domain/init.d.ts +42 -0
  133. package/dist/domain/init.js +114 -0
  134. package/dist/domain/journey.d.ts +673 -0
  135. package/dist/domain/journey.js +109 -0
  136. package/dist/domain/layout.d.ts +35 -0
  137. package/dist/domain/layout.js +33 -0
  138. package/dist/domain/locator.d.ts +8 -0
  139. package/dist/domain/locator.js +5 -0
  140. package/dist/domain/project-context.d.ts +101 -0
  141. package/dist/domain/project-context.js +64 -0
  142. package/dist/domain/proposed-step.d.ts +341 -0
  143. package/dist/domain/proposed-step.js +87 -0
  144. package/dist/domain/report.d.ts +349 -0
  145. package/dist/domain/report.js +135 -0
  146. package/dist/domain/runtime-snapshot.d.ts +16 -0
  147. package/dist/domain/runtime-snapshot.js +46 -0
  148. package/dist/ports/adb.d.ts +42 -0
  149. package/dist/ports/adb.js +1 -0
  150. package/dist/ports/android-cli.d.ts +21 -0
  151. package/dist/ports/android-cli.js +1 -0
  152. package/dist/ports/artifact-store.d.ts +12 -0
  153. package/dist/ports/artifact-store.js +1 -0
  154. package/dist/ports/clock.d.ts +4 -0
  155. package/dist/ports/clock.js +1 -0
  156. package/dist/ports/generation-meta-writer.d.ts +10 -0
  157. package/dist/ports/generation-meta-writer.js +1 -0
  158. package/dist/ports/generation-prompt.d.ts +19 -0
  159. package/dist/ports/generation-prompt.js +6 -0
  160. package/dist/ports/generation-session-store.d.ts +32 -0
  161. package/dist/ports/generation-session-store.js +26 -0
  162. package/dist/ports/init-prompt.d.ts +8 -0
  163. package/dist/ports/init-prompt.js +6 -0
  164. package/dist/ports/journey-writer.d.ts +10 -0
  165. package/dist/ports/journey-writer.js +1 -0
  166. package/dist/ports/process-runner.d.ts +32 -0
  167. package/dist/ports/process-runner.js +1 -0
  168. package/dist/ports/project-bound-file.d.ts +8 -0
  169. package/dist/ports/project-bound-file.js +1 -0
  170. package/dist/ports/project-file-inspector.d.ts +33 -0
  171. package/dist/ports/project-file-inspector.js +1 -0
  172. package/dist/ports/recorder-prompt.d.ts +29 -0
  173. package/dist/ports/recorder-prompt.js +1 -0
  174. package/dist/ports/skill-installer.d.ts +8 -0
  175. package/dist/ports/skill-installer.js +1 -0
  176. 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 {};