@testin-official/xagent 0.3.7
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/README.md +157 -0
- package/dist/action/client.d.ts +13 -0
- package/dist/action/client.d.ts.map +1 -0
- package/dist/action/client.js +28 -0
- package/dist/action/client.js.map +1 -0
- package/dist/action/mcp-content.d.ts +2 -0
- package/dist/action/mcp-content.d.ts.map +1 -0
- package/dist/action/mcp-content.js +116 -0
- package/dist/action/mcp-content.js.map +1 -0
- package/dist/action/protocol.d.ts +36 -0
- package/dist/action/protocol.d.ts.map +1 -0
- package/dist/action/protocol.js +29 -0
- package/dist/action/protocol.js.map +1 -0
- package/dist/assets/contracts/app-list.schema.json +31 -0
- package/dist/assets/contracts/app-upload.schema.json +45 -0
- package/dist/assets/contracts/app.schema.json +18 -0
- package/dist/assets/contracts/authoring-attempt-create.schema.json +12 -0
- package/dist/assets/contracts/authoring-attempt.schema.json +56 -0
- package/dist/assets/contracts/authoring-complete.schema.json +13 -0
- package/dist/assets/contracts/authoring-draft-attempt-create.schema.json +12 -0
- package/dist/assets/contracts/authoring-observation.schema.json +28 -0
- package/dist/assets/contracts/authoring-revision-create.schema.json +13 -0
- package/dist/assets/contracts/authoring-revision.schema.json +19 -0
- package/dist/assets/contracts/authoring-session-create.schema.json +11 -0
- package/dist/assets/contracts/authoring-session.schema.json +54 -0
- package/dist/assets/contracts/canonical-script.schema.json +219 -0
- package/dist/assets/contracts/compat-script.schema.json +380 -0
- package/dist/assets/contracts/device-list.schema.json +19 -0
- package/dist/assets/contracts/error.schema.json +47 -0
- package/dist/assets/contracts/execution-target.schema.json +55 -0
- package/dist/assets/contracts/fixtures/authoring-attempt-create.valid.json +4 -0
- package/dist/assets/contracts/fixtures/authoring-attempt.passed.json +16 -0
- package/dist/assets/contracts/fixtures/authoring-draft-attempt-create.valid.json +10 -0
- package/dist/assets/contracts/fixtures/authoring-observation.valid.json +7 -0
- package/dist/assets/contracts/fixtures/authoring-revision.valid.json +16 -0
- package/dist/assets/contracts/fixtures/authoring-session.active.json +12 -0
- package/dist/assets/contracts/fixtures/canonical-script.valid.json +17 -0
- package/dist/assets/contracts/fixtures/ci-exit-status-cases.json +44 -0
- package/dist/assets/contracts/fixtures/command-mutual-exclusion-cases.json +20 -0
- package/dist/assets/contracts/fixtures/compat-script.all-platform-actions.yaml +227 -0
- package/dist/assets/contracts/fixtures/execution-target.both.invalid.json +7 -0
- package/dist/assets/contracts/fixtures/execution-target.concurrency-two.invalid.json +4 -0
- package/dist/assets/contracts/fixtures/execution-target.count-two.invalid.json +6 -0
- package/dist/assets/contracts/fixtures/execution-target.dynamic.valid.json +12 -0
- package/dist/assets/contracts/fixtures/execution-target.fixed.valid.json +4 -0
- package/dist/assets/contracts/fixtures/execution-target.multiple-devices.invalid.json +4 -0
- package/dist/assets/contracts/fixtures/execution-target.multiple-filters.invalid.json +7 -0
- package/dist/assets/contracts/fixtures/execution-target.strategy-all.invalid.json +6 -0
- package/dist/assets/contracts/fixtures/idempotency.conflict.json +26 -0
- package/dist/assets/contracts/fixtures/idempotency.same-request.json +26 -0
- package/dist/assets/contracts/fixtures/principal.personal.json +23 -0
- package/dist/assets/contracts/fixtures/principal.project-ci.json +20 -0
- package/dist/assets/contracts/fixtures/project-config.no-default.valid.json +9 -0
- package/dist/assets/contracts/fixtures/project-config.secret.invalid.json +10 -0
- package/dist/assets/contracts/fixtures/project-config.target-name.invalid.json +10 -0
- package/dist/assets/contracts/fixtures/project-config.valid.json +22 -0
- package/dist/assets/contracts/fixtures/public-script.all-cli-actions.yaml +39 -0
- package/dist/assets/contracts/fixtures/public-script.invalid.json +8 -0
- package/dist/assets/contracts/fixtures/public-script.valid.json +17 -0
- package/dist/assets/contracts/fixtures/run-create-v2.device-id.invalid.json +16 -0
- package/dist/assets/contracts/fixtures/run-create.personal-upload.valid.json +20 -0
- package/dist/assets/contracts/fixtures/run-create.single-device.json +19 -0
- package/dist/assets/contracts/fixtures/run-create.target.valid.json +22 -0
- package/dist/assets/contracts/fixtures/run.queued.json +19 -0
- package/dist/assets/contracts/fixtures/run.target.queued.json +38 -0
- package/dist/assets/contracts/fixtures/skill-catalog.valid.json +19 -0
- package/dist/assets/contracts/fixtures/skill-reference.valid.json +9 -0
- package/dist/assets/contracts/fixtures/skill.valid.json +10 -0
- package/dist/assets/contracts/fixtures/target-hash-cases.json +30 -0
- package/dist/assets/contracts/fixtures/target-preview.hash-mismatch.error.json +8 -0
- package/dist/assets/contracts/fixtures/target-preview.no-match.error.json +9 -0
- package/dist/assets/contracts/fixtures/target-preview.request.valid.json +9 -0
- package/dist/assets/contracts/fixtures/target-preview.success.json +18 -0
- package/dist/assets/contracts/fixtures/target-preview.unavailable.json +18 -0
- package/dist/assets/contracts/principal.schema.json +72 -0
- package/dist/assets/contracts/project-config.schema.json +23 -0
- package/dist/assets/contracts/public-script.schema.json +397 -0
- package/dist/assets/contracts/run-create-v2.schema.json +78 -0
- package/dist/assets/contracts/run-create.schema.json +70 -0
- package/dist/assets/contracts/run.schema.json +137 -0
- package/dist/assets/contracts/skill-catalog.schema.json +15 -0
- package/dist/assets/contracts/skill-reference.schema.json +17 -0
- package/dist/assets/contracts/skill-summary.schema.json +28 -0
- package/dist/assets/contracts/skill.schema.json +30 -0
- package/dist/assets/contracts/target-preview.schema.json +75 -0
- package/dist/assets/integrations/manifest.json +17 -0
- package/dist/assets/integrations/xagent-skill/SKILL.md +28 -0
- package/dist/assets/integrations/xagent-skill/agents/openai.yaml +4 -0
- package/dist/assets/integrations/xagent-skill/references/cli-mcp-interop.md +10 -0
- package/dist/assets/integrations/xagent-skill/references/host-adapters.md +9 -0
- package/dist/assets/integrations/xagent-skill/references/installation.md +10 -0
- package/dist/assets/integrations/xagent-skill/references/privacy.md +3 -0
- package/dist/assets/integrations/xagent-skill/references/routing.md +32 -0
- package/dist/assets/integrations/xagent-skill/scripts/install-cli.ps1 +5 -0
- package/dist/assets/integrations/xagent-skill/scripts/install-cli.sh +6 -0
- package/dist/assets/skills/xagent-analyze-failure/SKILL.md +13 -0
- package/dist/assets/skills/xagent-analyze-failure/agents/openai.yaml +4 -0
- package/dist/assets/skills/xagent-analyze-failure/references/failure-classification.md +20 -0
- package/dist/assets/skills/xagent-author-test/SKILL.md +19 -0
- package/dist/assets/skills/xagent-author-test/agents/openai.yaml +4 -0
- package/dist/assets/skills/xagent-author-test/references/authoring-workflow.md +41 -0
- package/dist/assets/skills/xagent-author-test/references/host-interaction-adapters.md +81 -0
- package/dist/assets/skills/xagent-author-test/references/interaction-modes.md +97 -0
- package/dist/assets/skills/xagent-author-test/references/interactive-steering.md +57 -0
- package/dist/assets/skills/xagent-author-test/references/script-actions.md +34 -0
- package/dist/assets/skills/xagent-author-test/references/script-format.md +100 -0
- package/dist/assets/skills/xagent-cli/SKILL.md +31 -0
- package/dist/assets/skills/xagent-cli/agents/openai.yaml +4 -0
- package/dist/assets/skills/xagent-cli/references/capability-routing.md +36 -0
- package/dist/assets/skills/xagent-cli/references/cli-commands.md +23 -0
- package/dist/assets/skills/xagent-cli/references/failure-routing.md +18 -0
- package/dist/assets/skills/xagent-cli/references/host-modes.md +21 -0
- package/dist/assets/skills/xagent-cli/references/mcp-tools.md +26 -0
- package/dist/assets/skills/xagent-cli/references/permissions.md +17 -0
- package/dist/assets/skills/xagent-explore-device/SKILL.md +26 -0
- package/dist/assets/skills/xagent-explore-device/agents/openai.yaml +4 -0
- package/dist/assets/skills/xagent-explore-device/references/actions.md +24 -0
- package/dist/assets/skills/xagent-explore-device/references/single-action-protocol.md +59 -0
- package/dist/assets/skills/xagent-explore-device/references/tool-adapters.md +32 -0
- package/dist/assets/skills/xagent-repair-test/SKILL.md +16 -0
- package/dist/assets/skills/xagent-repair-test/agents/openai.yaml +4 -0
- package/dist/assets/skills/xagent-repair-test/references/repair-workflow.md +21 -0
- package/dist/assets/skills/xagent-run-test/SKILL.md +16 -0
- package/dist/assets/skills/xagent-run-test/agents/openai.yaml +4 -0
- package/dist/assets/skills/xagent-run-test/references/execution-modes.md +23 -0
- package/dist/assets/skills/xagent-run-test/references/result-status.md +13 -0
- package/dist/assets/skills/xagent-test-report/SKILL.md +20 -0
- package/dist/assets/skills/xagent-test-report/agents/openai.yaml +4 -0
- package/dist/assets/skills/xagent-test-report/references/evidence-policy.md +20 -0
- package/dist/assets/skills/xagent-test-report/references/report-template.md +41 -0
- package/dist/assets/skills/xagent-test-report/references/result-classification.md +13 -0
- package/dist/authoring/draft-store.d.ts +19 -0
- package/dist/authoring/draft-store.d.ts.map +1 -0
- package/dist/authoring/draft-store.js +104 -0
- package/dist/authoring/draft-store.js.map +1 -0
- package/dist/authoring/session-lease.d.ts +64 -0
- package/dist/authoring/session-lease.d.ts.map +1 -0
- package/dist/authoring/session-lease.js +190 -0
- package/dist/authoring/session-lease.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +529 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/credentials.d.ts +16 -0
- package/dist/config/credentials.d.ts.map +1 -0
- package/dist/config/credentials.js +93 -0
- package/dist/config/credentials.js.map +1 -0
- package/dist/config/store.d.ts +37 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +142 -0
- package/dist/config/store.js.map +1 -0
- package/dist/core/assets.d.ts +5 -0
- package/dist/core/assets.d.ts.map +1 -0
- package/dist/core/assets.js +46 -0
- package/dist/core/assets.js.map +1 -0
- package/dist/core/errors.d.ts +19 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +36 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/interrupt.d.ts +8 -0
- package/dist/core/interrupt.d.ts.map +1 -0
- package/dist/core/interrupt.js +54 -0
- package/dist/core/interrupt.js.map +1 -0
- package/dist/core/json.d.ts +5 -0
- package/dist/core/json.d.ts.map +1 -0
- package/dist/core/json.js +21 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/version.d.ts +8 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +8 -0
- package/dist/core/version.js.map +1 -0
- package/dist/integrations/manager.d.ts +12 -0
- package/dist/integrations/manager.d.ts.map +1 -0
- package/dist/integrations/manager.js +218 -0
- package/dist/integrations/manager.js.map +1 -0
- package/dist/integrations/routing.d.ts +105 -0
- package/dist/integrations/routing.d.ts.map +1 -0
- package/dist/integrations/routing.js +85 -0
- package/dist/integrations/routing.js.map +1 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +69 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +30 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +345 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/project/config.d.ts +15 -0
- package/dist/project/config.d.ts.map +1 -0
- package/dist/project/config.js +46 -0
- package/dist/project/config.js.map +1 -0
- package/dist/run/progress.d.ts +5 -0
- package/dist/run/progress.d.ts.map +1 -0
- package/dist/run/progress.js +29 -0
- package/dist/run/progress.js.map +1 -0
- package/dist/script/service.d.ts +42 -0
- package/dist/script/service.d.ts.map +1 -0
- package/dist/script/service.js +457 -0
- package/dist/script/service.js.map +1 -0
- package/dist/sdk/authoring.d.ts +19 -0
- package/dist/sdk/authoring.d.ts.map +1 -0
- package/dist/sdk/authoring.js +43 -0
- package/dist/sdk/authoring.js.map +1 -0
- package/dist/sdk/client.d.ts +49 -0
- package/dist/sdk/client.d.ts.map +1 -0
- package/dist/sdk/client.js +241 -0
- package/dist/sdk/client.js.map +1 -0
- package/dist/skills/catalog.d.ts +7 -0
- package/dist/skills/catalog.d.ts.map +1 -0
- package/dist/skills/catalog.js +59 -0
- package/dist/skills/catalog.js.map +1 -0
- package/package.json +45 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { AuthoringDraftStore } from "./authoring/draft-store.js";
|
|
8
|
+
import { buildDeviceActionRequest } from "./action/protocol.js";
|
|
9
|
+
import { SessionLeaseManager } from "./authoring/session-lease.js";
|
|
10
|
+
import { CredentialResolver } from "./config/credentials.js";
|
|
11
|
+
import { ConfigStore, Profile } from "./config/store.js";
|
|
12
|
+
import { CliError, XAgentApiError } from "./core/errors.js";
|
|
13
|
+
import { onInterrupt } from "./core/interrupt.js";
|
|
14
|
+
import { CANONICAL_SCRIPT_SCHEMA_VERSION, CONFIG_SCHEMA_VERSION, DEVELOPER_API_VERSION, PRINCIPAL_SCHEMA_VERSION, PROJECT_SCHEMA_VERSION, PUBLIC_SCRIPT_SCHEMA_VERSION, VERSION } from "./core/version.js";
|
|
15
|
+
import { loadProjectConfig } from "./project/config.js";
|
|
16
|
+
import { createRunProgressReporter } from "./run/progress.js";
|
|
17
|
+
import { parseYaml } from "./script/service.js";
|
|
18
|
+
import { XAgentClient } from "./sdk/client.js";
|
|
19
|
+
import { exportSkills, listSkillReferences, listSkills, readSkill, readSkillReference } from "./skills/catalog.js";
|
|
20
|
+
import { runStdioServer } from "./mcp/server.js";
|
|
21
|
+
import { MCP_MODES } from "./mcp/tools.js";
|
|
22
|
+
import { detectHost, doctorHost, HOSTS, installHost, routeOutput, syncHost, uninstallHost, verifySkillDirectory } from "./integrations/manager.js";
|
|
23
|
+
import { DEFAULT_WORKFLOW_DOMAIN, USER_WORKFLOW_DOMAINS, USER_WORKFLOW_ROUTES, routeForDomain } from "./integrations/routing.js";
|
|
24
|
+
const API_EXIT = {
|
|
25
|
+
invalid_request: 2, schema_invalid: 2, configuration_invalid: 2, target_not_found: 2, target_invalid: 2,
|
|
26
|
+
target_hash_mismatch: 2, idempotency_conflict: 2, authoring_state_invalid: 2, revision_conflict: 2, action_in_progress: 2, observation_stale: 2,
|
|
27
|
+
attempt_in_progress: 2, authentication_required: 3, token_invalid: 3, token_expired: 3, token_revoked: 3,
|
|
28
|
+
token_type_mismatch: 3, permission_denied: 3, cancelled: 4, device_unavailable: 5, resource_not_found: 5,
|
|
29
|
+
authoring_session_expired: 5, authoring_session_not_active: 5,
|
|
30
|
+
test_failed: 1, infrastructure_failed: 6, invalid_response: 6, authoring_session_renewal_failed: 6, timeout: 7,
|
|
31
|
+
};
|
|
32
|
+
function context(command) {
|
|
33
|
+
const options = command.optsWithGlobals();
|
|
34
|
+
return { json: Boolean(options.json), profile: options.profile, userConfig: options.userConfig, projectConfig: options.config, nonInteractive: Boolean(options.nonInteractive), config: new ConfigStore(options.userConfig) };
|
|
35
|
+
}
|
|
36
|
+
function emit(ctx, data, human) {
|
|
37
|
+
if (ctx.json)
|
|
38
|
+
process.stdout.write(`${JSON.stringify(data)}\n`);
|
|
39
|
+
else if (human)
|
|
40
|
+
process.stdout.write(`${human}\n`);
|
|
41
|
+
else
|
|
42
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
43
|
+
}
|
|
44
|
+
function unexpected(error) {
|
|
45
|
+
// Keep whatever the runtime actually threw. Collapsing every unexpected
|
|
46
|
+
// error to a bare "XAgent command failed" left no way to tell a broken
|
|
47
|
+
// socket apart from a bad path or an out-of-memory failure.
|
|
48
|
+
const cause = error instanceof Error ? error : undefined;
|
|
49
|
+
const message = (cause?.message ?? String(error ?? "")).trim();
|
|
50
|
+
const details = {};
|
|
51
|
+
if (cause?.name)
|
|
52
|
+
details.cause_name = cause.name;
|
|
53
|
+
const causeCode = error?.code ?? cause?.cause?.code;
|
|
54
|
+
if (causeCode !== undefined)
|
|
55
|
+
details.cause_code = String(causeCode);
|
|
56
|
+
if (cause?.stack)
|
|
57
|
+
details.cause_stack = cause.stack.split("\n").slice(0, 6).join("\n");
|
|
58
|
+
return new CliError("infrastructure_failed", message ? `XAgent command failed: ${message}` : "XAgent command failed", 6, details);
|
|
59
|
+
}
|
|
60
|
+
function fail(error) {
|
|
61
|
+
const known = error instanceof CliError || error instanceof XAgentApiError ? error : unexpected(error);
|
|
62
|
+
const json = process.argv.includes("--json");
|
|
63
|
+
const payload = known.toJSON();
|
|
64
|
+
if (json)
|
|
65
|
+
process.stdout.write(`${JSON.stringify({ error: payload })}\n`);
|
|
66
|
+
else
|
|
67
|
+
process.stderr.write(`Error: ${String(payload.message)}\n${payload.application_url ? `Apply at: ${payload.application_url}\n` : ""}`);
|
|
68
|
+
process.exit(API_EXIT[known.code] ?? (known instanceof CliError ? known.exitCode : 6));
|
|
69
|
+
}
|
|
70
|
+
function tokenApplicationUrl(baseUrl, tokenType) {
|
|
71
|
+
try {
|
|
72
|
+
const url = new URL(baseUrl);
|
|
73
|
+
return `${url.origin}/agent/developer-tokens/index.html?type=${encodeURIComponent(tokenType)}`;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return `${baseUrl.replace(/\/+$/, "")}/agent/developer-tokens/index.html?type=${encodeURIComponent(tokenType)}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function readSecret(prompt) {
|
|
80
|
+
if (!input.isTTY || typeof input.setRawMode !== "function")
|
|
81
|
+
throw new CliError("authentication_required", "--token-stdin is required when no interactive terminal is available", 3);
|
|
82
|
+
output.write(prompt);
|
|
83
|
+
input.setRawMode(true);
|
|
84
|
+
input.resume();
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
let value = "";
|
|
87
|
+
const finish = (error) => {
|
|
88
|
+
input.off("data", onData);
|
|
89
|
+
input.setRawMode(false);
|
|
90
|
+
input.pause();
|
|
91
|
+
output.write("\n");
|
|
92
|
+
if (error)
|
|
93
|
+
reject(error);
|
|
94
|
+
else
|
|
95
|
+
resolve(value);
|
|
96
|
+
};
|
|
97
|
+
const onData = (chunk) => {
|
|
98
|
+
for (const character of chunk.toString("utf8")) {
|
|
99
|
+
if (character === "\r" || character === "\n") {
|
|
100
|
+
finish();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (character === "\u0003") {
|
|
104
|
+
finish(new CliError("cancelled", "Token input was cancelled", 4));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (character === "\b" || character === "\u007f")
|
|
108
|
+
value = value.slice(0, -1);
|
|
109
|
+
else
|
|
110
|
+
value += character;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
input.on("data", onData);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async function selectedProfile(ctx) { return ctx.config.getProfile(ctx.profile); }
|
|
117
|
+
async function authenticatedClient(ctx, uploadTimeoutMs) {
|
|
118
|
+
const profile = await selectedProfile(ctx);
|
|
119
|
+
let token;
|
|
120
|
+
try {
|
|
121
|
+
token = await new CredentialResolver().resolve(profile);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof CliError && error.code === "authentication_required")
|
|
125
|
+
throw new CliError(error.code, error.message, 3, { required_token_type: profile.data.expected_token_type, application_url: profile.data.token_apply_url });
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
const client = new XAgentClient(profile.data.base_url, token, profile.data.upload_base_url, 30_000, uploadTimeoutMs ?? 900_000);
|
|
129
|
+
return { profile, client };
|
|
130
|
+
}
|
|
131
|
+
async function withFreshSession(ctx, sessionId, operation) {
|
|
132
|
+
const { client } = await authenticatedClient(ctx);
|
|
133
|
+
const manager = new SessionLeaseManager(client.authoring);
|
|
134
|
+
await manager.ensureFresh(sessionId);
|
|
135
|
+
try {
|
|
136
|
+
return await operation(client);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
manager.unregister(sessionId);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function withFreshActionSession(ctx, sessionId, operation) {
|
|
143
|
+
const { client } = await authenticatedClient(ctx);
|
|
144
|
+
const manager = new SessionLeaseManager(client.action);
|
|
145
|
+
await manager.ensureFresh(sessionId);
|
|
146
|
+
try {
|
|
147
|
+
return await operation(client);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
manager.unregister(sessionId);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function loadScript(file) {
|
|
154
|
+
let content;
|
|
155
|
+
try {
|
|
156
|
+
content = await readFile(file, "utf8");
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
throw new CliError("script_read_failed", `unable to read ${file}`);
|
|
160
|
+
}
|
|
161
|
+
const parsed = await parseYaml(content);
|
|
162
|
+
if (!parsed.valid)
|
|
163
|
+
throw new CliError("schema_invalid", "YAML script validation failed", 2, { issues: parsed.issues });
|
|
164
|
+
return { ...parsed, content };
|
|
165
|
+
}
|
|
166
|
+
function requireProjectCi(profile, commandName) { if (profile.data.expected_token_type !== "project_ci")
|
|
167
|
+
throw new CliError("token_type_mismatch", `${commandName} requires a project_ci Profile`, 3); }
|
|
168
|
+
function parseVariables(values) { const result = {}; for (const value of values) {
|
|
169
|
+
const split = value.indexOf("=");
|
|
170
|
+
if (split <= 0)
|
|
171
|
+
throw new CliError("invalid_request", "--var must use NAME=VALUE");
|
|
172
|
+
result[value.slice(0, split).trim()] = value.slice(split + 1);
|
|
173
|
+
} return result; }
|
|
174
|
+
function billingText(value) {
|
|
175
|
+
const billing = value?.billing ?? value?.session?.billing;
|
|
176
|
+
return billing ? ` billing=${billing.status} minutes=${billing.billable_minutes} cost=${billing.cost}` : "";
|
|
177
|
+
}
|
|
178
|
+
function emitRun(ctx, run, useStatusExit = true) { emit(ctx, run, `Run ${run.id} status=${run.status}${billingText(run)}`); if (!useStatusExit)
|
|
179
|
+
return; process.exitCode = { failed: 1, cancelled: 4, infrastructure_failed: 6, timed_out: 7 }[run.status] ?? 0; }
|
|
180
|
+
function progressReporter(ctx) { return createRunProgressReporter(ctx); }
|
|
181
|
+
const program = new Command().name("xagent").description("XAgent Developer CLI").showHelpAfterError();
|
|
182
|
+
program.option("--json", "Emit stable JSON output").option("--profile <name>", "Select a named authentication Profile").option("--config <path>", "Use a project xagent.yaml").option("--user-config <path>", "Use an explicit user Profile configuration").option("--non-interactive", "Disable interactive prompts");
|
|
183
|
+
program.command("version").description("Show CLI and contract versions").action(async function () {
|
|
184
|
+
const ctx = context(this);
|
|
185
|
+
emit(ctx, { version: VERSION, developer_api_version: DEVELOPER_API_VERSION, config_schema_version: CONFIG_SCHEMA_VERSION, project_schema_version: PROJECT_SCHEMA_VERSION, principal_schema_version: PRINCIPAL_SCHEMA_VERSION, public_script_schema_version: PUBLIC_SCRIPT_SCHEMA_VERSION, canonical_script_schema_version: CANONICAL_SCRIPT_SCHEMA_VERSION }, `xagent ${VERSION}`);
|
|
186
|
+
});
|
|
187
|
+
program.command("doctor").description("Check local configuration and credential source").action(async function () {
|
|
188
|
+
const ctx = context(this);
|
|
189
|
+
const payload = await ctx.config.load();
|
|
190
|
+
const profile = ctx.profile || payload.default_profile;
|
|
191
|
+
emit(ctx, { checks: { configuration: "ok", profile: profile ? "ok" : "not_configured", credential_source: profile && payload.profiles[profile] && (payload.profiles[profile].credential_env || payload.profiles[profile].credential_ref) ? "configured" : "not_configured", online_checks_performed: false } }, "Local configuration is valid");
|
|
192
|
+
});
|
|
193
|
+
const config = program.command("config");
|
|
194
|
+
config.command("path").action(async function () { const ctx = context(this); emit(ctx, { path: ctx.config.filePath }, ctx.config.filePath); });
|
|
195
|
+
config.command("validate").action(async function () { const ctx = context(this); await ctx.config.load(); emit(ctx, { valid: true, schema_version: CONFIG_SCHEMA_VERSION }, "Configuration is valid"); });
|
|
196
|
+
config.command("show").action(async function () { const ctx = context(this); const payload = await ctx.config.load(); emit(ctx, { schema_version: payload.schema_version, default_profile: payload.default_profile, profiles: Object.fromEntries(Object.entries(payload.profiles).sort(([a], [b]) => a.localeCompare(b)).map(([name, item]) => [name, Profile.from(name, item).publicData()])) }); });
|
|
197
|
+
const profiles = program.command("profiles");
|
|
198
|
+
profiles.command("add").argument("<name>").requiredOption("--base-url <url>").requiredOption("--expected-token-type <type>").option("--token-env <name>").option("--token-apply-url <url>").option("--upload-base-url <url>").option("--use").action(async function (name, options) { const ctx = context(this); const data = { base_url: options.baseUrl, expected_token_type: options.expectedTokenType, ...(options.tokenEnv ? { credential_env: options.tokenEnv } : {}), ...(options.uploadBaseUrl ? { upload_base_url: options.uploadBaseUrl } : {}), token_apply_url: options.tokenApplyUrl ?? tokenApplicationUrl(options.baseUrl, options.expectedTokenType) }; const profile = Profile.from(name, data); await ctx.config.putProfile(profile); if (options.use)
|
|
199
|
+
await ctx.config.useProfile(name); emit(ctx, profile.publicData(), `Profile '${name}' added`); });
|
|
200
|
+
profiles.command("list").action(async function () { const ctx = context(this); const payload = await ctx.config.load(); emit(ctx, { profiles: (await ctx.config.listProfiles()).map((profile) => ({ ...profile.publicData(), default: profile.name === payload.default_profile })) }); });
|
|
201
|
+
profiles.command("show").argument("[name]").action(async function (name) { const ctx = context(this); const profile = await ctx.config.getProfile(name ?? ctx.profile); emit(ctx, profile.publicData()); });
|
|
202
|
+
profiles.command("use").argument("<name>").action(async function (name) { const ctx = context(this); await ctx.config.useProfile(name); emit(ctx, { default_profile: name }, `Profile '${name}' selected`); });
|
|
203
|
+
profiles.command("remove").argument("<name>").action(async function (name) { const ctx = context(this); const profile = await ctx.config.getProfile(name); await new CredentialResolver().delete(profile); await ctx.config.removeProfile(name); emit(ctx, { removed: name }, `Profile '${name}' removed`); });
|
|
204
|
+
const auth = program.command("auth");
|
|
205
|
+
auth.command("status").action(async function () { const ctx = context(this); const { profile, client } = await authenticatedClient(ctx); const principal = await client.getPrincipal(); if (principal.token_type !== profile.data.expected_token_type)
|
|
206
|
+
throw new CliError("token_type_mismatch", `Profile expects ${profile.data.expected_token_type}, server resolved ${principal.token_type}`, 3); emit(ctx, { profile: profile.name, principal }, `${profile.name}: ${principal.token_type} eid=${principal.eid} project=${principal.project_id}`); });
|
|
207
|
+
auth.command("login").option("--save").option("--token-stdin").action(async function (options) { const ctx = context(this); const profile = await selectedProfile(ctx); if (!options.tokenStdin && !ctx.json)
|
|
208
|
+
process.stderr.write(`Token 申请地址:${profile.data.token_apply_url ?? tokenApplicationUrl(profile.data.base_url, profile.data.expected_token_type)}\n`); let token = ""; if (options.tokenStdin)
|
|
209
|
+
token = (await readFile(0, "utf8")).trim().replace(/^\uFEFF/, "");
|
|
210
|
+
else if (ctx.nonInteractive)
|
|
211
|
+
throw new CliError("authentication_required", "--token-stdin is required in non-interactive mode", 3);
|
|
212
|
+
else
|
|
213
|
+
token = (await readSecret("Developer Token: ")).trim(); if (!token)
|
|
214
|
+
throw new CliError("authentication_required", "Developer Token is required", 3); const client = new XAgentClient(profile.data.base_url, token); let principal; try {
|
|
215
|
+
principal = await client.getPrincipal();
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (error instanceof XAgentApiError && ["token_invalid", "token_expired", "token_revoked"].includes(error.code))
|
|
219
|
+
throw new CliError(error.code, error.message, 3, { required_token_type: profile.data.expected_token_type, application_url: profile.data.token_apply_url });
|
|
220
|
+
throw error;
|
|
221
|
+
} if (principal.token_type !== profile.data.expected_token_type)
|
|
222
|
+
throw new CliError("token_type_mismatch", `Profile expects ${profile.data.expected_token_type}, server resolved ${principal.token_type}`, 3); let saved = false; if (options.save) {
|
|
223
|
+
const resolver = new CredentialResolver();
|
|
224
|
+
const reference = await resolver.save(profile, token);
|
|
225
|
+
try {
|
|
226
|
+
await ctx.config.setCredentialReference(profile.name, reference);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
await resolver.store.delete(reference);
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
saved = true;
|
|
233
|
+
} emit(ctx, { profile: profile.name, saved, principal }, `Authenticated Profile '${profile.name}'`); });
|
|
234
|
+
auth.command("logout").action(async function () { const ctx = context(this); const profile = await selectedProfile(ctx); await new CredentialResolver().delete(profile); await ctx.config.setCredentialReference(profile.name); emit(ctx, { profile: profile.name, credential_removed: true }, `Profile '${profile.name}' logged out`); });
|
|
235
|
+
program.command("init").requiredOption("--base-url <url>").option("--token-type <type>", undefined, "personal").option("--name <name>", undefined, "default").option("--token-env <name>").option("--token-apply-url <url>").option("--upload-base-url <url>").action(async function (options) { const ctx = context(this); const profile = Profile.from(options.name, { base_url: options.baseUrl, expected_token_type: options.tokenType, ...(options.tokenEnv ? { credential_env: options.tokenEnv } : {}), ...(options.uploadBaseUrl ? { upload_base_url: options.uploadBaseUrl } : {}), token_apply_url: options.tokenApplyUrl ?? tokenApplicationUrl(options.baseUrl, options.tokenType) }); await ctx.config.putProfile(profile); await ctx.config.useProfile(profile.name); emit(ctx, profile.publicData(), `Profile '${profile.name}' initialized`); if (!ctx.json)
|
|
236
|
+
process.stderr.write(`Token 申请地址:${profile.data.token_apply_url}\n`); });
|
|
237
|
+
program.command("validate").argument("<scriptFile>").action(async function (file) { const ctx = context(this); const parsed = await loadScript(file); emit(ctx, { valid: true, schema_version: parsed.inputSchemaVersion ?? parsed.script.schema_version, execution_schema_version: parsed.script.schema_version, source_hash: parsed.sourceHash, step_count: parsed.script.steps.length }, `${file}: valid (${parsed.script.steps.length} steps)`); });
|
|
238
|
+
const skills = program.command("skills");
|
|
239
|
+
skills.command("list").action(async function () { const ctx = context(this); emit(ctx, await (await authenticatedClient(ctx)).client.listEffectiveSkills()); });
|
|
240
|
+
skills.command("show").argument("<name>").action(async function (name) { const ctx = context(this); emit(ctx, await (await authenticatedClient(ctx)).client.getEffectiveSkill(name)); });
|
|
241
|
+
skills.command("reference").argument("<name>").argument("<path>").action(async function (name, reference) { const ctx = context(this); emit(ctx, await (await authenticatedClient(ctx)).client.getEffectiveSkillReference(name, reference)); });
|
|
242
|
+
const packaged = skills.command("packaged");
|
|
243
|
+
packaged.command("list").action(async function () { const ctx = context(this); emit(ctx, { skills: listSkills() }); });
|
|
244
|
+
packaged.command("show").argument("<name>").action(async function (name) { const ctx = context(this); try {
|
|
245
|
+
emit(ctx, { name, content: await readSkill(name) });
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
throw new XAgentApiError("resource_not_found", "Skill was not found");
|
|
249
|
+
} });
|
|
250
|
+
packaged.command("references").argument("<name>").action(async function (name) { const ctx = context(this); try {
|
|
251
|
+
emit(ctx, { name, references: await listSkillReferences(name) });
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
throw new XAgentApiError("resource_not_found", "Skill was not found");
|
|
255
|
+
} });
|
|
256
|
+
packaged.command("reference").argument("<name>").argument("<path>").action(async function (name, reference) { const ctx = context(this); try {
|
|
257
|
+
emit(ctx, { name, path: reference, content: await readSkillReference(name, reference) });
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
throw new XAgentApiError("resource_not_found", "Skill Reference was not found");
|
|
261
|
+
} });
|
|
262
|
+
packaged.command("export").argument("<directory>").option("--overwrite").action(async function (directory, options) { const ctx = context(this); emit(ctx, { written: await exportSkills(directory, Boolean(options.overwrite)) }); });
|
|
263
|
+
function hostArgument(value) {
|
|
264
|
+
if (!HOSTS.includes(value))
|
|
265
|
+
throw new CliError("invalid_request", `host must be one of: ${HOSTS.join(", ")}`);
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
function domainArgument(value) {
|
|
269
|
+
if (!USER_WORKFLOW_DOMAINS.includes(value))
|
|
270
|
+
throw new CliError("invalid_request", `user behavior domain must be one of: ${USER_WORKFLOW_DOMAINS.join(", ")}`);
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
const integrations = program.command("integrations").description("Install and verify the XAgent Agent Host Skill");
|
|
274
|
+
integrations.command("detect").option("--host <host>").action(async function (options) {
|
|
275
|
+
const ctx = context(this);
|
|
276
|
+
emit(ctx, { hosts: await detectHost(options.host ? hostArgument(options.host) : undefined) });
|
|
277
|
+
});
|
|
278
|
+
integrations.command("install").argument("<host>").option("--force", "Overwrite the existing XAgent Skill only").action(async function (host, options) {
|
|
279
|
+
const ctx = context(this);
|
|
280
|
+
emit(ctx, await installHost(hostArgument(host), Boolean(options.force)), `Installed xagent-skill for ${host}`);
|
|
281
|
+
});
|
|
282
|
+
integrations.command("doctor").option("--host <host>").action(async function (options) {
|
|
283
|
+
const ctx = context(this);
|
|
284
|
+
emit(ctx, { checks: await doctorHost(options.host ? hostArgument(options.host) : undefined) });
|
|
285
|
+
});
|
|
286
|
+
integrations.command("sync").option("--host <host>").action(async function (options) {
|
|
287
|
+
const ctx = context(this);
|
|
288
|
+
emit(ctx, { synced: await syncHost(options.host ? hostArgument(options.host) : undefined) });
|
|
289
|
+
});
|
|
290
|
+
integrations.command("uninstall").argument("<host>").requiredOption("--force", "Confirm removal of the XAgent Skill directory").action(async function (host, options) {
|
|
291
|
+
const ctx = context(this);
|
|
292
|
+
emit(ctx, await uninstallHost(hostArgument(host), Boolean(options.force)), `Removed xagent-skill for ${host}`);
|
|
293
|
+
});
|
|
294
|
+
integrations.command("verify-skill").argument("<directory>").action(async function (directory) {
|
|
295
|
+
const ctx = context(this);
|
|
296
|
+
const result = await verifySkillDirectory(directory);
|
|
297
|
+
emit(ctx, result, result.valid ? "XAgent Skill is compatible" : "XAgent Skill is incompatible");
|
|
298
|
+
if (!result.valid)
|
|
299
|
+
process.exitCode = 2;
|
|
300
|
+
});
|
|
301
|
+
integrations.command("route").argument("<domain>").option("--profile <name>", "Profile name to include in the MCP command").action(async function (domain, options) {
|
|
302
|
+
const ctx = context(this);
|
|
303
|
+
const selected = domainArgument(domain);
|
|
304
|
+
routeForDomain(selected);
|
|
305
|
+
emit(ctx, routeOutput(selected, options.profile ?? ctx.profile));
|
|
306
|
+
});
|
|
307
|
+
integrations.command("options").action(async function () {
|
|
308
|
+
const ctx = context(this);
|
|
309
|
+
emit(ctx, {
|
|
310
|
+
default_domain: DEFAULT_WORKFLOW_DOMAIN,
|
|
311
|
+
default_mode: routeForDomain(DEFAULT_WORKFLOW_DOMAIN).mode,
|
|
312
|
+
choices: USER_WORKFLOW_ROUTES.map(({ domain, mode, skill, side_effects, description }) => ({ domain, mode, skill, side_effects, description })),
|
|
313
|
+
selection_required_before_side_effects: true,
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
const authoring = program.command("authoring");
|
|
317
|
+
authoring.command("start").requiredOption("--device-id <id>").requiredOption("--idempotency-key <key>").action(async function (options) { const ctx = context(this); const result = await (await authenticatedClient(ctx)).client.authoring.createSession(options.deviceId, options.idempotencyKey); emit(ctx, result, `Authoring Session ${result.id} status=${result.status}${billingText(result)}`); });
|
|
318
|
+
authoring.command("show").argument("<sessionId>").action(async function (sessionId) { const ctx = context(this); emit(ctx, await (await authenticatedClient(ctx)).client.authoring.getSession(sessionId)); });
|
|
319
|
+
authoring.command("renew").argument("<sessionId>").action(async function (sessionId) { const ctx = context(this); const result = await (await authenticatedClient(ctx)).client.authoring.renewSession(sessionId); emit(ctx, result, `Authoring Session ${result.id} renewed until ${result.expires_at}${billingText(result)}`); });
|
|
320
|
+
authoring.command("keepalive").argument("<sessionId>").option("--poll-interval <seconds>", undefined, "30").action(async function (sessionId, options) {
|
|
321
|
+
const ctx = context(this);
|
|
322
|
+
const { client } = await authenticatedClient(ctx);
|
|
323
|
+
const manager = new SessionLeaseManager(client.authoring, { onEvent: (event) => emit(ctx, event, event.type === "renewed" ? `Authoring Session ${event.session_id} renewed until ${event.expires_at ?? "unknown"}` : undefined) });
|
|
324
|
+
// Ctrl+C stops this foreground keepalive without cancelling the Session;
|
|
325
|
+
// the user can explicitly run `authoring cancel` when the work is finished.
|
|
326
|
+
const deregister = onInterrupt(async () => { manager.unregister(sessionId); });
|
|
327
|
+
try {
|
|
328
|
+
await manager.keepAlive(sessionId, { pollMs: Number(options.pollInterval) * 1000 });
|
|
329
|
+
emit(ctx, { session_id: sessionId, status: "TERMINAL", lease: manager.getLease(sessionId) ?? null }, `Authoring Session ${sessionId} is terminal`);
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
deregister();
|
|
333
|
+
manager.unregister(sessionId);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
authoring.command("revise").argument("<sessionId>").argument("<scriptFile>").requiredOption("--source <source>").option("--base-revision-id <id>").requiredOption("--idempotency-key <key>").action(async function (sessionId, scriptFile, options) { const ctx = context(this); const parsed = await loadScript(scriptFile); const result = await withFreshSession(ctx, sessionId, (client) => client.authoring.createRevision(sessionId, parsed.script, options.source, options.idempotencyKey, options.baseRevisionId)); emit(ctx, result, `Authoring Revision ${result.id} created`); });
|
|
337
|
+
authoring.command("revision").argument("<sessionId>").argument("<revisionId>").action(async function (sessionId, revisionId) { const ctx = context(this); emit(ctx, await (await authenticatedClient(ctx)).client.authoring.getRevision(sessionId, revisionId)); });
|
|
338
|
+
authoring.command("observe").argument("<sessionId>").action(async function (sessionId) { const ctx = context(this); emit(ctx, await withFreshSession(ctx, sessionId, (client) => client.authoring.observe(sessionId))); });
|
|
339
|
+
authoring.command("draft-save").argument("<sessionId>").argument("<scriptFile>").requiredOption("--next-step-index <number>").option("--workspace <dir>", undefined, ".").option("--execution-mode <mode>").option("--attempt-id <id>").option("--attempt-status <status>").action(async function (sessionId, scriptFile, options) { const ctx = context(this); const content = (await loadScript(scriptFile)).content; const result = await new AuthoringDraftStore(options.workspace).checkpoint(sessionId, content, { nextStepIndex: Number(options.nextStepIndex), executionMode: options.executionMode, attemptId: options.attemptId, attemptStatus: options.attemptStatus }); emit(ctx, result, `Local Authoring checkpoint ${result.checkpoint} saved`); });
|
|
340
|
+
authoring.command("draft-resume").argument("<sessionId>").option("--workspace <dir>", undefined, ".").action(async function (sessionId, options) { const ctx = context(this); emit(ctx, await new AuthoringDraftStore(options.workspace).resume(sessionId)); });
|
|
341
|
+
authoring.command("drafts").option("--workspace <dir>", undefined, ".").action(async function (options) { const ctx = context(this); emit(ctx, await new AuthoringDraftStore(options.workspace).list()); });
|
|
342
|
+
authoring.command("draft-step").argument("<sessionId>").argument("<scriptFile>").argument("<stepIndex>").requiredOption("--idempotency-key <key>").option("--workspace <dir>", undefined, ".").option("--execution-mode <mode>").action(async function (sessionId, scriptFile, stepIndex, options) { const ctx = context(this); const parsed = await loadScript(scriptFile); const store = new AuthoringDraftStore(options.workspace); await store.checkpoint(sessionId, parsed.content, { nextStepIndex: Number(stepIndex), executionMode: options.executionMode, attemptStatus: "READY" }); const attempt = await withFreshSession(ctx, sessionId, (client) => client.authoring.executeDraftStep(sessionId, parsed.script, Number(stepIndex), options.idempotencyKey)); const checkpoint = await store.checkpoint(sessionId, parsed.content, { nextStepIndex: attempt.status === "PASSED" ? Number(stepIndex) + 1 : Number(stepIndex), executionMode: options.executionMode, attemptId: attempt.id, attemptStatus: attempt.status }); emit(ctx, { ...attempt, local_checkpoint: checkpoint }, `Draft Step Attempt ${attempt.id} status=${attempt.status}`); });
|
|
343
|
+
authoring.command("attempt").argument("<attemptId>").action(async function (attemptId) { const ctx = context(this); emit(ctx, await (await authenticatedClient(ctx)).client.authoring.getAttempt(attemptId)); });
|
|
344
|
+
authoring.command("complete").argument("<sessionId>").argument("<revisionId>").requiredOption("--idempotency-key <key>").action(async function (sessionId, revisionId, options) { const ctx = context(this); emit(ctx, await withFreshSession(ctx, sessionId, (client) => client.authoring.complete(sessionId, revisionId, options.idempotencyKey))); });
|
|
345
|
+
authoring.command("cancel").argument("<sessionId>").requiredOption("--idempotency-key <key>").action(async function (sessionId, options) { const ctx = context(this); emit(ctx, await (await authenticatedClient(ctx)).client.authoring.cancel(sessionId, options.idempotencyKey)); });
|
|
346
|
+
const device = program.command("device").description("Direct structured control of one Android device");
|
|
347
|
+
device.command("list").option("--device-id <id>").option("--keyword <text>").option("--platform <name>").option("--brand <name>").option("--model <name>").option("--os-version <version>").option("--resolution <value>").option("--min-sdk-version <version>").option("--available").option("--page <number>").option("--page-size <number>").action(async function (options) { const ctx = context(this); const query = { device_id: options.deviceId, keyword: options.keyword, platform: options.platform, brand: options.brand, model: options.model, os_version: options.osVersion, resolution: options.resolution, min_sdk_version: options.minSdkVersion, ...(options.available ? { available: true } : {}), page: options.page, page_size: options.pageSize }; emit(ctx, await (await authenticatedClient(ctx)).client.authoring.listDevices(query)); });
|
|
348
|
+
device.command("acquire").requiredOption("--device-id <id>").requiredOption("--idempotency-key <key>").action(async function (options) { const ctx = context(this); const result = await (await authenticatedClient(ctx)).client.action.acquireDevice(options.deviceId, options.idempotencyKey); emit(ctx, result, `Device Session ${result.session.id} acquired${billingText(result)}`); });
|
|
349
|
+
device.command("renew").argument("<sessionId>").action(async function (sessionId) { const ctx = context(this); const result = await (await authenticatedClient(ctx)).client.action.renewSession(sessionId); emit(ctx, result, `Device Session ${result.id} renewed until ${result.expires_at}${billingText(result)}`); });
|
|
350
|
+
device.command("keepalive").argument("<sessionId>").option("--poll-interval <seconds>", undefined, "30").action(async function (sessionId, options) {
|
|
351
|
+
const ctx = context(this);
|
|
352
|
+
const { client } = await authenticatedClient(ctx);
|
|
353
|
+
const manager = new SessionLeaseManager(client.action, { onEvent: (event) => emit(ctx, event, event.type === "renewed" ? `Device Session ${event.session_id} renewed until ${event.expires_at ?? "unknown"}` : undefined) });
|
|
354
|
+
const deregister = onInterrupt(async () => { manager.unregister(sessionId); });
|
|
355
|
+
try {
|
|
356
|
+
await manager.keepAlive(sessionId, { pollMs: Number(options.pollInterval) * 1000 });
|
|
357
|
+
emit(ctx, { session_id: sessionId, status: "TERMINAL", lease: manager.getLease(sessionId) ?? null }, `Device Session ${sessionId} is terminal`);
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
deregister();
|
|
361
|
+
manager.unregister(sessionId);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
device.command("observe").argument("<sessionId>").action(async function (sessionId) { const ctx = context(this); emit(ctx, await withFreshActionSession(ctx, sessionId, (client) => client.action.observe(sessionId))); });
|
|
365
|
+
device.command("action").argument("<sessionId>").requiredOption("--request-json <json>").option("--screen-id <id>").requiredOption("--idempotency-key <key>").action(async function (sessionId, options) { const ctx = context(this); const actionRequest = buildDeviceActionRequest({ screenId: options.screenId, requestJson: options.requestJson }); const result = await withFreshActionSession(ctx, sessionId, (client) => client.action.executeAction(sessionId, actionRequest, options.idempotencyKey)); emit(ctx, result, `Device Action ${result.action?.action} status=${result.status}${billingText(result)}`); });
|
|
366
|
+
device.command("release").argument("<sessionId>").requiredOption("--idempotency-key <key>").action(async function (sessionId, options) { const ctx = context(this); emit(ctx, await (await authenticatedClient(ctx)).client.action.release(sessionId, options.idempotencyKey)); });
|
|
367
|
+
const targets = program.command("targets");
|
|
368
|
+
targets.command("list").action(async function () { const ctx = context(this); const project = await loadProjectConfig(ctx.projectConfig); emit(ctx, { schema_version: PROJECT_SCHEMA_VERSION, config_path: project.path, default_target: project.defaultTarget, targets: project.listTargets().map((item) => ({ name: item.name, target_hash: item.targetHash, target: item.definition, default: item.name === project.defaultTarget })) }); });
|
|
369
|
+
targets.command("show").argument("<name>").action(async function (name) { const ctx = context(this); const project = await loadProjectConfig(ctx.projectConfig); const item = project.resolveTarget(name); emit(ctx, { schema_version: PROJECT_SCHEMA_VERSION, config_path: project.path, name: item.name, target_hash: item.targetHash, target: item.definition, default: name === project.defaultTarget }); });
|
|
370
|
+
targets.command("validate").argument("[name]").action(async function (name) { const ctx = context(this); const project = await loadProjectConfig(ctx.projectConfig); const selected = name ? [project.resolveTarget(name)] : project.listTargets(); emit(ctx, { valid: true, schema_version: PROJECT_SCHEMA_VERSION, config_path: project.path, targets: selected.map((item) => ({ name: item.name, target_hash: item.targetHash, target: item.definition })) }, `${project.path}: valid (${selected.length} Targets)`); });
|
|
371
|
+
targets.command("preview").argument("<name>").action(async function (name) { const ctx = context(this); const project = await loadProjectConfig(ctx.projectConfig); const item = project.resolveTarget(name); const { profile, client } = await authenticatedClient(ctx); requireProjectCi(profile, "Target preview"); emit(ctx, await client.previewTarget({ schema_version: "xagent-target-preview/v1", target_name: item.name, target_hash: item.targetHash, target: item.definition })); });
|
|
372
|
+
const apps = program.command("apps");
|
|
373
|
+
apps.command("list").option("--keyword <text>").option("--app-name <name>").option("--package-name <name>").option("--page <number>").option("--page-size <number>").action(async function (options) { const ctx = context(this); const query = { keyword: options.keyword, app_name: options.appName, package_name: options.packageName, page: options.page, page_size: options.pageSize }; const result = await (await authenticatedClient(ctx)).client.listApps(query); emit(ctx, result, `${result.total} application package(s)`); });
|
|
374
|
+
apps.command("show").argument("<packageId>").action(async function (packageId) { const ctx = context(this); const parsed = Number(packageId); if (!Number.isInteger(parsed) || parsed < 1)
|
|
375
|
+
throw new CliError("invalid_request", "package-id must be a positive integer"); const result = await (await authenticatedClient(ctx)).client.getApp(parsed); emit(ctx, result, `${result.package_id}: ${result.package_name}`); });
|
|
376
|
+
apps.command("upload").argument("<apkFile>").option("--upload-timeout <seconds>", undefined, "900").action(async function (apkFile, options) { const ctx = context(this); if (!apkFile.toLowerCase().endsWith(".apk"))
|
|
377
|
+
throw new CliError("invalid_request", "apps upload requires an APK file"); const { client } = await authenticatedClient(ctx, Number(options.uploadTimeout) * 1000); const result = await client.uploadApp(apkFile, (done, total) => (ctx.json ? process.stderr : process.stdout).write(`APK 分片上传:${done}/${total}\n`)); emit(ctx, result, `Uploaded ${apkFile}: package_id=${result.package_id} package=${result.app?.package_name}`); });
|
|
378
|
+
apps.command("release").argument("<appUploadId>").action(async function (id) { const ctx = context(this); const { client } = await authenticatedClient(ctx); const result = await client.releaseApp(id); emit(ctx, result, `App upload ${id} status=${result.status}`); });
|
|
379
|
+
program.command("run")
|
|
380
|
+
.argument("<scriptFile>")
|
|
381
|
+
.option("--package-id <id>")
|
|
382
|
+
.option("--app-upload-id <id>")
|
|
383
|
+
.option("--apk <path>")
|
|
384
|
+
.option("--device-id <id>")
|
|
385
|
+
.option("--target <name>")
|
|
386
|
+
.option("--idempotency-key <key>")
|
|
387
|
+
.option("--var <assignment>", "Runtime variable NAME=VALUE", (value, previous) => [...previous, value], [])
|
|
388
|
+
.option("--timeout-ms <milliseconds>", undefined, "600000")
|
|
389
|
+
.option("--wait")
|
|
390
|
+
.option("--wait-timeout <seconds>", undefined, "900")
|
|
391
|
+
.option("--upload-timeout <seconds>", undefined, "900")
|
|
392
|
+
.action(async function (scriptFile, options) {
|
|
393
|
+
const ctx = context(this);
|
|
394
|
+
const parsed = await loadScript(scriptFile);
|
|
395
|
+
const supplied = [options.packageId, options.appUploadId, options.apk].filter((item) => item !== undefined).length;
|
|
396
|
+
if (supplied > 1)
|
|
397
|
+
throw new CliError("invalid_request", "--package-id, --app-upload-id and --apk are mutually exclusive");
|
|
398
|
+
const script = structuredClone(parsed.script);
|
|
399
|
+
if (options.appUploadId || options.apk)
|
|
400
|
+
delete script.package_id;
|
|
401
|
+
const packageId = options.packageId ? Number(options.packageId) : script.package_id;
|
|
402
|
+
if (packageId !== undefined && (!Number.isInteger(Number(packageId)) || Number(packageId) < 1))
|
|
403
|
+
throw new CliError("invalid_request", "--package-id must be a positive integer");
|
|
404
|
+
const hasInstall = script.steps.some((step) => step.action === "install");
|
|
405
|
+
if (packageId === undefined && !options.appUploadId && !options.apk && hasInstall)
|
|
406
|
+
throw new CliError("invalid_request", "package_id or --app-upload-id is required when the script contains install");
|
|
407
|
+
const profile = await selectedProfile(ctx);
|
|
408
|
+
let request;
|
|
409
|
+
if (profile.data.expected_token_type === "personal") {
|
|
410
|
+
if (options.target)
|
|
411
|
+
throw new CliError("token_type_mismatch", "personal Profile cannot use --target", 3);
|
|
412
|
+
if (options.apk && !String(options.apk).toLowerCase().endsWith(".apk"))
|
|
413
|
+
throw new CliError("invalid_request", "--apk requires an APK file");
|
|
414
|
+
if (!options.deviceId)
|
|
415
|
+
throw new CliError("invalid_request", "--device-id is required for a personal Profile");
|
|
416
|
+
if (options.idempotencyKey)
|
|
417
|
+
throw new CliError("invalid_request", "--idempotency-key is only valid for a project_ci Profile");
|
|
418
|
+
request = { schema_version: "xagent-run-create/v1", script, device_id: options.deviceId, timeout_ms: Number(options.timeoutMs), source_hash: parsed.sourceHash };
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
if (options.apk)
|
|
422
|
+
throw new CliError("token_type_mismatch", "project_ci Profile must use apps upload and --app-upload-id", 3);
|
|
423
|
+
if (options.deviceId)
|
|
424
|
+
throw new CliError("token_type_mismatch", "project_ci Profile cannot use --device-id", 3);
|
|
425
|
+
if (!String(options.idempotencyKey ?? "").trim())
|
|
426
|
+
throw new CliError("invalid_request", "--idempotency-key is required for a project_ci Run");
|
|
427
|
+
const target = (await loadProjectConfig(ctx.projectConfig)).resolveTarget(options.target);
|
|
428
|
+
request = { schema_version: "xagent-run-create/v2", script, target_name: target.name, target_hash: target.targetHash, target: target.definition, timeout_ms: Number(options.timeoutMs), source_hash: parsed.sourceHash };
|
|
429
|
+
}
|
|
430
|
+
const variables = parseVariables(options.var ?? []);
|
|
431
|
+
if (Object.keys(variables).length)
|
|
432
|
+
request.variables = variables;
|
|
433
|
+
const { client } = await authenticatedClient(ctx, Number(options.uploadTimeout) * 1000);
|
|
434
|
+
let uploadedId;
|
|
435
|
+
let primaryError;
|
|
436
|
+
let run;
|
|
437
|
+
// Ctrl+C would otherwise skip the finally below and leave both the APK
|
|
438
|
+
// and a Run occupying a device allocated until each expires on its own.
|
|
439
|
+
let createdRunId;
|
|
440
|
+
const waiting = new AbortController();
|
|
441
|
+
const releaseOnInterrupt = onInterrupt(async () => {
|
|
442
|
+
// Stop the poll loop first so it cannot keep issuing requests while we release.
|
|
443
|
+
waiting.abort();
|
|
444
|
+
// Both releases are independent: a failed cancel must not skip the APK release.
|
|
445
|
+
const report = (what, error) => process.stderr.write(`${what} failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
446
|
+
if (createdRunId) {
|
|
447
|
+
try {
|
|
448
|
+
await client.cancelRun(createdRunId);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
report(`Cancel run ${createdRunId}`, error);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (uploadedId) {
|
|
455
|
+
try {
|
|
456
|
+
await client.releaseApp(uploadedId);
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
report(`Release app upload ${uploadedId}`, error);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
try {
|
|
464
|
+
if (options.apk) {
|
|
465
|
+
const uploaded = await client.uploadApp(options.apk, (done, total) => (ctx.json ? process.stderr : process.stdout).write(`APK 分片上传:${done}/${total}\n`));
|
|
466
|
+
uploadedId = String(uploaded.id ?? "");
|
|
467
|
+
if (!uploadedId)
|
|
468
|
+
throw new XAgentApiError("invalid_response", "APK upload did not return an upload ID");
|
|
469
|
+
request.app_upload_id = uploadedId;
|
|
470
|
+
}
|
|
471
|
+
else if (packageId !== undefined)
|
|
472
|
+
request.package_id = Number(packageId);
|
|
473
|
+
if (options.appUploadId)
|
|
474
|
+
request.app_upload_id = options.appUploadId;
|
|
475
|
+
run = await client.createRun(request, options.idempotencyKey?.trim());
|
|
476
|
+
createdRunId = String(run.id ?? "") || undefined;
|
|
477
|
+
if (options.wait) {
|
|
478
|
+
run = await client.waitRun(run.id, Number(options.waitTimeout), 2, progressReporter(ctx), waiting.signal);
|
|
479
|
+
createdRunId = undefined; // reached a terminal state, nothing left to cancel
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
primaryError = error;
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
if (uploadedId) {
|
|
488
|
+
const releasing = uploadedId;
|
|
489
|
+
// Cleared only on success so an interrupt mid-release still retries it.
|
|
490
|
+
try {
|
|
491
|
+
await client.releaseApp(releasing);
|
|
492
|
+
uploadedId = undefined;
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
if (!primaryError)
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
releaseOnInterrupt();
|
|
500
|
+
}
|
|
501
|
+
emitRun(ctx, run);
|
|
502
|
+
});
|
|
503
|
+
const runs = program.command("runs");
|
|
504
|
+
runs.command("show").argument("<runId>").action(async function (runId) { const ctx = context(this); emitRun(ctx, await (await authenticatedClient(ctx)).client.getRun(runId), false); });
|
|
505
|
+
runs.command("wait").argument("<runId>").option("--timeout <seconds>", undefined, "900").option("--poll-interval <seconds>", undefined, "2").action(async function (runId, options) { const ctx = context(this); const run = await (await authenticatedClient(ctx)).client.waitRun(runId, Number(options.timeout), Number(options.pollInterval), progressReporter(ctx)); emitRun(ctx, run); });
|
|
506
|
+
runs.command("cancel").argument("<runId>").action(async function (runId) { const ctx = context(this); emitRun(ctx, await (await authenticatedClient(ctx)).client.cancelRun(runId), false); });
|
|
507
|
+
program.command("mcp").option("--mode <mode>", `Isolated Tool surface: ${MCP_MODES.join(", ")}`, "action").action(async function (options) { const ctx = context(this); if (!MCP_MODES.includes(options.mode))
|
|
508
|
+
throw new CliError("invalid_request", `mcp --mode must be one of: ${MCP_MODES.join(", ")}`); await runStdioServer(ctx.profile, ctx.userConfig, undefined, options.mode); });
|
|
509
|
+
export async function runCli(argv = process.argv) {
|
|
510
|
+
try {
|
|
511
|
+
await program.parseAsync(argv);
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
fail(error);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function isCliEntryPoint() {
|
|
518
|
+
if (!process.argv[1])
|
|
519
|
+
return false;
|
|
520
|
+
try {
|
|
521
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (isCliEntryPoint())
|
|
528
|
+
await runCli();
|
|
529
|
+
//# sourceMappingURL=cli.js.map
|