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,3 @@
1
+ import { Command } from "commander";
2
+ import type { CliDependencies } from "../dependencies.js";
3
+ export declare function createRecordCommand(dependencies: CliDependencies): Command;
@@ -0,0 +1,88 @@
1
+ import { resolve } from "node:path";
2
+ import { Command } from "commander";
3
+ import { TapHoundConfigSchema } from "../../domain/config.js";
4
+ import { errorMessage, failureOutput, writeJson, writeLine } from "../output.js";
5
+ export function createRecordCommand(dependencies) {
6
+ return new Command("record")
7
+ .description("Interactively record a TapHound Journey")
8
+ .option("--project <path>", "Android project root", dependencies.cwd())
9
+ .option("--config <path>", "TapHound config path", "taphound.config.json")
10
+ .option("--device <serial>", "Select an online Android device")
11
+ .requiredOption("--name <name>", "Journey name")
12
+ .requiredOption("--output <path>", "Journey output path")
13
+ .option("--json", "Emit machine-readable JSON")
14
+ .action(async (options) => {
15
+ let config;
16
+ try {
17
+ config = TapHoundConfigSchema.parse(await dependencies.readJson(resolve(options.project, options.config)));
18
+ }
19
+ catch (error) {
20
+ const output = failureOutput(2, "CONFIG_INVALID", errorMessage(error));
21
+ if (options.json === true) {
22
+ writeJson(dependencies.stdout, output);
23
+ }
24
+ else {
25
+ writeLine(dependencies.stderr, output.failure.message);
26
+ }
27
+ dependencies.setExitCode(2);
28
+ return;
29
+ }
30
+ try {
31
+ const doctor = await dependencies.doctor.run({
32
+ packageName: config.run.packageName,
33
+ ...(options.device === undefined
34
+ ? {}
35
+ : { requestedDevice: options.device }),
36
+ ...(dependencies.signal === undefined
37
+ ? {}
38
+ : { signal: dependencies.signal })
39
+ });
40
+ if (doctor.status === "failed") {
41
+ const output = failureOutput(3, doctor.failureCode ?? "ENVIRONMENT_MISSING_TOOL", doctor.checks.find((check) => check.status === "failed")?.message
42
+ ?? "TapHound environment preflight failed");
43
+ if (options.json === true) {
44
+ writeJson(dependencies.stdout, output);
45
+ }
46
+ else {
47
+ writeLine(dependencies.stderr, output.failure.message);
48
+ }
49
+ dependencies.setExitCode(3);
50
+ return;
51
+ }
52
+ const deviceSerial = options.device ?? doctor.deviceSerial;
53
+ if (deviceSerial === undefined) {
54
+ throw new Error("Doctor did not select a device");
55
+ }
56
+ const result = await dependencies.recorder.record({
57
+ config,
58
+ projectRoot: options.project,
59
+ deviceSerial,
60
+ journeyName: options.name,
61
+ outputPath: resolve(options.project, options.output),
62
+ ...(dependencies.signal === undefined
63
+ ? {}
64
+ : { signal: dependencies.signal })
65
+ });
66
+ if (options.json === true) {
67
+ writeJson(dependencies.stdout, result);
68
+ }
69
+ else if (result.status === "completed") {
70
+ writeLine(dependencies.stdout, `Recorded ${String(result.stepsRecorded)} step${result.stepsRecorded === 1 ? "" : "s"}`);
71
+ }
72
+ else {
73
+ writeLine(dependencies.stderr, result.status === "failed" ? result.message : "Recording cancelled");
74
+ }
75
+ dependencies.setExitCode(result.status === "failed" ? 1 : 0);
76
+ }
77
+ catch (error) {
78
+ const output = failureOutput(4, "INTERNAL_ERROR", errorMessage(error));
79
+ if (options.json === true) {
80
+ writeJson(dependencies.stdout, output);
81
+ }
82
+ else {
83
+ writeLine(dependencies.stderr, output.failure.message);
84
+ }
85
+ dependencies.setExitCode(4);
86
+ }
87
+ });
88
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { CliDependencies } from "../dependencies.js";
3
+ export declare function createVerifyCommand(dependencies: CliDependencies): Command;
@@ -0,0 +1,106 @@
1
+ import { resolve } from "node:path";
2
+ import { Command } from "commander";
3
+ import { TapHoundConfigSchema } from "../../domain/config.js";
4
+ import { JourneySchema } from "../../domain/journey.js";
5
+ import { errorMessage, failureOutput, writeJson, writeLine } from "../output.js";
6
+ function toolVersions(checks) {
7
+ return Object.fromEntries(checks.flatMap((check) => (check.version === undefined
8
+ || !["node", "adb", "android"].includes(check.name)
9
+ ? []
10
+ : [[check.name, check.version]])));
11
+ }
12
+ export function createVerifyCommand(dependencies) {
13
+ return new Command("verify")
14
+ .description("Deterministically verify a TapHound Journey")
15
+ .option("--project <path>", "Android project root", dependencies.cwd())
16
+ .option("--config <path>", "TapHound config path", "taphound.config.json")
17
+ .requiredOption("--journey <path>", "TapHound Journey path")
18
+ .option("--device <serial>", "Select an online Android device")
19
+ .option("--package <name>", "Override run.packageName")
20
+ .option("--activity <name>", "Override run.activity")
21
+ .option("--reports <path>", "Override report output directory")
22
+ .option("--json", "Emit one machine-readable JSON value")
23
+ .action(async (options) => {
24
+ let config;
25
+ let journey;
26
+ try {
27
+ const rawConfig = await dependencies.readJson(resolve(options.project, options.config));
28
+ const parsed = TapHoundConfigSchema.parse(rawConfig);
29
+ config = TapHoundConfigSchema.parse({
30
+ ...parsed,
31
+ run: {
32
+ packageName: options.package ?? parsed.run.packageName,
33
+ activity: options.activity ?? parsed.run.activity
34
+ },
35
+ artifactsDir: options.reports ?? parsed.artifactsDir
36
+ });
37
+ journey = JourneySchema.parse(await dependencies.readJson(resolve(options.project, options.journey)));
38
+ }
39
+ catch (error) {
40
+ const output = failureOutput(2, "CONFIG_INVALID", errorMessage(error));
41
+ if (options.json === true) {
42
+ writeJson(dependencies.stdout, output);
43
+ }
44
+ else {
45
+ writeLine(dependencies.stderr, output.failure.message);
46
+ }
47
+ dependencies.setExitCode(2);
48
+ return;
49
+ }
50
+ try {
51
+ const doctor = await dependencies.doctor.run({
52
+ packageName: config.run.packageName,
53
+ ...(options.device === undefined
54
+ ? {}
55
+ : { requestedDevice: options.device }),
56
+ ...(dependencies.signal === undefined
57
+ ? {}
58
+ : { signal: dependencies.signal })
59
+ });
60
+ if (doctor.status === "failed") {
61
+ const output = failureOutput(3, doctor.failureCode ?? "ENVIRONMENT_MISSING_TOOL", doctor.checks.find((check) => check.status === "failed")?.message
62
+ ?? "TapHound environment preflight failed");
63
+ if (options.json === true) {
64
+ writeJson(dependencies.stdout, output);
65
+ }
66
+ else {
67
+ writeLine(dependencies.stderr, output.failure.message);
68
+ }
69
+ dependencies.setExitCode(3);
70
+ return;
71
+ }
72
+ const deviceSerial = options.device ?? doctor.deviceSerial;
73
+ if (deviceSerial === undefined) {
74
+ throw new Error("Doctor did not select a device");
75
+ }
76
+ writeLine(dependencies.stderr, `TapHound: verifying ${journey.name}`);
77
+ const result = await dependencies.verifier.verify({
78
+ config,
79
+ journey,
80
+ projectRoot: options.project,
81
+ deviceSerial,
82
+ toolVersions: toolVersions(doctor.checks),
83
+ ...(dependencies.signal === undefined
84
+ ? {}
85
+ : { signal: dependencies.signal })
86
+ });
87
+ if (options.json === true) {
88
+ writeJson(dependencies.stdout, result);
89
+ }
90
+ else {
91
+ writeLine(dependencies.stdout, `TapHound verify: ${result.status.toUpperCase()}\nReport: ${result.reportPath}`);
92
+ }
93
+ dependencies.setExitCode(result.exitCode);
94
+ }
95
+ catch (error) {
96
+ const output = failureOutput(4, "INTERNAL_ERROR", errorMessage(error));
97
+ if (options.json === true) {
98
+ writeJson(dependencies.stdout, output);
99
+ }
100
+ else {
101
+ writeLine(dependencies.stderr, output.failure.message);
102
+ }
103
+ dependencies.setExitCode(4);
104
+ }
105
+ });
106
+ }
@@ -0,0 +1,66 @@
1
+ import { ContextValidator } from "../application/context/context-validator.js";
2
+ import type { DoctorReport, DoctorRunInput } from "../application/doctor/doctor-service.js";
3
+ import { GenerationConfirmationService } from "../application/generation/generation-confirmation-service.js";
4
+ import { GenerationFinalizer } from "../application/generation/generation-finalizer.js";
5
+ import { GenerationStarter, type GenerationStartInput } from "../application/generation/generation-starter.js";
6
+ import { GenerationStepExecutor } from "../application/generation/generation-step-executor.js";
7
+ import { RuntimeObserver, type RuntimeObservation, type RuntimeObserveInput } from "../application/generation/runtime-observer.js";
8
+ import { type InitInput } from "../application/init/init-service.js";
9
+ import { ProjectDescriber } from "../application/project/project-describer.js";
10
+ import { type RecordInput, type RecordResult } from "../application/recorder/recorder-service.js";
11
+ import { type VerifyInput, type VerifyResult } from "../application/runtime/verify-runtime.js";
12
+ import type { TapHoundConfig } from "../domain/config.js";
13
+ import type { InitResult } from "../domain/init.js";
14
+ import type { GenerationSession } from "../domain/generation.js";
15
+ import type { InitPromptPort } from "../ports/init-prompt.js";
16
+ import type { GenerationSessionStore } from "../ports/generation-session-store.js";
17
+ export interface TextOutput {
18
+ write: (content: string) => void;
19
+ }
20
+ export interface GenerationCliRuntime {
21
+ confirmation: Pick<GenerationConfirmationService, "request" | "requestManual" | "confirmStored" | "findPendingManual">;
22
+ executor: Pick<GenerationStepExecutor, "execute">;
23
+ observer: Pick<RuntimeObserver, "observe">;
24
+ finalizer: Pick<GenerationFinalizer, "finalize">;
25
+ readSession: (id: string) => Promise<GenerationSession>;
26
+ assertConfigIdentity: (id: string) => Promise<void>;
27
+ }
28
+ export interface CliDependencies {
29
+ signal?: AbortSignal | undefined;
30
+ doctor: {
31
+ run: (input?: DoctorRunInput) => Promise<DoctorReport>;
32
+ };
33
+ recorder: {
34
+ record: (input: RecordInput) => Promise<RecordResult>;
35
+ };
36
+ verifier: {
37
+ verify: (input: VerifyInput) => Promise<VerifyResult>;
38
+ };
39
+ projectDescriber: Pick<ProjectDescriber, "describe">;
40
+ contextValidator: Pick<ContextValidator, "validate">;
41
+ generationStarter: {
42
+ start: (input: GenerationStartInput) => Promise<Awaited<ReturnType<GenerationStarter["start"]>>>;
43
+ };
44
+ runtimeObserver: {
45
+ observe: (input: RuntimeObserveInput & {
46
+ projectRoot: string;
47
+ }) => Promise<RuntimeObservation>;
48
+ };
49
+ generationRuntime?: (input: {
50
+ projectRoot: string;
51
+ config: TapHoundConfig;
52
+ }) => GenerationCliRuntime;
53
+ init: {
54
+ install: (input: InitInput) => Promise<InitResult>;
55
+ };
56
+ initPrompt: Pick<InitPromptPort, "selectAgents">;
57
+ readJson: (path: string) => Promise<unknown>;
58
+ cwd: () => string;
59
+ stdout: TextOutput;
60
+ stderr: TextOutput;
61
+ setExitCode: (code: number) => void;
62
+ }
63
+ export interface ProductionDependencyOptions {
64
+ generationStoreFactory?: (projectRoot: string) => GenerationSessionStore;
65
+ }
66
+ export declare function createProductionDependencies(signal?: AbortSignal, options?: ProductionDependencyOptions): CliDependencies;
@@ -0,0 +1,204 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { AdbAdapter } from "../adapters/adb/adb-adapter.js";
6
+ import { AndroidCliAdapter } from "../adapters/android-cli/android-cli-adapter.js";
7
+ import { SystemClock } from "../adapters/clock/system-clock.js";
8
+ import { FileSystemArtifactStore } from "../adapters/filesystem/artifact-store.js";
9
+ import { FileSystemGenerationMetaWriter } from "../adapters/filesystem/generation-meta-writer.js";
10
+ import { FileSystemGenerationSessionStore } from "../adapters/filesystem/generation-session-store.js";
11
+ import { FileSystemJourneyWriter } from "../adapters/filesystem/journey-writer.js";
12
+ import { NodeProjectFileInspector } from "../adapters/filesystem/project-file-inspector.js";
13
+ import { FileSystemSkillInstaller } from "../adapters/filesystem/skill-installer.js";
14
+ import { NodeProcessRunner } from "../adapters/process/node-process-runner.js";
15
+ import { InquirerRecorderPrompt } from "../adapters/prompt/inquirer-recorder-prompt.js";
16
+ import { InquirerGenerationPrompt } from "../adapters/prompt/inquirer-generation-prompt.js";
17
+ import { InquirerInitPrompt } from "../adapters/prompt/inquirer-init-prompt.js";
18
+ import { ContextValidator } from "../application/context/context-validator.js";
19
+ import { DoctorService } from "../application/doctor/doctor-service.js";
20
+ import { GenerationConfirmationService } from "../application/generation/generation-confirmation-service.js";
21
+ import { GenerationFinalizer } from "../application/generation/generation-finalizer.js";
22
+ import { GenerationPublisher } from "../application/generation/generation-publisher.js";
23
+ import { GenerationStarter, GenerationOperationError, hashGenerationBinding } from "../application/generation/generation-starter.js";
24
+ import { GenerationStepExecutor } from "../application/generation/generation-step-executor.js";
25
+ import { RuntimeObserver, SnapshotReobservationGuard } from "../application/generation/runtime-observer.js";
26
+ import { InitService } from "../application/init/init-service.js";
27
+ import { ProjectDescriber } from "../application/project/project-describer.js";
28
+ import { RecorderService } from "../application/recorder/recorder-service.js";
29
+ import { ReportWriter } from "../application/report/report-writer.js";
30
+ import { VerifyRuntime } from "../application/runtime/verify-runtime.js";
31
+ function runId() {
32
+ return `${new Date().toISOString().replaceAll(":", "-")}-${randomUUID()}`;
33
+ }
34
+ export function createProductionDependencies(signal, options = {}) {
35
+ const runner = new NodeProcessRunner();
36
+ const adb = new AdbAdapter(runner);
37
+ const androidCli = new AndroidCliAdapter(runner);
38
+ const clock = new SystemClock();
39
+ const generationStoreFactory = options.generationStoreFactory
40
+ ?? ((projectRoot) => (new FileSystemGenerationSessionStore(projectRoot)));
41
+ const contextValidator = new ContextValidator(new NodeProjectFileInspector());
42
+ return {
43
+ ...(signal === undefined ? {} : { signal }),
44
+ doctor: new DoctorService({
45
+ runner,
46
+ adb,
47
+ nodeVersion: process.version,
48
+ checkAndroidPermissions: async (deviceSerial, signal) => {
49
+ const directory = await mkdtemp(join(tmpdir(), "taphound-doctor-"));
50
+ try {
51
+ const result = await androidCli.captureScreen({
52
+ outputPath: join(directory, "screen.png"),
53
+ deviceSerial,
54
+ ...(signal === undefined ? {} : { signal })
55
+ });
56
+ if (result.exitCode !== 0
57
+ || result.spawnError !== undefined
58
+ || result.cancelled
59
+ || result.timedOut) {
60
+ return {
61
+ status: "failed",
62
+ message: result.stderr.trim()
63
+ || result.spawnError
64
+ || "Android screen capture permission probe failed"
65
+ };
66
+ }
67
+ return { status: "passed" };
68
+ }
69
+ finally {
70
+ await rm(directory, { recursive: true, force: true });
71
+ }
72
+ }
73
+ }),
74
+ recorder: new RecorderService({
75
+ androidCli,
76
+ adb,
77
+ clock,
78
+ prompt: new InquirerRecorderPrompt(),
79
+ journeyWriter: new FileSystemJourneyWriter()
80
+ }),
81
+ verifier: new VerifyRuntime({
82
+ androidCli,
83
+ adb,
84
+ clock,
85
+ artifactStore: new FileSystemArtifactStore(),
86
+ reportWriter: new ReportWriter(),
87
+ now: () => new Date(),
88
+ createRunId: runId
89
+ }),
90
+ projectDescriber: new ProjectDescriber(),
91
+ contextValidator,
92
+ init: new InitService({
93
+ installer: new FileSystemSkillInstaller(),
94
+ cwd: process.cwd(),
95
+ homedir: homedir()
96
+ }),
97
+ initPrompt: new InquirerInitPrompt(),
98
+ generationStarter: {
99
+ start: async (input) => new GenerationStarter({
100
+ contextValidator,
101
+ store: generationStoreFactory(input.projectRoot),
102
+ now: () => new Date(),
103
+ generateId: randomUUID,
104
+ randomBytes
105
+ }).start(input)
106
+ },
107
+ runtimeObserver: {
108
+ observe: async ({ projectRoot, ...input }) => (new RuntimeObserver({
109
+ store: generationStoreFactory(projectRoot),
110
+ adb,
111
+ androidCli,
112
+ now: () => new Date(),
113
+ createAttemptId: randomUUID
114
+ }).observe(input))
115
+ },
116
+ generationRuntime: ({ projectRoot, config }) => {
117
+ const store = generationStoreFactory(projectRoot);
118
+ const prompt = new InquirerGenerationPrompt();
119
+ const observer = new RuntimeObserver({
120
+ store,
121
+ adb,
122
+ androidCli,
123
+ now: () => new Date(),
124
+ createAttemptId: randomUUID
125
+ });
126
+ const freshnessGuard = new SnapshotReobservationGuard({
127
+ store,
128
+ adb,
129
+ androidCli,
130
+ now: () => new Date()
131
+ });
132
+ const confirmation = new GenerationConfirmationService({
133
+ store,
134
+ prompt,
135
+ now: () => new Date(),
136
+ generateChallengeId: randomUUID,
137
+ confirmationTtlMs: 5 * 60_000
138
+ });
139
+ const executor = new GenerationStepExecutor({
140
+ store,
141
+ freshnessGuard,
142
+ adb,
143
+ androidCli,
144
+ clock,
145
+ idle: config.idle,
146
+ now: () => new Date(),
147
+ generateAttemptId: randomUUID,
148
+ clearApprovedConfirmation: async (generationId, challenge) => confirmation.clearApproved({
149
+ generationId,
150
+ challenge
151
+ })
152
+ });
153
+ const publisher = new GenerationPublisher({
154
+ store,
155
+ journeyWriter: new FileSystemJourneyWriter(),
156
+ metaWriter: new FileSystemGenerationMetaWriter()
157
+ });
158
+ const finalizer = new GenerationFinalizer({
159
+ store,
160
+ contextValidator,
161
+ adb,
162
+ verifyRuntime: new VerifyRuntime({
163
+ androidCli,
164
+ adb,
165
+ clock,
166
+ artifactStore: new FileSystemArtifactStore(),
167
+ reportWriter: new ReportWriter(),
168
+ now: () => new Date(),
169
+ createRunId: runId
170
+ }),
171
+ publisher,
172
+ generateAttemptId: randomUUID
173
+ });
174
+ return {
175
+ confirmation,
176
+ executor,
177
+ observer,
178
+ finalizer,
179
+ readSession: (id) => store.read(id),
180
+ assertConfigIdentity: async (id) => {
181
+ const session = await store.read(id);
182
+ if (hashGenerationBinding(config) !== session.bindings.configHash) {
183
+ throw new GenerationOperationError("CONFIG_INVALID", "Generation configuration does not match the authoritative session");
184
+ }
185
+ }
186
+ };
187
+ },
188
+ readJson: async (path) => JSON.parse(await readFile(path, "utf8")),
189
+ cwd: () => process.cwd(),
190
+ stdout: {
191
+ write: (content) => {
192
+ process.stdout.write(content);
193
+ }
194
+ },
195
+ stderr: {
196
+ write: (content) => {
197
+ process.stderr.write(content);
198
+ }
199
+ },
200
+ setExitCode: (code) => {
201
+ process.exitCode = code;
202
+ }
203
+ };
204
+ }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { type CliDependencies } from "./dependencies.js";
3
+ export interface TerminationSignalSource {
4
+ once: (event: "SIGINT" | "SIGTERM", listener: () => void) => unknown;
5
+ off: (event: "SIGINT" | "SIGTERM", listener: () => void) => unknown;
6
+ }
7
+ export declare function withTerminationSignal<T>(operation: (signal: AbortSignal) => Promise<T>, source?: TerminationSignalSource): Promise<T>;
8
+ export declare function runMain(argv: readonly string[], dependencies?: CliDependencies): Promise<void>;
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { CommanderError } from "commander";
6
+ import { createProductionDependencies } from "./dependencies.js";
7
+ import { errorMessage, failureOutput, writeJson, writeLine } from "./output.js";
8
+ import { createProgram } from "./program.js";
9
+ export async function withTerminationSignal(operation, source = process) {
10
+ const controller = new AbortController();
11
+ const abort = () => {
12
+ controller.abort(new Error("TapHound was interrupted"));
13
+ };
14
+ source.once("SIGINT", abort);
15
+ source.once("SIGTERM", abort);
16
+ try {
17
+ return await operation(controller.signal);
18
+ }
19
+ finally {
20
+ source.off("SIGINT", abort);
21
+ source.off("SIGTERM", abort);
22
+ }
23
+ }
24
+ function asCommanderFailure(error) {
25
+ if (error instanceof CommanderError) {
26
+ return error;
27
+ }
28
+ if (error === null || typeof error !== "object") {
29
+ return undefined;
30
+ }
31
+ const candidate = error;
32
+ return typeof candidate.exitCode === "number"
33
+ && typeof candidate.code === "string"
34
+ && candidate.code.startsWith("commander.")
35
+ ? { exitCode: candidate.exitCode }
36
+ : undefined;
37
+ }
38
+ function overrideCommandExits(command) {
39
+ command.exitOverride();
40
+ for (const child of command.commands) {
41
+ overrideCommandExits(child);
42
+ }
43
+ }
44
+ export async function runMain(argv, dependencies = createProductionDependencies()) {
45
+ const json = argv.includes("--json");
46
+ try {
47
+ const program = createProgram(dependencies);
48
+ overrideCommandExits(program);
49
+ await program.parseAsync([...argv]);
50
+ }
51
+ catch (error) {
52
+ const commander = asCommanderFailure(error);
53
+ if (commander?.exitCode === 0) {
54
+ dependencies.setExitCode(0);
55
+ return;
56
+ }
57
+ const commanderFailure = commander !== undefined;
58
+ const exitCode = commanderFailure ? 2 : 4;
59
+ const output = failureOutput(exitCode, commanderFailure ? "CONFIG_INVALID" : "INTERNAL_ERROR", errorMessage(error));
60
+ if (json) {
61
+ writeJson(dependencies.stdout, output);
62
+ }
63
+ else {
64
+ writeLine(dependencies.stderr, output.failure.message);
65
+ }
66
+ dependencies.setExitCode(exitCode);
67
+ }
68
+ }
69
+ const entryPath = process.argv[1];
70
+ if (entryPath !== undefined
71
+ && import.meta.url === pathToFileURL(realpathSync(resolve(entryPath))).href) {
72
+ await withTerminationSignal((signal) => runMain(process.argv, createProductionDependencies(signal)));
73
+ }
@@ -0,0 +1,16 @@
1
+ import type { DoctorReport } from "../application/doctor/doctor-service.js";
2
+ import type { FailureCode } from "../domain/failure.js";
3
+ import type { TextOutput } from "./dependencies.js";
4
+ export interface CliFailureOutput {
5
+ status: "error";
6
+ exitCode: 2 | 3 | 4;
7
+ failure: {
8
+ code: FailureCode;
9
+ message: string;
10
+ };
11
+ }
12
+ export declare function writeJson(output: TextOutput, value: unknown): void;
13
+ export declare function writeLine(output: TextOutput, value: string): void;
14
+ export declare function failureOutput(exitCode: 2 | 3 | 4, code: FailureCode, message: string): CliFailureOutput;
15
+ export declare function doctorMessage(report: DoctorReport): string;
16
+ export declare function errorMessage(error: unknown): string;
@@ -0,0 +1,22 @@
1
+ export function writeJson(output, value) {
2
+ output.write(`${JSON.stringify(value)}\n`);
3
+ }
4
+ export function writeLine(output, value) {
5
+ output.write(`${value}\n`);
6
+ }
7
+ export function failureOutput(exitCode, code, message) {
8
+ return {
9
+ status: "error",
10
+ exitCode,
11
+ failure: { code, message }
12
+ };
13
+ }
14
+ export function doctorMessage(report) {
15
+ return report.checks.map((check) => {
16
+ const detail = check.version ?? check.message;
17
+ return `${check.status === "passed" ? "✓" : "✗"} ${check.name}${detail === undefined ? "" : `: ${detail}`}`;
18
+ }).join("\n");
19
+ }
20
+ export function errorMessage(error) {
21
+ return error instanceof Error ? error.message : String(error);
22
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import { type CliDependencies } from "./dependencies.js";
3
+ export declare function createProgram(dependencies?: CliDependencies): Command;
@@ -0,0 +1,43 @@
1
+ import { Command } from "commander";
2
+ import { createContextCommand } from "./commands/context.js";
3
+ import { createDoctorCommand } from "./commands/doctor.js";
4
+ import { createGenerationCommand } from "./commands/generation.js";
5
+ import { createInitCommand } from "./commands/init.js";
6
+ import { createProjectCommand } from "./commands/project.js";
7
+ import { createRecordCommand } from "./commands/record.js";
8
+ import { createVerifyCommand } from "./commands/verify.js";
9
+ import { createProductionDependencies } from "./dependencies.js";
10
+ export function createProgram(dependencies = createProductionDependencies()) {
11
+ const configureOutput = (command) => {
12
+ command.configureOutput({
13
+ writeOut: (content) => {
14
+ dependencies.stdout.write(content);
15
+ },
16
+ writeErr: (content) => {
17
+ dependencies.stderr.write(content);
18
+ }
19
+ });
20
+ for (const child of command.commands) {
21
+ configureOutput(child);
22
+ }
23
+ return command;
24
+ };
25
+ return new Command()
26
+ .name("taphound")
27
+ .description("Deterministic app journey recording and verification")
28
+ .configureOutput({
29
+ writeOut: (content) => {
30
+ dependencies.stdout.write(content);
31
+ },
32
+ writeErr: (content) => {
33
+ dependencies.stderr.write(content);
34
+ }
35
+ })
36
+ .addCommand(configureOutput(createDoctorCommand(dependencies)))
37
+ .addCommand(configureOutput(createRecordCommand(dependencies)))
38
+ .addCommand(configureOutput(createVerifyCommand(dependencies)))
39
+ .addCommand(configureOutput(createProjectCommand(dependencies)))
40
+ .addCommand(configureOutput(createContextCommand(dependencies)))
41
+ .addCommand(configureOutput(createGenerationCommand(dependencies)))
42
+ .addCommand(configureOutput(createInitCommand(dependencies)));
43
+ }
@@ -0,0 +1,7 @@
1
+ export interface ForegroundComponent {
2
+ packageName: string;
3
+ activity: string;
4
+ }
5
+ export declare function normalizeActivity(packageName: string, activityOrComponent: string): string;
6
+ export declare function parseObservedActivityComponent(component: string): ForegroundComponent;
7
+ export declare function normalizeObservedActivityComponent(component: string): string;