@itpay/cli 2.1.41 → 2.1.44
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 +12 -5
- package/assets/sell-preview/index.html +6 -0
- package/assets/sell-preview/preview.js +9 -0
- package/bin/darwin-amd64/itpay-sell +0 -0
- package/bin/darwin-arm64/itpay-sell +0 -0
- package/bin/linux-amd64/itpay-sell +0 -0
- package/bin/linux-arm64/itpay-sell +0 -0
- package/bin/windows-amd64/itpay-sell.exe +0 -0
- package/dist/src/client/backend.js +38 -2
- package/dist/src/client/http.js +2 -0
- package/dist/src/commands/aftercare.js +12 -0
- package/dist/src/commands/buy.js +17 -58
- package/dist/src/commands/cart.js +17 -15
- package/dist/src/commands/catalog.js +39 -14
- package/dist/src/commands/checkout.js +146 -5
- package/dist/src/commands/checkout_handoff.js +11 -61
- package/dist/src/commands/docs.js +7 -18
- package/dist/src/commands/guidance.js +1 -134
- package/dist/src/commands/install.js +6 -3
- package/dist/src/commands/order.js +34 -3
- package/dist/src/commands/orders.js +4 -9
- package/dist/src/commands/pay.js +1 -1
- package/dist/src/commands/presentation.js +153 -0
- package/dist/src/commands/rail_catalog.js +1 -0
- package/dist/src/commands/readyz.js +1 -1
- package/dist/src/commands/refund.js +13 -57
- package/dist/src/commands/services/actions.js +133 -0
- package/dist/src/commands/services/commerce.js +197 -0
- package/dist/src/commands/services/lifecycle.js +418 -0
- package/dist/src/commands/services/results.js +130 -0
- package/dist/src/commands/services.js +14 -1068
- package/dist/src/commands/skill.js +8 -5
- package/dist/src/commands/vault.js +27 -20
- package/dist/src/commands/vault_handoff.js +18 -0
- package/dist/src/main.js +332 -46
- package/dist/src/render/browser.js +65 -0
- package/dist/src/render/checkout_handoff.js +101 -0
- package/dist/src/render/checkout_plan.js +47 -0
- package/dist/src/render/markdown.js +0 -3
- package/dist/src/render/qr.js +1 -1
- package/dist/src/sell/commands.js +80 -0
- package/dist/src/sell/contract.js +86 -0
- package/dist/src/sell/local.js +260 -0
- package/dist/src/sell/mcp.js +49 -0
- package/dist/src/sell/preview.js +70 -0
- package/dist/src/sell/sync.js +145 -0
- package/dist/src/services/adapter.js +1 -0
- package/dist/src/services/presentation.js +198 -0
- package/dist/src/services/rail/actions.js +51 -0
- package/dist/src/services/rail/adapter.js +36 -0
- package/dist/src/services/rail/aftercare.js +45 -0
- package/dist/src/services/rail/catalog.js +261 -0
- package/dist/src/services/rail/checkout.js +19 -0
- package/dist/src/services/rail/fulfillment.js +76 -0
- package/dist/src/services/rail/guide.js +110 -0
- package/dist/src/services/rail/invocation.js +54 -0
- package/dist/src/services/rail/page.js +76 -0
- package/dist/src/services/rail/pagination.js +6 -0
- package/dist/src/services/rail/query.js +71 -0
- package/dist/src/services/rail/readers.js +107 -0
- package/dist/src/services/rail/reading.js +125 -0
- package/dist/src/services/rail/recovery.js +51 -0
- package/dist/src/services/rail/results.js +378 -0
- package/dist/src/services/rail/review.js +268 -0
- package/dist/src/services/rail/saved.js +49 -0
- package/dist/src/services/rail/wait.js +11 -0
- package/dist/src/services/registry.js +31 -0
- package/dist/src/services/schema.js +21 -0
- package/dist/src/services/shared/envelope.js +184 -0
- package/dist/src/services/shared/input.js +39 -0
- package/dist/src/services/shared/pagination.js +28 -0
- package/dist/src/services/shared/platform.js +365 -0
- package/dist/src/services/shared/refund.js +40 -0
- package/dist/src/services/shared/workflow.js +28 -0
- package/dist/src/state/account_auth.js +354 -0
- package/dist/src/state/cart_session.js +4 -1
- package/dist/src/state/client_context.js +72 -0
- package/dist/src/state/config.js +44 -14
- package/dist/src/state/device_authority.js +402 -72
- package/dist/src/state/task_journal.js +90 -0
- package/docs/agent/buyer/cart-checkout.json +8 -6
- package/docs/agent/buyer/catalog-list.json +29 -9
- package/docs/agent/buyer/identity-and-sessions.json +26 -9
- package/docs/agent/buyer/install-and-setup.json +17 -15
- package/docs/agent/buyer/orders-refunds.json +25 -9
- package/docs/agent/buyer/payment-flow.json +6 -4
- package/docs/agent/buyer/purchased-content.json +18 -4
- package/docs/agent/buyer/quickstart.json +26 -17
- package/docs/agent/buyer/rail-booking.json +176 -0
- package/docs/agent/buyer/rail-fast-checkout.json +48 -0
- package/docs/agent/buyer/render-hosts.json +23 -12
- package/docs/agent/buyer/service-feedback.json +4 -3
- package/docs/cli-reference/agent-types.md +7 -2
- package/docs/cli-reference/commands/aftercare.md +15 -0
- package/docs/cli-reference/commands/auth.md +96 -0
- package/docs/cli-reference/commands/buy.md +2 -1
- package/docs/cli-reference/commands/cart/add.md +5 -1
- package/docs/cli-reference/commands/cart/clear.md +1 -1
- package/docs/cli-reference/commands/cart/index.md +1 -1
- package/docs/cli-reference/commands/cart/next.md +5 -1
- package/docs/cli-reference/commands/cart/remove.md +1 -1
- package/docs/cli-reference/commands/cart/show.md +1 -1
- package/docs/cli-reference/commands/catalog/index.md +1 -1
- package/docs/cli-reference/commands/catalog/list.md +2 -2
- package/docs/cli-reference/commands/checkout.md +12 -2
- package/docs/cli-reference/commands/device.md +18 -11
- package/docs/cli-reference/commands/docs/index.md +1 -1
- package/docs/cli-reference/commands/docs/list.md +1 -1
- package/docs/cli-reference/commands/docs/search.md +1 -1
- package/docs/cli-reference/commands/docs/show.md +1 -1
- package/docs/cli-reference/commands/install.md +8 -6
- package/docs/cli-reference/commands/next.md +1 -1
- package/docs/cli-reference/commands/order.md +1 -1
- package/docs/cli-reference/commands/pay.md +1 -1
- package/docs/cli-reference/commands/readyz.md +11 -11
- package/docs/cli-reference/commands/refund/cancel.md +1 -1
- package/docs/cli-reference/commands/refund/create.md +1 -1
- package/docs/cli-reference/commands/refund/get.md +2 -2
- package/docs/cli-reference/commands/refund/index.md +1 -1
- package/docs/cli-reference/commands/refund/list.md +1 -1
- package/docs/cli-reference/commands/refund/watch.md +1 -1
- package/docs/cli-reference/commands/sell.md +1364 -0
- package/docs/cli-reference/commands/services/action.md +24 -3
- package/docs/cli-reference/commands/services/checkout.md +17 -2
- package/docs/cli-reference/commands/services/events.md +1 -1
- package/docs/cli-reference/commands/services/get.md +4 -2
- package/docs/cli-reference/commands/services/index.md +4 -3
- package/docs/cli-reference/commands/services/invoke.md +6 -4
- package/docs/cli-reference/commands/services/list.md +1 -1
- package/docs/cli-reference/commands/services/next.md +78 -12
- package/docs/cli-reference/commands/services/page.md +71 -0
- package/docs/cli-reference/commands/services/quote.md +4 -4
- package/docs/cli-reference/commands/services/read-result.md +17 -6
- package/docs/cli-reference/commands/services/run.md +152 -0
- package/docs/cli-reference/commands/services/start.md +5 -3
- package/docs/cli-reference/commands/vault/access.md +5 -1
- package/docs/cli-reference/conventions.md +4 -6
- package/docs/cli-reference/index.md +10 -1
- package/docs/sell.md +84 -0
- package/package.json +20 -12
- package/skills/itpay/SKILL.md +112 -102
- package/docs/skill-bundle-rollout/01-mcp-authentication.md +0 -134
- package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +0 -210
- package/docs/skill-bundle-rollout/03-platform-publishing.md +0 -161
- package/docs/skill-bundle-rollout/04-first-wave-platforms.md +0 -69
- package/docs/skill-bundle-rollout/05-sync-operations.md +0 -62
- package/docs/skill-bundle-rollout/README.md +0 -151
- /package/bin/{itp → itp.mjs} +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { buildOpenClawTelegramAction } from "./telegram.js";
|
|
2
|
+
export function shouldPrepareLocalCheckoutImage(platform) {
|
|
3
|
+
return platform === "markdown";
|
|
4
|
+
}
|
|
5
|
+
export function isWorkBuddyPlainChat(agentType, platform) {
|
|
6
|
+
return agentType?.trim().toLowerCase() === "workbuddy" && platform === "plain_chat";
|
|
7
|
+
}
|
|
8
|
+
export function isZCodePlainChat(agentType, platform) {
|
|
9
|
+
return agentType?.trim().toLowerCase() === "zcode" && platform === "plain_chat";
|
|
10
|
+
}
|
|
11
|
+
export function isLinkOnlyBrowserAgent(agentType, platform) {
|
|
12
|
+
return isWorkBuddyPlainChat(agentType, platform) || isZCodePlainChat(agentType, platform);
|
|
13
|
+
}
|
|
14
|
+
export function buildWorkBuddyPresentFilesAction(url) {
|
|
15
|
+
return {
|
|
16
|
+
tool: "present_files",
|
|
17
|
+
arguments: { files: [url] },
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function hasCompleteCheckoutTokens(url) {
|
|
21
|
+
if (!url) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const parsed = new URL(url);
|
|
26
|
+
return Boolean(parsed.searchParams.get("display_token") && parsed.searchParams.get("complete_exchange_token"));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function buildCheckoutHandoff(input) {
|
|
33
|
+
const url = input.plan?.localSimulation && input.mobileUrl && hasCompleteCheckoutTokens(input.mobileUrl) ? input.mobileUrl : input.url;
|
|
34
|
+
const handoff = { url, ...(input.plan?.localSimulation ? { environment: "local_simulation", query: "live", transactions: "simulated" } : {}) };
|
|
35
|
+
// mobile_url is only emitted when the link carries the complete token pair;
|
|
36
|
+
// a display-token-only link cannot finish the same-device exchange after payment.
|
|
37
|
+
const mobileUrl = !input.plan?.localSimulation && hasCompleteCheckoutTokens(input.mobileUrl) ? input.mobileUrl : undefined;
|
|
38
|
+
if (mobileUrl) {
|
|
39
|
+
handoff.mobile_url = mobileUrl;
|
|
40
|
+
}
|
|
41
|
+
const workBuddyAction = isWorkBuddyPlainChat(input.agentType, input.platform);
|
|
42
|
+
if (workBuddyAction) {
|
|
43
|
+
handoff.agent_action = buildWorkBuddyPresentFilesAction(url);
|
|
44
|
+
}
|
|
45
|
+
if (input.platform === "markdown") {
|
|
46
|
+
if (input.localPath)
|
|
47
|
+
handoff.qr_local_path = input.localPath;
|
|
48
|
+
if (input.markdown)
|
|
49
|
+
handoff.markdown = input.markdown;
|
|
50
|
+
}
|
|
51
|
+
else if (input.platform === "plain_chat" && input.qrImageURL && !isLinkOnlyBrowserAgent(input.agentType, input.platform)) {
|
|
52
|
+
handoff.qr_image_url = input.qrImageURL;
|
|
53
|
+
}
|
|
54
|
+
else if (input.platform === "telegram" && input.qrImageURL) {
|
|
55
|
+
handoff.qr_image_url = input.qrImageURL;
|
|
56
|
+
}
|
|
57
|
+
if (input.agentType?.trim().toLowerCase() === "openclaw" && input.platform === "telegram" && input.plan && input.target) {
|
|
58
|
+
handoff.agent_action = buildOpenClawTelegramAction(input.plan, input.target);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
handoff,
|
|
62
|
+
instruction: input.plan?.localSimulation
|
|
63
|
+
? `本地实验:铁路与地图查询真实,授权、通知和交易模拟,不会扣款。在本机打开 handoff.url 模拟付款入口,金额 ${input.amount};仅本机桌面可达。${workBuddyAction ? "按 handoff.agent_action 原样打开一次。" : "展示原入口。"}等待用户操作后读取同一 Checkout;不创建替代订单,用户声明不证明模拟付款已确认。`
|
|
64
|
+
: `先展示官方入口,再做非必要记忆或日志;保留必需安全检查和宿主权限。${checkoutHandoffInstruction(input.agentType, input.platform, input.amount, Boolean(mobileUrl))}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function checkoutHandoffInstruction(agentType, platform, amount, hasMobileLink) {
|
|
68
|
+
if (isWorkBuddyPlainChat(agentType, platform)) {
|
|
69
|
+
const mobileSentence = hasMobileLink
|
|
70
|
+
? ",然后把 handoff.mobile_url 作为可点击链接发给用户,说明“手机端点开可直接跳转支付宝或微信支付;小程序内打不开时复制到手机浏览器打开即可”"
|
|
71
|
+
: "";
|
|
72
|
+
const failLinks = hasMobileLink ? "发送 handoff.url 与 handoff.mobile_url" : "发送原始 handoff.url";
|
|
73
|
+
return `Backend 尚未确认付款。立即严格按 handoff.agent_action.tool 和 handoff.agent_action.arguments 原样执行一次,在右侧打开 handoff.url;确认工具调用成功后说明金额 ${amount}${mobileSentence},然后停止等待。若工具失败,只${failLinks},报告未自动打开并停止。不要用 present_files 打开本地文件或二维码 PNG,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
74
|
+
}
|
|
75
|
+
if (isZCodePlainChat(agentType, platform)) {
|
|
76
|
+
const mobileSentence = hasMobileLink
|
|
77
|
+
? ",并把 handoff.mobile_url 发给用户备用(手机端点开可直接跳转支付宝或微信支付)"
|
|
78
|
+
: "";
|
|
79
|
+
const fallbackLinks = hasMobileLink ? "同一个可点击 handoff.url 与 handoff.mobile_url" : "同一个可点击 handoff.url";
|
|
80
|
+
return `Backend 尚未确认付款。立即用 ZCode 内置浏览器打开 handoff.url,让用户完成付款;确认已发起打开后说明金额 ${amount}${mobileSentence},然后停止等待。不要只粘贴文字链接,不要下载、解析或重建二维码,不要创建新 Checkout、Payment Intent 或 Execution。只有内置浏览器明确不可用时,才展示${fallbackLinks}。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
81
|
+
}
|
|
82
|
+
if (agentType?.trim().toLowerCase() === "doubao-work" && platform === "plain_chat") {
|
|
83
|
+
const directLink = hasMobileLink ? "handoff.mobile_url" : "handoff.url";
|
|
84
|
+
const directLabel = hasMobileLink ? "手机直接打开收银台(可跳转支付宝/微信)" : "手机直接打开收银台";
|
|
85
|
+
return `Backend 尚未确认付款。在当前豆包工作对话中展示两个有标签的官方入口:将 ${directLink} 标为“${directLabel}”,将 handoff.qr_image_url 标为“二维码图片(保存或用另一台设备扫码)”。说明金额 ${amount} 后停止等待。不要解析或单独输出 URL credential,不要下载或重建二维码,不要调用 pay,不要创建新 Checkout、Payment Intent 或 Execution。只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
86
|
+
}
|
|
87
|
+
if (platform === "markdown") {
|
|
88
|
+
const mobileSentence = hasMobileLink ? ",并附 handoff.mobile_url 供手机端点击直达支付" : "";
|
|
89
|
+
return `Backend 尚未确认付款。把 handoff.markdown 原样发送到当前桌面对话,确认二维码、付款链接和金额 ${amount} 均已实际对用户可见${mobileSentence},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
90
|
+
}
|
|
91
|
+
if (platform === "terminal") {
|
|
92
|
+
const mobileSentence = hasMobileLink ? ",并展示 handoff.mobile_url 供手机端点击直达支付" : "";
|
|
93
|
+
return `Backend 尚未确认付款。在用户可见终端展示当前 Checkout 的二维码、handoff.url 和金额 ${amount}${mobileSentence},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
94
|
+
}
|
|
95
|
+
if (agentType?.trim().toLowerCase() === "openclaw" && platform === "telegram") {
|
|
96
|
+
const failLinks = hasMobileLink ? "handoff.qr_image_url、金额、handoff.url 与 handoff.mobile_url" : "handoff.qr_image_url、金额和 handoff.url";
|
|
97
|
+
return `Backend 尚未确认付款或授权状态。立即使用 OpenClaw 原生 message tool,严格按 handoff.agent_action.tool 和 handoff.agent_action.arguments 原样执行;它会发送金额 ${amount}、二维码、📱 手机点这儿支付和 📋 已授权给我读。不得改写 presentation、换用其他消息工具、拆开按钮发送或用普通回复声称按钮已经发送。确认 message tool 调用成功后停止等待。收到 callback_data "itp:grant_confirmed:<checkout_id>" 时,表示用户声明已在收银台授权读取;立即执行 next.command 查询同一 Checkout,再只执行 Backend 返回的 next.command 进入同一 Execution 的 grant 流程。该 callback 不证明付款成功或 grant 已生效;Backend 未返回 grant_active 前不得读取或猜测结果。若原生 message tool 明确失败或当前 Telegram 未启用 inline buttons,只发送现有 ${failLinks},报告按钮不可用并停止;不要创建新的 Checkout、Payment Intent 或 Execution。`;
|
|
98
|
+
}
|
|
99
|
+
const urlList = hasMobileLink ? "handoff.url、handoff.mobile_url(手机端点击直达支付)和可用的 handoff.qr_image_url" : "handoff.url 和可用的 handoff.qr_image_url";
|
|
100
|
+
return `Backend 尚未确认付款。把 ${urlList} 实际发送到当前会话,说明金额 ${amount},然后停止等待。不要创建新 Checkout、Payment Intent 或 Execution;只有用户明确表示已付款或要求查询状态时,才执行 next.command;用户的话不是付款成功证明。`;
|
|
101
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { platformKeyForHost } from "./plan.js";
|
|
2
|
+
import { isLinkOnlyBrowserAgent } from "./checkout_handoff.js";
|
|
3
|
+
import { qualifyItPayCommand } from "../state/agent_type.js";
|
|
4
|
+
// --- checkout QR plan ---
|
|
5
|
+
export function buildCheckoutQRPlan(input) {
|
|
6
|
+
const summary = input.localSimulation ? "在本机打开模拟付款入口;铁路与地图查询真实,授权、通知和交易模拟,不会扣款。" : `Scan the QR or open ${input.checkoutURL} to start the human checkout flow.`;
|
|
7
|
+
const isPayment = input.paymentIntentID != null;
|
|
8
|
+
const afterCommand = qualifyItPayCommand(`itpay checkout --id ${input.checkoutID} --token ${input.displayToken}${input.locale === "en" ? " --locale en" : ""} --json`, input.agentType);
|
|
9
|
+
const platform = {
|
|
10
|
+
text: summary,
|
|
11
|
+
links: [
|
|
12
|
+
{ label: "打开付款页面", url: input.checkoutURL },
|
|
13
|
+
],
|
|
14
|
+
buttons: [
|
|
15
|
+
{ label: "打开收银台", kind: "url", url: input.checkoutURL },
|
|
16
|
+
...(input.checkoutID
|
|
17
|
+
? [{ label: "查询 Checkout 状态", kind: "callback", intent: "check_checkout_status", ref: input.checkoutID }]
|
|
18
|
+
: []),
|
|
19
|
+
],
|
|
20
|
+
blocks: [],
|
|
21
|
+
...(input.qrPNGURL ? { media: [{ url: input.qrPNGURL, label: "Branded QR", mimeType: "image/png" }] } : {}),
|
|
22
|
+
};
|
|
23
|
+
const plan = {
|
|
24
|
+
kind: isPayment ? "payment_qr" : "checkout_qr",
|
|
25
|
+
...(input.localSimulation ? { localSimulation: true } : {}),
|
|
26
|
+
host: input.host,
|
|
27
|
+
summary,
|
|
28
|
+
url: input.qrPayload,
|
|
29
|
+
...(isLinkOnlyBrowserAgent(input.agentType, platformKeyForHost(input.host)) && input.cardURL ? { linkOnlyURL: input.cardURL } : {}),
|
|
30
|
+
preferredQRSources: [input.qrPNGURL ?? input.qrPayload],
|
|
31
|
+
checkoutID: input.checkoutID,
|
|
32
|
+
platform,
|
|
33
|
+
afterActionCommand: afterCommand,
|
|
34
|
+
afterActionLabel: "扫码或点击链接完成支付后,执行以下命令查询状态:",
|
|
35
|
+
};
|
|
36
|
+
if (input.orderItems)
|
|
37
|
+
plan.orderItems = input.orderItems;
|
|
38
|
+
if (input.orderCurrency)
|
|
39
|
+
plan.orderCurrency = input.orderCurrency;
|
|
40
|
+
if (input.paymentMethod)
|
|
41
|
+
plan.paymentMethod = input.paymentMethod;
|
|
42
|
+
if (input.paymentStatus)
|
|
43
|
+
plan.paymentStatus = input.paymentStatus;
|
|
44
|
+
if (input.paymentIntentID)
|
|
45
|
+
plan.paymentIntentID = input.paymentIntentID;
|
|
46
|
+
return plan;
|
|
47
|
+
}
|
|
@@ -24,9 +24,6 @@ export function buildAgentChatHandoff(plan) {
|
|
|
24
24
|
if (plan.orderItems?.length) {
|
|
25
25
|
lines.push(`项目:${plan.orderItems.map((item) => `${item.title} x${item.quantity}`).join("、")}`);
|
|
26
26
|
}
|
|
27
|
-
if (plan.afterActionCommand) {
|
|
28
|
-
lines.push("", `付款后查询:\`${plan.afterActionCommand}\``);
|
|
29
|
-
}
|
|
30
27
|
return {
|
|
31
28
|
type: "send_payment_handoff",
|
|
32
29
|
must_send_to_user: true,
|
package/dist/src/render/qr.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function renderTerminalQR(url, format) {
|
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
export async function writeLocalPNG(url) {
|
|
23
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "itpay-v3-"));
|
|
23
|
+
const dir = await fs.mkdtemp(path.join(process.env.ITPAY_STATE_DIR || os.tmpdir(), "itpay-v3-"));
|
|
24
24
|
const filePath = path.join(dir, `itpay-${randomUUID()}.png`);
|
|
25
25
|
await QRCode.toFile(filePath, url, {
|
|
26
26
|
type: "png",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { sellerAuth } from "../state/account_auth.js";
|
|
2
|
+
import { registerSync } from "./sync.js";
|
|
3
|
+
import { preview } from "./preview.js";
|
|
4
|
+
import { serveSellMCP } from "./mcp.js";
|
|
5
|
+
import { registerLocal } from "./local.js";
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { loadConfig, newBackendClient } from '../state/config.js';
|
|
8
|
+
import { SELL_GUIDE, SELL_OPERATIONS, sellRequest } from './contract.js';
|
|
9
|
+
export function registerSell(program) {
|
|
10
|
+
const sell = program.command('sell').description('Create, test and submit your service for review');
|
|
11
|
+
const auth = sell.command('auth').description('Authorize this CLI using normal ItPay account login');
|
|
12
|
+
for (const action of ['login', 'status', 'logout'])
|
|
13
|
+
auth.command(action).action(async () => {
|
|
14
|
+
process.stdout.write(JSON.stringify(await sellerAuth(action, loadConfig().baseURL)) + '\n');
|
|
15
|
+
});
|
|
16
|
+
const groups = new Map([['', sell]]);
|
|
17
|
+
for (const operation of SELL_OPERATIONS) {
|
|
18
|
+
const parts = operation.command.split(' ');
|
|
19
|
+
const leaf = parts.pop();
|
|
20
|
+
let parent = sell, key = '';
|
|
21
|
+
for (const part of parts) {
|
|
22
|
+
key = key ? `${key} ${part}` : part;
|
|
23
|
+
let group = groups.get(key);
|
|
24
|
+
if (!group) {
|
|
25
|
+
group = parent.command(part);
|
|
26
|
+
groups.set(key, group);
|
|
27
|
+
}
|
|
28
|
+
parent = group;
|
|
29
|
+
}
|
|
30
|
+
const command = parent.command(leaf).description(operation.description).option('--json', 'Structured output').option('--input-json <file>', 'Request fields as JSON');
|
|
31
|
+
const required = [...operation.path.matchAll(/\{(\w+)\}/g)].map(m => m[1]);
|
|
32
|
+
for (const field of required)
|
|
33
|
+
command.option(`--${field.replaceAll('_', '-')} <value>`, field);
|
|
34
|
+
if (operation.confirmation)
|
|
35
|
+
command.option('--confirm', 'User explicitly confirmed the disclosed version/action');
|
|
36
|
+
command.action(async (options) => {
|
|
37
|
+
try {
|
|
38
|
+
if (operation.command === 'guide' && !options.merchantId) {
|
|
39
|
+
process.stdout.write(JSON.stringify({ ...SELL_GUIDE, operations: SELL_OPERATIONS }, null, 2) + '\n');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (operation.confirmation && !options.confirm)
|
|
43
|
+
throw new Error('Explicit user confirmation required after reviewing this action; pass --confirm only after user approval');
|
|
44
|
+
const input = options.inputJson ? JSON.parse(readFileSync(String(options.inputJson), 'utf8')) : {};
|
|
45
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
46
|
+
throw new Error('Input JSON must be an object');
|
|
47
|
+
const params = {};
|
|
48
|
+
for (const field of required)
|
|
49
|
+
params[field] = options[field.replace(/_([a-z])/g, (_, c) => c.toUpperCase())];
|
|
50
|
+
const request = sellRequest(operation, params, input);
|
|
51
|
+
const result = await newBackendClient(loadConfig()).sellRequest(request);
|
|
52
|
+
process.stdout.write(JSON.stringify({ status: 'ok', operation: operation.command, result: result ?? null }, null, 2) + '\n');
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
process.stdout.write(JSON.stringify({ status: 'error', operation: operation.command, message: error instanceof Error ? error.message : 'Sell operation failed', next: 'itpay sell guide --json' }, null, 2) + '\n');
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
registerLocal(sell);
|
|
61
|
+
registerSync(sell);
|
|
62
|
+
sell.commands.find(command => command.name() === "workflow").command("preview").option("--project <directory>", "Project directory", ".").action(async (options) => { const result = await preview(options.project); process.stdout.write(JSON.stringify({ url: result.url, instruction: "Open this local URL to inspect the saved workflow" }) + "\n"); });
|
|
63
|
+
sell.command("mcp").description("Serve local Seller MCP over stdio").requiredOption("--stdio").option("--project <directory>", "Project directory", ".").action(async (options) => { await serveSellMCP(options.project); });
|
|
64
|
+
const submission = sell.commands.find(command => command.name() === "submission");
|
|
65
|
+
submission.command("watch").requiredOption("--merchant-id <id>").requiredOption("--submission-id <id>").option("--timeout <seconds>", "Maximum wait", "120").option("--json").action(async (options) => {
|
|
66
|
+
const timeout = Number(options.timeout);
|
|
67
|
+
if (!Number.isFinite(timeout) || timeout < 0 || timeout > 600)
|
|
68
|
+
throw new Error("timeout must be between 0 and 600");
|
|
69
|
+
const backend = newBackendClient(loadConfig()), until = Date.now() + timeout * 1000;
|
|
70
|
+
for (;;) {
|
|
71
|
+
const result = await backend.sellRequest({ method: "GET", path: "/v1/seller/organizations/" + encodeURIComponent(options.merchantId) + "/service-submissions/" + encodeURIComponent(options.submissionId) });
|
|
72
|
+
if (!["submitted", "pending", "under_review"].includes(result.status) || Date.now() >= until) {
|
|
73
|
+
process.stdout.write(JSON.stringify({ status: result.status, result }) + "\n");
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return sell;
|
|
80
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const org = "/v1/seller/organizations/{merchant_id}";
|
|
2
|
+
const draft = `${org}/service-drafts/{draft_id}`;
|
|
3
|
+
const read = "itpay.seller.read", write = "itpay.seller.write", test = "itpay.seller.test", submit = "itpay.seller.submit";
|
|
4
|
+
export const SELL_GUIDE = {
|
|
5
|
+
version: "itpay.sell.v1", command: "itpay sell", truth: "workflow.yaml",
|
|
6
|
+
steps: ["auth", "setup", "sources", "workflow", "local_test", "confirm", "push", "platform_verification", "pricing", "submission", "publication"],
|
|
7
|
+
rules: [
|
|
8
|
+
"Use the authenticated user's existing merchant. KYB and payout changes remain in the dashboard.",
|
|
9
|
+
"Resolve every API against its declared method, Content-Type, parameters, response schema and locked version. Ask for missing inputs; never guess credentials or change contracts.",
|
|
10
|
+
"Only saved versions may run. A run is not a saved version. Local evidence cannot grant platform test approval.",
|
|
11
|
+
"Use the platform node catalog and validator for supported pricing, quotas, confirmation and durable fulfillment. Every paid success path must cross payment and delivery gates.",
|
|
12
|
+
"Use own credentials for local requests. Platform-managed credentials stay on the platform. Mark execution location honestly.",
|
|
13
|
+
"Present side effects before live tests. Unknown results stop automatic retries. Local payment/refund/delivery gates are simulated, never real funds.",
|
|
14
|
+
"Show the exact workflow and version before user confirmation. Preview price, delivery and current terms before explicit submission approval.",
|
|
15
|
+
"Use platform guide for next actions. Submission is not publication: only an actual admin approval publishes the service."
|
|
16
|
+
]
|
|
17
|
+
};
|
|
18
|
+
export const SELL_OPERATIONS = [
|
|
19
|
+
{ command: "status", method: "GET", path: "/v1/seller/organizations", scope: read, fields: {}, description: "List existing merchant memberships" },
|
|
20
|
+
{ command: "guide", method: "GET", path: `${org}/guide`, scope: read, fields: { draft_id: "string" }, optional: ["draft_id"], description: "Read authoritative publication blockers and next actions" },
|
|
21
|
+
{ command: "library search", method: "GET", path: "/v1/library/apis", scope: read, fields: { q: "string" }, optional: ["q"], description: "Search available API contracts" },
|
|
22
|
+
{ command: "library get", method: "GET", path: "/v1/library/apis/{library_api_id}", scope: read, fields: {}, description: "Inspect an API contract" },
|
|
23
|
+
{ command: "sources list", method: "GET", path: `${org}/api-intakes`, scope: read, fields: {}, description: "List imported API sources" },
|
|
24
|
+
{ command: "sources inspect", method: "GET", path: `${org}/api-intakes/{intake_id}`, scope: read, fields: {}, description: "Inspect imported operations and verification results" },
|
|
25
|
+
{ command: "sources import", method: "POST", path: `${org}/api-intakes`, scope: write, fields: { provider_key: "string", source_url: "string", document: "document", media_type: "string" }, optional: ["source_url", "document", "media_type"], description: "Import an OpenAPI source without inventing request contracts" },
|
|
26
|
+
{ command: "sources library", method: "POST", path: `${org}/api-intakes/from-library`, scope: write, fields: { library_api_id: "string" }, description: "Bind a platform library source" },
|
|
27
|
+
{ command: "sources probe", method: "POST", path: `${org}/api-intakes/{intake_id}/probe`, scope: test, fields: { credential_profile_id: "string", operation_inputs: "object" }, description: "Verify selected API requests on the platform", confirmation: true },
|
|
28
|
+
{ command: "credentials status", method: "GET", path: `${org}/provider-credential-profiles`, scope: read, fields: {}, description: "Read credential bindings without secret values" },
|
|
29
|
+
{ command: "services list", method: "GET", path: `${org}/service-drafts`, scope: read, fields: {}, description: "List own service drafts and publication state" },
|
|
30
|
+
{ command: "services create", method: "POST", path: `${org}/service-drafts`, scope: write, fields: { service_id: "string", public_name: "string" }, description: "Create a platform service draft" },
|
|
31
|
+
{ command: "services icon", method: "PUT", path: `${draft}/icon/content`, scope: write, fields: { content_base64: "string" }, description: "Upload a PNG/JPEG service icon, max 512 KiB, as signed JSON" },
|
|
32
|
+
{ command: "services get", method: "GET", path: `${draft}/workflow`, scope: read, fields: {}, description: "Read the current workflow and service settings" },
|
|
33
|
+
{ command: "workflow catalog", method: "GET", path: `${org}/node-catalog`, scope: read, fields: {}, description: "Read exact supported node templates and required configuration" },
|
|
34
|
+
{ command: "workflow plan", method: "POST", path: `${draft}/orchestration-runs`, scope: test, fields: { expected_revision: "number", instruction: "string", api_intake_ids: "array", credential_profile_bindings: "object" }, description: "Explicitly use ItPay AI planning and platform verification", confirmation: true },
|
|
35
|
+
{ command: "workflow plan-get", method: "GET", path: `${draft}/orchestration-runs/{run_id}`, scope: read, fields: {}, description: "Read the AI candidate and diagnostics" },
|
|
36
|
+
{ command: "workflow plan-apply", method: "POST", path: `${draft}/orchestration-runs/{run_id}/apply`, scope: write, fields: { expected_revision: "number" }, description: "Apply a reviewed AI candidate to the draft", confirmation: true },
|
|
37
|
+
{ command: "workflow upload", method: "PUT", path: `${draft}/workflow`, scope: write, fields: { expected_revision: "number", public_name: "string", arazzo: "object" }, optional: ["public_name"], description: "Update the platform workflow with optimistic concurrency" },
|
|
38
|
+
{ command: "workflow validate-platform", method: "POST", path: `${draft}/workflow/validate`, scope: write, fields: {}, description: "Validate using the same public runtime rules as approval" },
|
|
39
|
+
{ command: "workflow versions list-platform", method: "GET", path: `${draft}/workflow/versions`, scope: read, fields: {}, description: "List explicitly saved platform versions" },
|
|
40
|
+
{ command: "workflow versions save-platform", method: "POST", path: `${draft}/workflow/versions`, scope: write, fields: { name: "string", source: "string", arazzo_document: "string", expected_revision: "number" }, description: "Save an explicit immutable platform version" },
|
|
41
|
+
{ command: "workflow versions delete-platform", method: "DELETE", path: `${draft}/workflow/versions/{version_id}`, scope: write, fields: { expected_revision: "number" }, description: "Delete an eligible saved version and its runs", confirmation: true },
|
|
42
|
+
{ command: "fixtures get", method: "GET", path: `${draft}/fixtures`, scope: read, fields: {}, description: "Read test inputs" },
|
|
43
|
+
{ command: "fixtures set", method: "PUT", path: `${draft}/fixtures`, scope: write, fields: { expected_revision: "number", fixtures: "array" }, description: "Save explicit test inputs" },
|
|
44
|
+
{ command: "pricing get", method: "GET", path: `${draft}/workflow`, scope: read, fields: {}, description: "Read the current price and refund policy" },
|
|
45
|
+
{ command: "pricing set", method: "PUT", path: `${draft}/pricing`, scope: write, fields: { expected_revision: "number", pricing: "object", policy: "object" }, description: "Set per-call price and platform refund policy" },
|
|
46
|
+
{ command: "verify", method: "POST", path: `${draft}/validation-runs`, scope: test, fields: { workflow_version_id: "string", expected_semantic_revision: "number", expected_fixture_revision: "number" }, description: "Start platform verification for an explicitly saved version", confirmation: true },
|
|
47
|
+
{ command: "tests start-checkout", method: "POST", path: `${draft}/live-checkout-tests`, scope: test, fields: { expected_revision: "number", agent_instance_id: "string" }, description: "Dev admin only: freeze one success fixture into a private real-checkout test bound to your registered Agent. Payment still requires user confirmation; this does not publish the service.", confirmation: true },
|
|
48
|
+
{ command: "tests accept-checkout", method: "POST", path: `${draft}/live-checkout-tests/{execution_id}/accept`, scope: test, fields: {}, description: "Record server-observed issued tickets and completed delivery as evidence for the exact tested revision. Never synthesizes payment or issuance.", confirmation: true },
|
|
49
|
+
{ command: "runs get", method: "GET", path: `${draft}/validation-runs/{run_id}`, scope: read, fields: {}, description: "Inspect platform test nodes and diagnostics" },
|
|
50
|
+
{ command: "runs confirm", method: "POST", path: `${draft}/validation-runs/{run_id}/confirm`, scope: test, fields: { risk_hash: "string" }, description: "Confirm the exact disclosed platform test side effects", confirmation: true },
|
|
51
|
+
{ command: "submission preview", method: "GET", path: `${org}/guide`, scope: read, fields: { draft_id: "string" }, description: "Read publication readiness and required agreements" },
|
|
52
|
+
{ command: "submission submit", method: "POST", path: `${draft}/submit`, scope: submit, fields: { expected_revision: "number", terms_version: "string", confirmations: "array" }, description: "Submit the confirmed version for review, never approve it", confirmation: true },
|
|
53
|
+
{ command: "submission get", method: "GET", path: `${org}/service-submissions/{submission_id}`, scope: read, fields: {}, description: "Read the actual review/publication result" },
|
|
54
|
+
{ command: "submission withdraw", method: "POST", path: `${org}/service-submissions/{submission_id}/withdraw`, scope: submit, fields: {}, description: "Withdraw own pending submission", confirmation: true },
|
|
55
|
+
];
|
|
56
|
+
export function sellRequest(operation, params, input = {}) {
|
|
57
|
+
let path = operation.path.replace(/\{(\w+)\}/g, (_, key) => {
|
|
58
|
+
const value = params[key];
|
|
59
|
+
if (typeof value !== "string" || !value.trim())
|
|
60
|
+
throw new Error(`${key} is required`);
|
|
61
|
+
return encodeURIComponent(value);
|
|
62
|
+
});
|
|
63
|
+
for (const key of Object.keys(input))
|
|
64
|
+
if (!operation.fields[key])
|
|
65
|
+
throw new Error(`Unsupported field: ${key}`);
|
|
66
|
+
for (const [key, type] of Object.entries(operation.fields)) {
|
|
67
|
+
const value = input[key];
|
|
68
|
+
if (value === undefined && operation.optional?.includes(key))
|
|
69
|
+
continue;
|
|
70
|
+
const valid = type === "document" ? (typeof value === "string" || (!!value && typeof value === "object" && !Array.isArray(value))) : type === "array" ? Array.isArray(value) : type === "object" ? !!value && typeof value === "object" && !Array.isArray(value) : typeof value === type;
|
|
71
|
+
if (!valid)
|
|
72
|
+
throw new Error(`${key} must be ${type}`);
|
|
73
|
+
if (type === "number" && (!Number.isSafeInteger(value) || Number(value) < 0))
|
|
74
|
+
throw new Error(`${key} must be a nonnegative integer`);
|
|
75
|
+
}
|
|
76
|
+
if (operation.method === "GET") {
|
|
77
|
+
const query = [];
|
|
78
|
+
for (const [key, value] of Object.entries(input))
|
|
79
|
+
if (value !== undefined)
|
|
80
|
+
query.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
81
|
+
if (query.length)
|
|
82
|
+
path += `?${query.join("&")}`;
|
|
83
|
+
return { path, method: operation.method };
|
|
84
|
+
}
|
|
85
|
+
return { path, method: operation.method, body: input };
|
|
86
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { newBackendClient, loadConfig } from "../state/config.js";
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, rmSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { resolve, join, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
const hash = (value) => 'sha256:' + createHash('sha256').update(value).digest('hex');
|
|
8
|
+
function read(path) { return JSON.parse(readFileSync(path, 'utf8')); }
|
|
9
|
+
function save(path, value) { mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); const tmp = path + '.' + randomUUID() + '.tmp'; writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 }); renameSync(tmp, path); }
|
|
10
|
+
export function project(directory) { const root = resolve(directory); return { root, state: join(root, '.itpay-sell'), config: read(join(root, 'service.json')), yaml: readFileSync(join(root, 'workflow.yaml'), 'utf8') }; }
|
|
11
|
+
export function localDependencies(state) {
|
|
12
|
+
const platformPath = join(state, 'platform-dependencies.json');
|
|
13
|
+
const dependencies = existsSync(platformPath) ? read(platformPath) : {};
|
|
14
|
+
const sources = join(state, 'sources');
|
|
15
|
+
if (existsSync(sources))
|
|
16
|
+
for (const file of readdirSync(sources)) {
|
|
17
|
+
const source = read(join(sources, file));
|
|
18
|
+
for (const operation of source.operations)
|
|
19
|
+
dependencies[operation.provider_operation_version_id] = {
|
|
20
|
+
provider_operation_version_id: operation.provider_operation_version_id,
|
|
21
|
+
provider_key: source.provider_key, operation_key: operation.operation_key,
|
|
22
|
+
operation_hash: operation.operation_hash, operation: operation.operation,
|
|
23
|
+
request_schema: operation.request_schema, response_schema: operation.response_schema,
|
|
24
|
+
retry_policy: operation.retry_policy ?? {},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return dependencies;
|
|
28
|
+
}
|
|
29
|
+
export async function companion(input, onEvent) {
|
|
30
|
+
const target = `${process.platform === 'win32' ? 'windows' : process.platform}-${process.arch === 'x64' ? 'amd64' : process.arch}/itpay-sell${process.platform === 'win32' ? '.exe' : ''}`;
|
|
31
|
+
const packaged = fileURLToPath(new URL(`../../../bin/${target}`, import.meta.url));
|
|
32
|
+
const binary = process.env.ITPAY_SELL_RUNNER ?? (existsSync(packaged) ? packaged : fileURLToPath(new URL(`../../bin/${target}`, import.meta.url)));
|
|
33
|
+
if (!existsSync(binary))
|
|
34
|
+
throw new Error(`Local runner missing for ${process.platform}-${process.arch}; install a CLI package including its native runner`);
|
|
35
|
+
return new Promise((done, fail) => {
|
|
36
|
+
const child = spawn(binary, [], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
37
|
+
let buffer = '', stderr = '';
|
|
38
|
+
const events = [];
|
|
39
|
+
child.on('error', fail);
|
|
40
|
+
child.stderr.on('data', chunk => { stderr += String(chunk).slice(0, 2000); });
|
|
41
|
+
child.stdout.on('data', chunk => {
|
|
42
|
+
buffer += String(chunk);
|
|
43
|
+
let index;
|
|
44
|
+
while ((index = buffer.indexOf('\n')) >= 0) {
|
|
45
|
+
const line = buffer.slice(0, index);
|
|
46
|
+
buffer = buffer.slice(index + 1);
|
|
47
|
+
try {
|
|
48
|
+
const event = JSON.parse(line);
|
|
49
|
+
events.push(event);
|
|
50
|
+
onEvent?.(event);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
child.kill();
|
|
54
|
+
fail(new Error('Invalid local runner response'));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
child.on('close', code => {
|
|
59
|
+
if (code !== 0)
|
|
60
|
+
fail(new Error(events.find(event => event.type === 'error')?.value?.message ?? stderr ?? 'Local runner failed'));
|
|
61
|
+
else
|
|
62
|
+
done(events);
|
|
63
|
+
});
|
|
64
|
+
child.stdin.on('error', () => { });
|
|
65
|
+
child.stdin.end(JSON.stringify(input));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function last(events, type) { return [...events].reverse().find(event => event.type === type)?.value; }
|
|
69
|
+
export async function localAction(action, directory, options) {
|
|
70
|
+
const root = resolve(directory), state = join(root, '.itpay-sell');
|
|
71
|
+
if (action === 'init') {
|
|
72
|
+
if (existsSync(join(root, 'service.json')) || existsSync(join(root, 'workflow.yaml')) || existsSync(state))
|
|
73
|
+
throw new Error('Project already exists; existing files were not replaced');
|
|
74
|
+
mkdirSync(root, { recursive: true });
|
|
75
|
+
const config = { format_version: 1, service_id: options.serviceId ?? '', public_name: options.name ?? '', pricing: { billing_mode: 'per_call' }, policy: {}, fixtures: [] };
|
|
76
|
+
save(join(root, 'service.json'), config);
|
|
77
|
+
writeFileSync(join(root, 'workflow.yaml'), '# Generate the workflow from locked API contracts using itpay sell guide.\n', { flag: 'wx' });
|
|
78
|
+
save(join(state, 'versions.json'), { versions: [] });
|
|
79
|
+
return { root, next: 'itpay sell sources add --file <openapi> --provider-key <key>', missing: ['workflow', 'price', 'fixtures'] };
|
|
80
|
+
}
|
|
81
|
+
const p = project(root);
|
|
82
|
+
if (action === 'workflow import') {
|
|
83
|
+
if (!options.file)
|
|
84
|
+
throw new Error('--file is required');
|
|
85
|
+
const yaml = readFileSync(resolve(options.file), 'utf8');
|
|
86
|
+
const validation = last(await companion({ action: 'validate', dependencies: localDependencies(state), document: yaml, pricing: p.config.pricing, policy: p.config.policy }), 'validation');
|
|
87
|
+
const backup = join(state, 'imports', hash(p.yaml).slice(7) + '.yaml');
|
|
88
|
+
mkdirSync(dirname(backup), { recursive: true, mode: 0o700 });
|
|
89
|
+
writeFileSync(backup, p.yaml, { mode: 0o600 });
|
|
90
|
+
writeFileSync(join(root, 'workflow.yaml'), yaml);
|
|
91
|
+
return { imported: true, validation, next: 'Resolve all imported API references and save a new workflow version' };
|
|
92
|
+
}
|
|
93
|
+
if (action === 'config') {
|
|
94
|
+
if (options.file) {
|
|
95
|
+
const value = read(resolve(options.file));
|
|
96
|
+
for (const key of Object.keys(value))
|
|
97
|
+
if (!['service_id', 'public_name', 'pricing', 'policy', 'fixtures', 'merchant_id', 'draft_id', 'expected_revision'].includes(key))
|
|
98
|
+
throw new Error('Unsupported setting: ' + key);
|
|
99
|
+
save(join(root, 'service.json'), { ...p.config, ...value });
|
|
100
|
+
}
|
|
101
|
+
return read(join(root, 'service.json'));
|
|
102
|
+
}
|
|
103
|
+
if (action === 'sources add') {
|
|
104
|
+
if ((!options.file && !options.url) || !options.providerKey)
|
|
105
|
+
throw new Error('--file or --url, and --provider-key are required');
|
|
106
|
+
if (options.file && options.url)
|
|
107
|
+
throw new Error('Choose file or URL, not both');
|
|
108
|
+
const raw = options.file ? readFileSync(resolve(options.file), 'utf8') : last(await companion({ action: 'fetch', source_url: options.url }), 'source').document;
|
|
109
|
+
const id = hash(raw).slice(7);
|
|
110
|
+
const operations = last(await companion({ action: 'compile', document: raw, provider_key: options.providerKey }), 'compiled');
|
|
111
|
+
const path = join(state, 'sources', id + '.json');
|
|
112
|
+
save(path, { provider_key: options.providerKey, source_hash: hash(raw), original: raw, operations });
|
|
113
|
+
return { source_hash: hash(raw), operations, instruction: 'Use these exact contracts when constructing workflow.yaml; bind local credentials by environment reference' };
|
|
114
|
+
}
|
|
115
|
+
if (action === 'credentials bind') {
|
|
116
|
+
if (!options.profile || !options.file)
|
|
117
|
+
throw new Error('--profile and --file are required; file must map credential fields to environment variable names, not secret values');
|
|
118
|
+
const bindings = read(resolve(options.file));
|
|
119
|
+
for (const value of Object.values(bindings))
|
|
120
|
+
if (typeof value !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value))
|
|
121
|
+
throw new Error('Credential bindings must be environment variable names');
|
|
122
|
+
const path = join(state, 'credentials.json');
|
|
123
|
+
save(path, { ...(existsSync(path) ? read(path) : {}), [options.profile]: bindings });
|
|
124
|
+
return { profile: options.profile, bound_fields: Object.keys(bindings) };
|
|
125
|
+
}
|
|
126
|
+
if (action === 'credentials upload') {
|
|
127
|
+
if (!options.confirm || !options.profile || !options.providerKey || !options.merchantId)
|
|
128
|
+
throw new Error('Merchant, provider, local profile and explicit confirmation are required');
|
|
129
|
+
const bindings = read(join(state, 'credentials.json'))[options.profile];
|
|
130
|
+
if (!bindings)
|
|
131
|
+
throw new Error('Local profile not bound');
|
|
132
|
+
const secrets = {};
|
|
133
|
+
for (const [field, env] of Object.entries(bindings)) {
|
|
134
|
+
const value = process.env[String(env)];
|
|
135
|
+
if (!value)
|
|
136
|
+
throw new Error('Missing credential environment for ' + field);
|
|
137
|
+
secrets[field] = value;
|
|
138
|
+
}
|
|
139
|
+
const result = await newBackendClient(loadConfig()).sellRequest({ method: 'POST', path: '/v1/seller/organizations/' + encodeURIComponent(options.merchantId) + '/provider-credential-profiles', body: { provider_key: options.providerKey, profile_key: options.profile, display_name: options.name ?? options.profile, environment: options.environment ?? 'sandbox', secrets, metadata: options.file ? read(resolve(options.file)) : {} } });
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
const versionsPath = join(state, 'versions.json');
|
|
143
|
+
const versions = read(versionsPath);
|
|
144
|
+
if (action === 'workflow versions save') {
|
|
145
|
+
const validation = last(await companion({ action: 'validate', dependencies: localDependencies(state), document: p.yaml, pricing: p.config.pricing, policy: p.config.policy }), 'validation');
|
|
146
|
+
const version = { version_id: randomUUID(), name: options.name ?? 'Saved version', hash: hash(p.yaml), config_hash: hash(JSON.stringify(p.config)), created_at: new Date().toISOString() };
|
|
147
|
+
save(join(state, 'versions', version.version_id + '.json'), { ...version, document: p.yaml, config: p.config });
|
|
148
|
+
versions.versions.push(version);
|
|
149
|
+
versions.selected = version.version_id;
|
|
150
|
+
save(versionsPath, versions);
|
|
151
|
+
return { ...version, validation };
|
|
152
|
+
}
|
|
153
|
+
if (action === 'workflow versions list')
|
|
154
|
+
return versions;
|
|
155
|
+
if (action === 'workflow versions use' || action === 'workflow versions delete') {
|
|
156
|
+
const version = versions.versions.find((v) => v.version_id === options.version);
|
|
157
|
+
if (!version)
|
|
158
|
+
throw new Error('Saved version not found');
|
|
159
|
+
if (action.endsWith('use')) {
|
|
160
|
+
const saved = read(join(state, 'versions', version.version_id + '.json'));
|
|
161
|
+
writeFileSync(join(root, 'workflow.yaml'), saved.document);
|
|
162
|
+
save(join(root, 'service.json'), saved.config);
|
|
163
|
+
versions.selected = version.version_id;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
if (!options.confirm)
|
|
167
|
+
throw new Error('Confirm version deletion with --confirm');
|
|
168
|
+
versions.versions = versions.versions.filter((v) => v.version_id !== version.version_id);
|
|
169
|
+
if (versions.selected === version.version_id)
|
|
170
|
+
delete versions.selected;
|
|
171
|
+
rmSync(join(state, 'versions', version.version_id + '.json'));
|
|
172
|
+
const runs = join(state, 'runs');
|
|
173
|
+
if (existsSync(runs))
|
|
174
|
+
for (const run of readdirSync(runs)) {
|
|
175
|
+
const meta = join(runs, run, 'meta.json');
|
|
176
|
+
if (existsSync(meta) && read(meta).version_id === version.version_id)
|
|
177
|
+
rmSync(join(runs, run), { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
save(versionsPath, versions);
|
|
181
|
+
return versions;
|
|
182
|
+
}
|
|
183
|
+
if (action === 'workflow validate')
|
|
184
|
+
return last(await companion({ action: 'validate', dependencies: localDependencies(state), document: p.yaml, pricing: p.config.pricing, policy: p.config.policy }), 'validation');
|
|
185
|
+
if (action === 'test get') {
|
|
186
|
+
if (!options.run)
|
|
187
|
+
throw new Error('--run is required');
|
|
188
|
+
return read(join(state, 'runs', safeID(options.run), 'report.json'));
|
|
189
|
+
}
|
|
190
|
+
const selected = versions.versions.find((v) => v.version_id === versions.selected);
|
|
191
|
+
if (!selected || selected.hash !== hash(p.yaml) || selected.config_hash !== hash(JSON.stringify(p.config)))
|
|
192
|
+
throw new Error('Save the current workflow and configuration before confirming or running');
|
|
193
|
+
if (action === 'workflow confirm') {
|
|
194
|
+
if (!options.confirm)
|
|
195
|
+
return { version: selected, config: p.config, workflow: p.yaml, instruction: 'Review this exact workflow and service configuration with the user, then confirm' };
|
|
196
|
+
save(join(state, 'confirmation.json'), { version_id: selected.version_id, hash: selected.hash, config_hash: selected.config_hash, confirmed_at: new Date().toISOString() });
|
|
197
|
+
return { confirmed: selected };
|
|
198
|
+
}
|
|
199
|
+
if (action === 'test run' || action === 'test resume') {
|
|
200
|
+
if (!options.confirm)
|
|
201
|
+
throw new Error('Live Provider calls may cost money or change data. Review dependencies and confirm with --confirm');
|
|
202
|
+
const dependencies = localDependencies(state);
|
|
203
|
+
const credentials = {};
|
|
204
|
+
const credentialPath = join(state, 'credentials.json');
|
|
205
|
+
if (existsSync(credentialPath))
|
|
206
|
+
for (const [profile, fields] of Object.entries(read(credentialPath))) {
|
|
207
|
+
const secrets = {};
|
|
208
|
+
for (const [field, env] of Object.entries(fields)) {
|
|
209
|
+
if (!process.env[String(env)])
|
|
210
|
+
throw new Error(`Missing local environment variable for credential field ${field}`);
|
|
211
|
+
secrets[field] = process.env[String(env)];
|
|
212
|
+
}
|
|
213
|
+
credentials[profile] = { ID: profile, Secrets: secrets, Metadata: {} };
|
|
214
|
+
}
|
|
215
|
+
if (action === 'test resume' && !options.run)
|
|
216
|
+
throw new Error('--run is required for resume');
|
|
217
|
+
const runID = options.run ? safeID(options.run) : randomUUID(), runDirectory = join(state, 'runs', runID);
|
|
218
|
+
const metaPath = join(runDirectory, 'meta.json');
|
|
219
|
+
if (action === 'test resume' && !existsSync(metaPath))
|
|
220
|
+
throw new Error('Run does not exist');
|
|
221
|
+
if (existsSync(metaPath) && read(metaPath).version_id !== selected.version_id)
|
|
222
|
+
throw new Error('Run belongs to a different version');
|
|
223
|
+
save(metaPath, { run_id: runID, version_id: selected.version_id, hash: selected.hash, config_hash: selected.config_hash });
|
|
224
|
+
save(join(state, "last-run.json"), { run_id: runID });
|
|
225
|
+
const events = await companion({ action: 'run', document: p.yaml, pricing: p.config.pricing, policy: p.config.policy, fixtures: p.config.fixtures, dependencies, credentials, run_directory: runDirectory, confirmed: true }, event => {
|
|
226
|
+
if (event.type === 'nodes')
|
|
227
|
+
save(join(runDirectory, 'nodes.json'), event.value);
|
|
228
|
+
});
|
|
229
|
+
const result = last(events, 'result');
|
|
230
|
+
save(join(runDirectory, 'report.json'), result);
|
|
231
|
+
return { run_id: runID, ...result };
|
|
232
|
+
}
|
|
233
|
+
throw new Error('Unsupported local action: ' + action);
|
|
234
|
+
}
|
|
235
|
+
function safeID(value) {
|
|
236
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value))
|
|
237
|
+
throw new Error('Invalid local run ID');
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
export const LOCAL_ACTIONS = ['init', 'config', 'workflow import', 'sources add', 'credentials bind', 'credentials upload', 'workflow validate', 'workflow confirm', 'workflow versions save', 'workflow versions list', 'workflow versions use', 'workflow versions delete', 'test run', 'test resume', 'test get'];
|
|
241
|
+
export function registerLocal(sell) {
|
|
242
|
+
for (const name of LOCAL_ACTIONS) {
|
|
243
|
+
let parent = sell;
|
|
244
|
+
const parts = name.split(' ');
|
|
245
|
+
const leaf = parts.pop();
|
|
246
|
+
for (const part of parts) {
|
|
247
|
+
parent = parent.commands.find(cmd => cmd.name() === part) ?? parent.command(part);
|
|
248
|
+
}
|
|
249
|
+
parent.command(leaf).description('Local Sell: ' + name).option('--project <directory>', 'Project directory', '.').option('--file <file>').option('--url <https-url>').option('--name <name>').option('--service-id <id>').option('--provider-key <key>').option('--profile <id>').option('--merchant-id <id>').option('--environment <environment>').option('--version-id <id>').option('--run <id>').option('--confirm').option('--json').action(async (options) => {
|
|
250
|
+
try {
|
|
251
|
+
const result = await localAction(name.replace('list-local', 'list'), options.project, { ...options, version: options.versionId });
|
|
252
|
+
process.stdout.write(JSON.stringify({ status: 'ok', result }, null, 2) + '\n');
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
process.stdout.write(JSON.stringify({ status: 'error', message: error instanceof Error ? error.message : 'Local action failed' }) + '\n');
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|