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,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,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>;
|
package/dist/cli/main.js
ADDED
|
@@ -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,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;
|