@stripe/link-cli 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/cli.js +406 -93
- package/package.json +3 -1
- package/postinstall.mjs +50 -0
package/README.md
CHANGED
|
@@ -63,11 +63,12 @@ Link CLI can run as a local MCP server. Add the following to your MCP client con
|
|
|
63
63
|
Use `serve` to expose link-cli as an MCP endpoint over HTTP. This is useful for remote or containerised agents that can't launch a local subprocess.
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
link-cli serve #
|
|
66
|
+
link-cli serve # binds 127.0.0.1:54321 (loopback only)
|
|
67
67
|
link-cli serve --port 8080
|
|
68
|
+
link-cli serve --host 0.0.0.0 # expose beyond localhost (see warning below)
|
|
68
69
|
```
|
|
69
70
|
|
|
70
|
-
The
|
|
71
|
+
The server only handles the `/mcp` endpoint (and `/.well-known/skills/` discovery); any other path returns `404`. It binds to `127.0.0.1` by default so only the local host can reach it. Anyone who can reach the port can use this CLI's authenticated Link session, so only override `--host` on a trusted, isolated network — doing so prints a warning.
|
|
71
72
|
|
|
72
73
|
## Quickstart
|
|
73
74
|
|
|
@@ -230,7 +231,7 @@ A spend request moves through: **create** → **request approval** → **approve
|
|
|
230
231
|
**Required fields for create:** `merchant_name`, `merchant_url`, `context`, `amount`. `payment_method_id` is optional — if omitted, your default payment method will be used, or the first eligible one if no default is set.
|
|
231
232
|
|
|
232
233
|
**Constraints:** `context` must be at least 100 characters; `amount` must not exceed 500000 (cents); `currency` must be a 3-letter ISO code. The user has 10 minutes from when approval is requested to approve. Approved credentials (card or SPT) are valid for 12 hours from spend request creation.
|
|
233
|
-
**Test mode:** Pass `--test` to create testmode credentials (
|
|
234
|
+
**Test mode:** Pass `--test` to create a testmode SpendRequest. A testmode SpendRequest will return test payment credentials (e.g test card `4000009990001984`) rather than a real payment credential. Testmode SpendRequests will not charge the underlying payment method of the SpendRequest. This is useful for development and integration testing without real payment methods.
|
|
234
235
|
|
|
235
236
|
```bash
|
|
236
237
|
# Update before approval
|
package/dist/cli.js
CHANGED
|
@@ -11282,6 +11282,11 @@ var BalancesResource = class extends BaseResource {
|
|
|
11282
11282
|
}
|
|
11283
11283
|
buildUrl(params) {
|
|
11284
11284
|
const url = new URL(this.endpoint);
|
|
11285
|
+
if (params.sources !== void 0) {
|
|
11286
|
+
for (const source of params.sources) {
|
|
11287
|
+
url.searchParams.append("sources[]", source);
|
|
11288
|
+
}
|
|
11289
|
+
}
|
|
11285
11290
|
if (params.limit !== void 0) {
|
|
11286
11291
|
url.searchParams.set("limit", String(params.limit));
|
|
11287
11292
|
}
|
|
@@ -13097,6 +13102,7 @@ var BalancesList = ({
|
|
|
13097
13102
|
// src/commands/balances/schema.ts
|
|
13098
13103
|
import { z as z2 } from "incur";
|
|
13099
13104
|
var listOptions = z2.object({
|
|
13105
|
+
source: z2.array(z2.string()).default([]).describe("Filter by source ID. Repeat to include multiple sources."),
|
|
13100
13106
|
limit: z2.coerce.number().int().positive().max(100).optional().describe("Maximum number of balances to return (1-100)."),
|
|
13101
13107
|
startingAfter: z2.string().optional().describe("Cursor: return balances after this balance ID."),
|
|
13102
13108
|
endingBefore: z2.string().optional().describe("Cursor: return balances before this balance ID.")
|
|
@@ -13117,6 +13123,7 @@ function createBalancesCli(createResource, authStorage2, envAccessToken2) {
|
|
|
13117
13123
|
const opts = c.options;
|
|
13118
13124
|
const resource = createResource();
|
|
13119
13125
|
const params = {};
|
|
13126
|
+
if (opts.source.length > 0) params.sources = opts.source;
|
|
13120
13127
|
if (opts.limit !== void 0) params.limit = opts.limit;
|
|
13121
13128
|
if (opts.startingAfter !== void 0)
|
|
13122
13129
|
params.starting_after = opts.startingAfter;
|
|
@@ -13814,11 +13821,11 @@ function getMethodDetails(request) {
|
|
|
13814
13821
|
}
|
|
13815
13822
|
function resolveStripeChallenge(challenges) {
|
|
13816
13823
|
const stripeChallenge = challenges.find(
|
|
13817
|
-
(challenge) => challenge.method === "stripe" && challenge.intent === "charge"
|
|
13824
|
+
(challenge) => challenge.method === "stripe" && (challenge.intent === "charge" || challenge.intent === "session")
|
|
13818
13825
|
);
|
|
13819
13826
|
if (!stripeChallenge) {
|
|
13820
13827
|
throw new Error(
|
|
13821
|
-
"WWW-Authenticate header does not include a stripe charge challenge"
|
|
13828
|
+
"WWW-Authenticate header does not include a stripe charge or session challenge"
|
|
13822
13829
|
);
|
|
13823
13830
|
}
|
|
13824
13831
|
if (typeof stripeChallenge.request !== "object" || stripeChallenge.request == null || Array.isArray(stripeChallenge.request)) {
|
|
@@ -13853,7 +13860,7 @@ function decodeStripeChallenge(challengeHeader) {
|
|
|
13853
13860
|
id: challenge.id,
|
|
13854
13861
|
realm: challenge.realm,
|
|
13855
13862
|
method: "stripe",
|
|
13856
|
-
intent:
|
|
13863
|
+
intent: challenge.intent,
|
|
13857
13864
|
description: challenge.description,
|
|
13858
13865
|
digest: challenge.digest,
|
|
13859
13866
|
expires: challenge.expires,
|
|
@@ -13887,7 +13894,11 @@ function buildHeaders(data, headers) {
|
|
|
13887
13894
|
async function readPayResult(response) {
|
|
13888
13895
|
const responseHeaders = Object.fromEntries(response.headers.entries());
|
|
13889
13896
|
const body = await response.text();
|
|
13890
|
-
return {
|
|
13897
|
+
return sanitizeDeep({
|
|
13898
|
+
status: response.status,
|
|
13899
|
+
headers: responseHeaders,
|
|
13900
|
+
body
|
|
13901
|
+
});
|
|
13891
13902
|
}
|
|
13892
13903
|
function createStripePaymentClient(spt) {
|
|
13893
13904
|
const stripeCharge = Method.toClient(StripeMethods.charge, {
|
|
@@ -13898,8 +13909,19 @@ function createStripePaymentClient(spt) {
|
|
|
13898
13909
|
});
|
|
13899
13910
|
}
|
|
13900
13911
|
});
|
|
13912
|
+
const stripeSession = Method.toClient(
|
|
13913
|
+
{ ...StripeMethods.charge, intent: "session" },
|
|
13914
|
+
{
|
|
13915
|
+
async createCredential({ challenge }) {
|
|
13916
|
+
return Credential.serialize({
|
|
13917
|
+
challenge,
|
|
13918
|
+
payload: { action: "open", grantedToken: spt }
|
|
13919
|
+
});
|
|
13920
|
+
}
|
|
13921
|
+
}
|
|
13922
|
+
);
|
|
13901
13923
|
return Mppx.create({
|
|
13902
|
-
methods: [stripeCharge],
|
|
13924
|
+
methods: [stripeCharge, stripeSession],
|
|
13903
13925
|
polyfill: false,
|
|
13904
13926
|
transport: Transport.from({
|
|
13905
13927
|
name: "stripe-http",
|
|
@@ -13917,7 +13939,7 @@ function createStripePaymentClient(spt) {
|
|
|
13917
13939
|
})
|
|
13918
13940
|
});
|
|
13919
13941
|
}
|
|
13920
|
-
async function
|
|
13942
|
+
async function runMppPayWithSpendRequest(url, spendRequestId, method, data, headers, repository) {
|
|
13921
13943
|
const spendRequest = await repository.getSpendRequest(spendRequestId, {
|
|
13922
13944
|
include: ["shared_payment_token"]
|
|
13923
13945
|
});
|
|
@@ -13938,7 +13960,15 @@ async function runMppPay(url, spendRequestId, method, data, headers, repository)
|
|
|
13938
13960
|
if (!spendRequest.shared_payment_token) {
|
|
13939
13961
|
throw new Error("Spend request does not have a shared payment token");
|
|
13940
13962
|
}
|
|
13941
|
-
|
|
13963
|
+
return payWithSpt(
|
|
13964
|
+
url,
|
|
13965
|
+
spendRequest.shared_payment_token.id,
|
|
13966
|
+
method,
|
|
13967
|
+
data,
|
|
13968
|
+
headers
|
|
13969
|
+
);
|
|
13970
|
+
}
|
|
13971
|
+
async function payWithSpt(url, spt, method, data, headers) {
|
|
13942
13972
|
const httpMethod = method ?? (data !== void 0 ? "POST" : "GET");
|
|
13943
13973
|
const requestHeaders = buildHeaders(data, headers);
|
|
13944
13974
|
const initialResponse = await fetch(url, {
|
|
@@ -13960,72 +13990,156 @@ async function runMppPay(url, spendRequestId, method, data, headers, repository)
|
|
|
13960
13990
|
});
|
|
13961
13991
|
return readPayResult(retryResponse);
|
|
13962
13992
|
}
|
|
13993
|
+
async function runMppPayFullFlow(opts) {
|
|
13994
|
+
const {
|
|
13995
|
+
url,
|
|
13996
|
+
method,
|
|
13997
|
+
data,
|
|
13998
|
+
headers,
|
|
13999
|
+
context,
|
|
14000
|
+
amountOverride,
|
|
14001
|
+
paymentMethodId,
|
|
14002
|
+
test,
|
|
14003
|
+
repository,
|
|
14004
|
+
paymentMethodsFactory,
|
|
14005
|
+
onStep,
|
|
14006
|
+
onApprovalUrl
|
|
14007
|
+
} = opts;
|
|
14008
|
+
const httpMethod = method ?? (data !== void 0 ? "POST" : "GET");
|
|
14009
|
+
const requestHeaders = buildHeaders(data, headers);
|
|
14010
|
+
onStep?.("probing");
|
|
14011
|
+
const probeResponse = await fetch(url, {
|
|
14012
|
+
method: httpMethod,
|
|
14013
|
+
body: data,
|
|
14014
|
+
headers: requestHeaders
|
|
14015
|
+
});
|
|
14016
|
+
if (probeResponse.status !== 402) {
|
|
14017
|
+
return readPayResult(probeResponse);
|
|
14018
|
+
}
|
|
14019
|
+
const wwwAuth = probeResponse.headers.get("www-authenticate");
|
|
14020
|
+
if (!wwwAuth) {
|
|
14021
|
+
throw new Error("URL returned 402 but no WWW-Authenticate header");
|
|
14022
|
+
}
|
|
14023
|
+
const decoded = decodeStripeChallenge(wwwAuth);
|
|
14024
|
+
const networkId = decoded.network_id;
|
|
14025
|
+
const challengeAmount = decoded.request_json.amount ? Number(decoded.request_json.amount) : void 0;
|
|
14026
|
+
const challengeCurrency = decoded.request_json.currency ?? "usd";
|
|
14027
|
+
const amount = amountOverride ?? challengeAmount;
|
|
14028
|
+
if (!amount) {
|
|
14029
|
+
throw new Error(
|
|
14030
|
+
"Could not determine amount from 402 challenge. Pass --amount explicitly."
|
|
14031
|
+
);
|
|
14032
|
+
}
|
|
14033
|
+
let pmId = paymentMethodId;
|
|
14034
|
+
if (!pmId) {
|
|
14035
|
+
onStep?.("creating");
|
|
14036
|
+
const pmResource = paymentMethodsFactory();
|
|
14037
|
+
const methods = await pmResource.list();
|
|
14038
|
+
if (!methods.length) {
|
|
14039
|
+
throw new Error(
|
|
14040
|
+
"No payment methods found. Add one with `link-cli payment-methods add`."
|
|
14041
|
+
);
|
|
14042
|
+
}
|
|
14043
|
+
pmId = methods[0].id;
|
|
14044
|
+
}
|
|
14045
|
+
onStep?.("creating");
|
|
14046
|
+
const spendRequest = await repository.createSpendRequest({
|
|
14047
|
+
payment_details: pmId,
|
|
14048
|
+
credential_type: "shared_payment_token",
|
|
14049
|
+
network_id: networkId,
|
|
14050
|
+
amount,
|
|
14051
|
+
currency: challengeCurrency,
|
|
14052
|
+
context,
|
|
14053
|
+
request_approval: true,
|
|
14054
|
+
test: test || void 0
|
|
14055
|
+
});
|
|
14056
|
+
onStep?.("approving");
|
|
14057
|
+
if (spendRequest.approval_url) {
|
|
14058
|
+
onApprovalUrl?.(spendRequest.approval_url);
|
|
14059
|
+
}
|
|
14060
|
+
const approved = await pollUntilApproved(repository, spendRequest.id);
|
|
14061
|
+
if (approved.status !== "approved") {
|
|
14062
|
+
throw new Error(
|
|
14063
|
+
`Spend request was not approved (status: ${approved.status})`
|
|
14064
|
+
);
|
|
14065
|
+
}
|
|
14066
|
+
onStep?.("signing");
|
|
14067
|
+
let withSpt = await repository.getSpendRequest(spendRequest.id, {
|
|
14068
|
+
include: ["shared_payment_token"]
|
|
14069
|
+
});
|
|
14070
|
+
for (let i = 0; i < 3 && withSpt && !withSpt.shared_payment_token; i++) {
|
|
14071
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
14072
|
+
withSpt = await repository.getSpendRequest(spendRequest.id, {
|
|
14073
|
+
include: ["shared_payment_token"]
|
|
14074
|
+
});
|
|
14075
|
+
}
|
|
14076
|
+
if (!withSpt?.shared_payment_token) {
|
|
14077
|
+
throw new Error("Failed to retrieve shared payment token");
|
|
14078
|
+
}
|
|
14079
|
+
onStep?.("submitting");
|
|
14080
|
+
return payWithSpt(
|
|
14081
|
+
url,
|
|
14082
|
+
withSpt.shared_payment_token.id,
|
|
14083
|
+
method,
|
|
14084
|
+
data,
|
|
14085
|
+
headers
|
|
14086
|
+
);
|
|
14087
|
+
}
|
|
13963
14088
|
function MppPay({
|
|
13964
14089
|
url,
|
|
13965
14090
|
spendRequestId,
|
|
13966
14091
|
method,
|
|
13967
14092
|
data,
|
|
13968
14093
|
headers,
|
|
14094
|
+
context,
|
|
14095
|
+
amountOverride,
|
|
14096
|
+
paymentMethodId,
|
|
14097
|
+
test,
|
|
13969
14098
|
repository,
|
|
14099
|
+
paymentMethodsFactory,
|
|
13970
14100
|
onComplete
|
|
13971
14101
|
}) {
|
|
13972
|
-
const [step, setStep] = useState5(
|
|
14102
|
+
const [step, setStep] = useState5(
|
|
14103
|
+
spendRequestId ? "signing" : "probing"
|
|
14104
|
+
);
|
|
13973
14105
|
const [result, setResult] = useState5(null);
|
|
13974
14106
|
const [error, setError] = useState5(null);
|
|
14107
|
+
const [approvalUrl, setApprovalUrl] = useState5(null);
|
|
13975
14108
|
useEffect5(() => {
|
|
13976
14109
|
(async () => {
|
|
13977
14110
|
try {
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
|
|
13986
|
-
|
|
13987
|
-
|
|
13988
|
-
`Spend request ${spendRequestId} must have credential_type 'shared_payment_token' (current: '${type}')`
|
|
14111
|
+
let payResult;
|
|
14112
|
+
if (spendRequestId) {
|
|
14113
|
+
setStep("signing");
|
|
14114
|
+
payResult = await runMppPayWithSpendRequest(
|
|
14115
|
+
url,
|
|
14116
|
+
spendRequestId,
|
|
14117
|
+
method,
|
|
14118
|
+
data,
|
|
14119
|
+
headers,
|
|
14120
|
+
repository
|
|
13989
14121
|
);
|
|
13990
|
-
}
|
|
13991
|
-
|
|
13992
|
-
|
|
13993
|
-
|
|
13994
|
-
|
|
13995
|
-
}
|
|
13996
|
-
if (!spendRequest.shared_payment_token) {
|
|
13997
|
-
throw new Error("Spend request does not have a shared payment token");
|
|
13998
|
-
}
|
|
13999
|
-
const spt = spendRequest.shared_payment_token.id;
|
|
14000
|
-
const httpMethod = method ?? (data !== void 0 ? "POST" : "GET");
|
|
14001
|
-
const requestHeaders = buildHeaders(data, headers);
|
|
14002
|
-
setStep("probing");
|
|
14003
|
-
const initialResponse = await fetch(url, {
|
|
14004
|
-
method: httpMethod,
|
|
14005
|
-
body: data,
|
|
14006
|
-
headers: requestHeaders
|
|
14007
|
-
});
|
|
14008
|
-
if (initialResponse.status !== 402) {
|
|
14009
|
-
const payResult2 = await readPayResult(initialResponse);
|
|
14010
|
-
setResult(payResult2);
|
|
14011
|
-
setStep("done");
|
|
14012
|
-
onComplete(payResult2);
|
|
14013
|
-
return;
|
|
14014
|
-
}
|
|
14015
|
-
setStep("signing");
|
|
14016
|
-
const authHeader = await createStripePaymentClient(spt).createCredential(
|
|
14017
|
-
initialResponse
|
|
14018
|
-
);
|
|
14019
|
-
setStep("submitting");
|
|
14020
|
-
const retryResponse = await fetch(url, {
|
|
14021
|
-
method: httpMethod,
|
|
14022
|
-
body: data,
|
|
14023
|
-
headers: {
|
|
14024
|
-
...requestHeaders,
|
|
14025
|
-
Authorization: authHeader
|
|
14122
|
+
} else {
|
|
14123
|
+
if (!context) {
|
|
14124
|
+
throw new Error(
|
|
14125
|
+
"--context is required for the full MPP flow (min 100 chars)"
|
|
14126
|
+
);
|
|
14026
14127
|
}
|
|
14027
|
-
|
|
14028
|
-
|
|
14128
|
+
payResult = await runMppPayFullFlow({
|
|
14129
|
+
url,
|
|
14130
|
+
method,
|
|
14131
|
+
data,
|
|
14132
|
+
headers,
|
|
14133
|
+
context,
|
|
14134
|
+
amountOverride,
|
|
14135
|
+
paymentMethodId,
|
|
14136
|
+
test: test ?? false,
|
|
14137
|
+
repository,
|
|
14138
|
+
paymentMethodsFactory,
|
|
14139
|
+
onStep: setStep,
|
|
14140
|
+
onApprovalUrl: (u) => setApprovalUrl(u)
|
|
14141
|
+
});
|
|
14142
|
+
}
|
|
14029
14143
|
setResult(payResult);
|
|
14030
14144
|
setStep("done");
|
|
14031
14145
|
onComplete(payResult);
|
|
@@ -14034,10 +14148,24 @@ function MppPay({
|
|
|
14034
14148
|
onComplete(null);
|
|
14035
14149
|
}
|
|
14036
14150
|
})();
|
|
14037
|
-
}, [
|
|
14151
|
+
}, [
|
|
14152
|
+
url,
|
|
14153
|
+
spendRequestId,
|
|
14154
|
+
method,
|
|
14155
|
+
data,
|
|
14156
|
+
headers,
|
|
14157
|
+
context,
|
|
14158
|
+
amountOverride,
|
|
14159
|
+
paymentMethodId,
|
|
14160
|
+
test,
|
|
14161
|
+
repository,
|
|
14162
|
+
paymentMethodsFactory,
|
|
14163
|
+
onComplete
|
|
14164
|
+
]);
|
|
14038
14165
|
const stepLabels = {
|
|
14039
|
-
|
|
14040
|
-
|
|
14166
|
+
probing: "Probing URL for 402 challenge",
|
|
14167
|
+
creating: "Creating spend request",
|
|
14168
|
+
approving: "Waiting for approval",
|
|
14041
14169
|
signing: "Signing credential",
|
|
14042
14170
|
submitting: "Submitting payment",
|
|
14043
14171
|
done: "Done"
|
|
@@ -14049,12 +14177,19 @@ function MppPay({
|
|
|
14049
14177
|
] });
|
|
14050
14178
|
}
|
|
14051
14179
|
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
14052
|
-
step !== "done" && /* @__PURE__ */
|
|
14053
|
-
/* @__PURE__ */ jsx10(
|
|
14054
|
-
|
|
14055
|
-
|
|
14056
|
-
|
|
14057
|
-
|
|
14180
|
+
step !== "done" && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
14181
|
+
/* @__PURE__ */ jsx10(Box7, { children: /* @__PURE__ */ jsxs7(Text9, { color: "cyan", children: [
|
|
14182
|
+
/* @__PURE__ */ jsx10(Spinner4, { type: "dots" }),
|
|
14183
|
+
" ",
|
|
14184
|
+
stepLabels[step],
|
|
14185
|
+
"..."
|
|
14186
|
+
] }) }),
|
|
14187
|
+
step === "approving" && approvalUrl && /* @__PURE__ */ jsx10(Box7, { marginTop: 1, paddingX: 2, children: /* @__PURE__ */ jsxs7(Text9, { children: [
|
|
14188
|
+
"Approve in Link app:",
|
|
14189
|
+
" ",
|
|
14190
|
+
/* @__PURE__ */ jsx10(Text9, { bold: true, color: "blue", children: approvalUrl })
|
|
14191
|
+
] }) })
|
|
14192
|
+
] }),
|
|
14058
14193
|
result && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
14059
14194
|
/* @__PURE__ */ jsxs7(
|
|
14060
14195
|
Text9,
|
|
@@ -14221,7 +14356,7 @@ var SptFlow = ({
|
|
|
14221
14356
|
setStep("mpp-pay-gate");
|
|
14222
14357
|
await waitForEnter();
|
|
14223
14358
|
setStep("mpp-pay");
|
|
14224
|
-
const payResponse = await
|
|
14359
|
+
const payResponse = await runMppPayWithSpendRequest(
|
|
14225
14360
|
DEMO_CLIMATE_API_URL,
|
|
14226
14361
|
result.id,
|
|
14227
14362
|
"POST",
|
|
@@ -14641,12 +14776,22 @@ function DecodeChallengeView({
|
|
|
14641
14776
|
// src/commands/mpp/schema.ts
|
|
14642
14777
|
import { z as z4 } from "incur";
|
|
14643
14778
|
var payOptions = z4.object({
|
|
14644
|
-
spendRequestId: z4.string().describe(
|
|
14645
|
-
'Approved spend request ID with credential_type "shared_payment_token"'
|
|
14779
|
+
spendRequestId: z4.string().optional().describe(
|
|
14780
|
+
'Approved spend request ID with credential_type "shared_payment_token". If omitted, the command handles the full flow: probe URL, parse challenge, create spend request, get approval, and pay.'
|
|
14646
14781
|
),
|
|
14647
14782
|
method: z4.string().optional().describe("HTTP method (default: GET, or POST if --data is provided)"),
|
|
14648
14783
|
data: z4.string().optional().describe("Request body (implies POST if --method is not set)"),
|
|
14649
|
-
header: z4.array(z4.string()).default([]).describe('Request header in "Name: Value" format (repeatable)')
|
|
14784
|
+
header: z4.array(z4.string()).default([]).describe('Request header in "Name: Value" format (repeatable)'),
|
|
14785
|
+
context: z4.string().min(100).optional().describe(
|
|
14786
|
+
"Min 100 chars \u2014 describe the purchase and rationale; the user reads this when approving. Required when --spend-request-id is not provided."
|
|
14787
|
+
),
|
|
14788
|
+
amount: z4.coerce.number().int().positive().optional().describe(
|
|
14789
|
+
"Amount in cents (derived from 402 challenge if omitted; required if challenge has no amount)"
|
|
14790
|
+
),
|
|
14791
|
+
paymentMethodId: z4.string().optional().describe("Payment method ID (uses default if omitted)"),
|
|
14792
|
+
test: z4.boolean().default(false).describe(
|
|
14793
|
+
"Use test mode (creates testmode credentials from test card data)"
|
|
14794
|
+
)
|
|
14650
14795
|
});
|
|
14651
14796
|
var decodeOptions = z4.object({
|
|
14652
14797
|
challenge: z4.string().describe(
|
|
@@ -14656,12 +14801,12 @@ var decodeOptions = z4.object({
|
|
|
14656
14801
|
|
|
14657
14802
|
// src/commands/mpp/index.tsx
|
|
14658
14803
|
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
14659
|
-
function createMppCli(repository, authStorage2, envAccessToken2) {
|
|
14804
|
+
function createMppCli(repository, paymentMethodsFactory, authStorage2, envAccessToken2) {
|
|
14660
14805
|
const cli2 = Cli4.create("mpp", {
|
|
14661
14806
|
description: "Machine payment protocol (MPP) commands"
|
|
14662
14807
|
});
|
|
14663
14808
|
cli2.command("pay", {
|
|
14664
|
-
description: "
|
|
14809
|
+
description: "Pay a URL via the Machine Payment Protocol. Handles the full 402 flow: probes the URL, parses the challenge, creates a spend request, gets approval, and pays with the SPT. Pass --spend-request-id to skip creation and use a pre-approved spend request.",
|
|
14665
14810
|
args: z5.object({
|
|
14666
14811
|
url: z5.string().describe("URL to pay")
|
|
14667
14812
|
}),
|
|
@@ -14669,7 +14814,7 @@ function createMppCli(repository, authStorage2, envAccessToken2) {
|
|
|
14669
14814
|
alias: { method: "X", data: "d", header: "H" },
|
|
14670
14815
|
outputPolicy: "agent-only",
|
|
14671
14816
|
middleware: [requireAuth(authStorage2, envAccessToken2)],
|
|
14672
|
-
async run(c) {
|
|
14817
|
+
async *run(c) {
|
|
14673
14818
|
const url = c.args.url;
|
|
14674
14819
|
const opts = c.options;
|
|
14675
14820
|
const method = opts.method;
|
|
@@ -14686,7 +14831,12 @@ function createMppCli(repository, authStorage2, envAccessToken2) {
|
|
|
14686
14831
|
method,
|
|
14687
14832
|
data,
|
|
14688
14833
|
headers,
|
|
14834
|
+
context: opts.context,
|
|
14835
|
+
amountOverride: opts.amount,
|
|
14836
|
+
paymentMethodId: opts.paymentMethodId,
|
|
14837
|
+
test: opts.test,
|
|
14689
14838
|
repository,
|
|
14839
|
+
paymentMethodsFactory,
|
|
14690
14840
|
onComplete: (result) => {
|
|
14691
14841
|
capturedResult = result;
|
|
14692
14842
|
}
|
|
@@ -14699,14 +14849,90 @@ function createMppCli(repository, authStorage2, envAccessToken2) {
|
|
|
14699
14849
|
}
|
|
14700
14850
|
);
|
|
14701
14851
|
}
|
|
14702
|
-
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14852
|
+
if (opts.spendRequestId) {
|
|
14853
|
+
yield await runMppPayWithSpendRequest(
|
|
14854
|
+
url,
|
|
14855
|
+
opts.spendRequestId,
|
|
14856
|
+
method,
|
|
14857
|
+
data,
|
|
14858
|
+
headers,
|
|
14859
|
+
repository
|
|
14860
|
+
);
|
|
14861
|
+
return;
|
|
14862
|
+
}
|
|
14863
|
+
const httpMethod = method ?? (data !== void 0 ? "POST" : "GET");
|
|
14864
|
+
const requestHeaders = buildHeaders(data, headers);
|
|
14865
|
+
const probeResponse = await fetch(url, {
|
|
14866
|
+
method: httpMethod,
|
|
14867
|
+
body: data,
|
|
14868
|
+
headers: requestHeaders
|
|
14869
|
+
});
|
|
14870
|
+
if (probeResponse.status !== 402) {
|
|
14871
|
+
yield await readPayResult(probeResponse);
|
|
14872
|
+
return;
|
|
14873
|
+
}
|
|
14874
|
+
const wwwAuth = probeResponse.headers.get("www-authenticate");
|
|
14875
|
+
if (!wwwAuth) {
|
|
14876
|
+
return c.error({
|
|
14877
|
+
code: "INVALID_RESPONSE",
|
|
14878
|
+
message: "URL returned 402 but no WWW-Authenticate header"
|
|
14879
|
+
});
|
|
14880
|
+
}
|
|
14881
|
+
const decoded = decodeStripeChallenge(wwwAuth);
|
|
14882
|
+
const networkId = decoded.network_id;
|
|
14883
|
+
const challengeAmount = decoded.request_json.amount ? Number(decoded.request_json.amount) : void 0;
|
|
14884
|
+
const challengeCurrency = decoded.request_json.currency ?? "usd";
|
|
14885
|
+
const amount = opts.amount ?? challengeAmount;
|
|
14886
|
+
if (!amount) {
|
|
14887
|
+
return c.error({
|
|
14888
|
+
code: "INVALID_INPUT",
|
|
14889
|
+
message: "Could not determine amount from 402 challenge. Pass --amount explicitly."
|
|
14890
|
+
});
|
|
14891
|
+
}
|
|
14892
|
+
if (!opts.context) {
|
|
14893
|
+
return c.error({
|
|
14894
|
+
code: "INVALID_INPUT",
|
|
14895
|
+
message: "--context is required for the full MPP flow (min 100 chars). Describe the purchase and rationale."
|
|
14896
|
+
});
|
|
14897
|
+
}
|
|
14898
|
+
let pmId = opts.paymentMethodId;
|
|
14899
|
+
if (!pmId) {
|
|
14900
|
+
const pmResource = paymentMethodsFactory();
|
|
14901
|
+
const methods = await pmResource.list();
|
|
14902
|
+
if (!methods.length) {
|
|
14903
|
+
return c.error({
|
|
14904
|
+
code: "NO_PAYMENT_METHOD",
|
|
14905
|
+
message: "No payment methods found. Add one with `link-cli payment-methods add`."
|
|
14906
|
+
});
|
|
14907
|
+
}
|
|
14908
|
+
pmId = methods[0].id;
|
|
14909
|
+
}
|
|
14910
|
+
const spendRequest = await repository.createSpendRequest({
|
|
14911
|
+
payment_details: pmId,
|
|
14912
|
+
credential_type: "shared_payment_token",
|
|
14913
|
+
network_id: networkId,
|
|
14914
|
+
amount,
|
|
14915
|
+
currency: challengeCurrency,
|
|
14916
|
+
context: opts.context,
|
|
14917
|
+
request_approval: true,
|
|
14918
|
+
test: opts.test || void 0
|
|
14919
|
+
});
|
|
14920
|
+
const nextFlags = [`--spend-request-id ${spendRequest.id}`];
|
|
14921
|
+
if (method) nextFlags.push(`-X ${method}`);
|
|
14922
|
+
if (data) nextFlags.push(`-d '${data}'`);
|
|
14923
|
+
if (headers) {
|
|
14924
|
+
for (const h of headers) nextFlags.push(`-H '${h}'`);
|
|
14925
|
+
}
|
|
14926
|
+
const nextCommand = `mpp pay ${url} ${nextFlags.join(" ")}`;
|
|
14927
|
+
yield {
|
|
14928
|
+
...spendRequest,
|
|
14929
|
+
instruction: `Present the approval_url to the user and ask them to approve in the Link app. Then call \`spend-request retrieve ${spendRequest.id} --interval 2 --max-attempts 300\` to poll until approved. Once approved, run the _next.command to complete payment. Do not wait for the user to reply \u2014 start polling immediately.`,
|
|
14930
|
+
_next: {
|
|
14931
|
+
poll_command: `spend-request retrieve ${spendRequest.id} --interval 2 --max-attempts 300`,
|
|
14932
|
+
pay_command: nextCommand,
|
|
14933
|
+
until: "status changes from pending_approval, then run pay_command"
|
|
14934
|
+
}
|
|
14935
|
+
};
|
|
14710
14936
|
}
|
|
14711
14937
|
});
|
|
14712
14938
|
cli2.command("decode", {
|
|
@@ -15083,17 +15309,47 @@ async function sendWebResponse(webRes, res) {
|
|
|
15083
15309
|
res.writeHead(webRes.status);
|
|
15084
15310
|
res.end(Buffer.from(buffer));
|
|
15085
15311
|
}
|
|
15312
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost"]);
|
|
15313
|
+
function isLoopbackHost(host) {
|
|
15314
|
+
return LOOPBACK_HOSTS.has(host.toLowerCase());
|
|
15315
|
+
}
|
|
15316
|
+
function isAllowedOrigin(origin) {
|
|
15317
|
+
if (!origin) return true;
|
|
15318
|
+
try {
|
|
15319
|
+
return isLoopbackHost(new URL(origin).hostname);
|
|
15320
|
+
} catch {
|
|
15321
|
+
return false;
|
|
15322
|
+
}
|
|
15323
|
+
}
|
|
15324
|
+
function isAllowedRoute(method, pathname) {
|
|
15325
|
+
if (pathname === "/mcp") return true;
|
|
15326
|
+
if (pathname.startsWith("/.well-known/skills/") && method === "GET")
|
|
15327
|
+
return true;
|
|
15328
|
+
return false;
|
|
15329
|
+
}
|
|
15086
15330
|
function createServeCli(rootCli) {
|
|
15087
15331
|
return Cli8.create("serve", {
|
|
15088
15332
|
description: "Start an HTTP server exposing link-cli as an MCP endpoint at /mcp",
|
|
15089
15333
|
options: z7.object({
|
|
15090
|
-
port: z7.coerce.number().default(54321).describe("Port to listen on")
|
|
15334
|
+
port: z7.coerce.number().default(54321).describe("Port to listen on"),
|
|
15335
|
+
host: z7.string().default("127.0.0.1").describe(
|
|
15336
|
+
"Host/interface to bind. Defaults to loopback; set explicitly (e.g. 0.0.0.0) to expose beyond localhost."
|
|
15337
|
+
)
|
|
15091
15338
|
}),
|
|
15092
15339
|
async run(c) {
|
|
15093
|
-
const { port } = c.options;
|
|
15340
|
+
const { port, host } = c.options;
|
|
15094
15341
|
const server = createServer(
|
|
15095
15342
|
async (req, res) => {
|
|
15096
|
-
|
|
15343
|
+
const origin = req.headers.origin;
|
|
15344
|
+
if (!isAllowedOrigin(origin)) {
|
|
15345
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
15346
|
+
res.end(JSON.stringify({ error: "forbidden origin" }));
|
|
15347
|
+
return;
|
|
15348
|
+
}
|
|
15349
|
+
if (origin) {
|
|
15350
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
15351
|
+
res.setHeader("Vary", "Origin");
|
|
15352
|
+
}
|
|
15097
15353
|
res.setHeader(
|
|
15098
15354
|
"Access-Control-Allow-Methods",
|
|
15099
15355
|
"GET, POST, DELETE, OPTIONS"
|
|
@@ -15107,6 +15363,12 @@ function createServeCli(rootCli) {
|
|
|
15107
15363
|
res.end();
|
|
15108
15364
|
return;
|
|
15109
15365
|
}
|
|
15366
|
+
const pathname = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
|
|
15367
|
+
if (!isAllowedRoute(req.method ?? "GET", pathname)) {
|
|
15368
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
15369
|
+
res.end(JSON.stringify({ error: "not found" }));
|
|
15370
|
+
return;
|
|
15371
|
+
}
|
|
15110
15372
|
try {
|
|
15111
15373
|
const webReq = await nodeRequestToWebRequest(req, port);
|
|
15112
15374
|
const webRes = await rootCli.fetch(webReq);
|
|
@@ -15120,9 +15382,16 @@ function createServeCli(rootCli) {
|
|
|
15120
15382
|
);
|
|
15121
15383
|
await new Promise((resolve, reject) => {
|
|
15122
15384
|
server.on("error", reject);
|
|
15123
|
-
server.listen(port, () => {
|
|
15385
|
+
server.listen(port, host, () => {
|
|
15386
|
+
if (!isLoopbackHost(host)) {
|
|
15387
|
+
process.stderr.write(
|
|
15388
|
+
`WARNING: link-cli serve is bound to ${host}, which may be reachable beyond localhost.
|
|
15389
|
+
Any caller that can reach this port can use the authenticated Link session of this CLI. Only do this on a trusted, isolated network.
|
|
15390
|
+
`
|
|
15391
|
+
);
|
|
15392
|
+
}
|
|
15124
15393
|
process.stderr.write(
|
|
15125
|
-
`link-cli MCP server listening on http
|
|
15394
|
+
`link-cli MCP server listening on http://${host}:${port}/mcp
|
|
15126
15395
|
`
|
|
15127
15396
|
);
|
|
15128
15397
|
});
|
|
@@ -15733,6 +16002,7 @@ var CreateSpendRequest = ({
|
|
|
15733
16002
|
const [request, setRequest] = useState9(null);
|
|
15734
16003
|
const [error, setError] = useState9("");
|
|
15735
16004
|
const [verificationUrl, setVerificationUrl] = useState9("");
|
|
16005
|
+
const [supportUrl, setSupportUrl] = useState9("");
|
|
15736
16006
|
const [outputFilePath, setOutputFilePath] = useState9(null);
|
|
15737
16007
|
const [fileError, setFileError] = useState9("");
|
|
15738
16008
|
const approvalUrl = request?.approval_url ?? "";
|
|
@@ -15773,8 +16043,11 @@ var CreateSpendRequest = ({
|
|
|
15773
16043
|
} catch (err) {
|
|
15774
16044
|
setError(err.message);
|
|
15775
16045
|
if (err instanceof LinkApiError) {
|
|
15776
|
-
const
|
|
15777
|
-
if (
|
|
16046
|
+
const errDetail = err.details;
|
|
16047
|
+
if (errDetail?.error?.verification_url)
|
|
16048
|
+
setVerificationUrl(errDetail.error.verification_url);
|
|
16049
|
+
if (errDetail?.error?.support_url)
|
|
16050
|
+
setSupportUrl(errDetail.error.support_url);
|
|
15778
16051
|
}
|
|
15779
16052
|
setStatus("error");
|
|
15780
16053
|
setTimeout(() => completeAndExit(null), DISPLAY_DELAY_MS);
|
|
@@ -15807,6 +16080,10 @@ var CreateSpendRequest = ({
|
|
|
15807
16080
|
verificationUrl && /* @__PURE__ */ jsxs18(Text20, { color: "red", children: [
|
|
15808
16081
|
"Complete additional verification at: ",
|
|
15809
16082
|
verificationUrl
|
|
16083
|
+
] }),
|
|
16084
|
+
supportUrl && /* @__PURE__ */ jsxs18(Text20, { color: "red", children: [
|
|
16085
|
+
"Identity verification failed. Contact support at: ",
|
|
16086
|
+
supportUrl
|
|
15810
16087
|
] })
|
|
15811
16088
|
] });
|
|
15812
16089
|
}
|
|
@@ -15976,6 +16253,7 @@ var RequestApproval = ({
|
|
|
15976
16253
|
const [result, setResult] = useState10(null);
|
|
15977
16254
|
const [error, setError] = useState10("");
|
|
15978
16255
|
const [verificationUrl, setVerificationUrl] = useState10("");
|
|
16256
|
+
const [supportUrl, setSupportUrl] = useState10("");
|
|
15979
16257
|
const { exit } = useApp6();
|
|
15980
16258
|
const completeAndExit = useCallback10(
|
|
15981
16259
|
(result2) => {
|
|
@@ -16005,8 +16283,11 @@ var RequestApproval = ({
|
|
|
16005
16283
|
} catch (err) {
|
|
16006
16284
|
setError(err.message);
|
|
16007
16285
|
if (err instanceof LinkApiError) {
|
|
16008
|
-
const
|
|
16009
|
-
if (
|
|
16286
|
+
const errDetail = err.details;
|
|
16287
|
+
if (errDetail?.error?.verification_url)
|
|
16288
|
+
setVerificationUrl(errDetail.error.verification_url);
|
|
16289
|
+
if (errDetail?.error?.support_url)
|
|
16290
|
+
setSupportUrl(errDetail.error.support_url);
|
|
16010
16291
|
}
|
|
16011
16292
|
setStatus("error");
|
|
16012
16293
|
setTimeout(() => {
|
|
@@ -16030,6 +16311,10 @@ var RequestApproval = ({
|
|
|
16030
16311
|
verificationUrl && /* @__PURE__ */ jsxs20(Text22, { color: "red", children: [
|
|
16031
16312
|
"Complete additional verification at: ",
|
|
16032
16313
|
verificationUrl
|
|
16314
|
+
] }),
|
|
16315
|
+
supportUrl && /* @__PURE__ */ jsxs20(Text22, { color: "red", children: [
|
|
16316
|
+
"Identity verification failed. Contact support at: ",
|
|
16317
|
+
supportUrl
|
|
16033
16318
|
] })
|
|
16034
16319
|
] });
|
|
16035
16320
|
}
|
|
@@ -16315,7 +16600,16 @@ var RetrieveSpendRequest = ({
|
|
|
16315
16600
|
/* @__PURE__ */ jsx30(Text23, { bold: true, children: psd.refund_details.state })
|
|
16316
16601
|
] })
|
|
16317
16602
|
] })
|
|
16318
|
-
] })
|
|
16603
|
+
] }),
|
|
16604
|
+
request?.link_transaction_id && /* @__PURE__ */ jsx30(Box21, { marginTop: 1, children: /* @__PURE__ */ jsxs21(Text23, { children: [
|
|
16605
|
+
"Transaction ID: ",
|
|
16606
|
+
/* @__PURE__ */ jsx30(Text23, { bold: true, children: request.link_transaction_id })
|
|
16607
|
+
] }) }),
|
|
16608
|
+
request?.status === "succeeded" && request?.activity_url && /* @__PURE__ */ jsx30(Box21, { marginTop: 1, children: /* @__PURE__ */ jsxs21(Text23, { children: [
|
|
16609
|
+
"Activity URL:",
|
|
16610
|
+
" ",
|
|
16611
|
+
/* @__PURE__ */ jsx30(Text23, { bold: true, color: "cyan", children: request.activity_url })
|
|
16612
|
+
] }) })
|
|
16319
16613
|
] })
|
|
16320
16614
|
] });
|
|
16321
16615
|
}
|
|
@@ -16779,6 +17073,12 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
|
|
|
16779
17073
|
message: `${err.message} Verification URL: ${apiErr.error.verification_url}`
|
|
16780
17074
|
});
|
|
16781
17075
|
}
|
|
17076
|
+
if (apiErr?.error?.support_url) {
|
|
17077
|
+
return c.error({
|
|
17078
|
+
code: err.code,
|
|
17079
|
+
message: `${err.message} Support URL: ${apiErr.error.support_url}`
|
|
17080
|
+
});
|
|
17081
|
+
}
|
|
16782
17082
|
}
|
|
16783
17083
|
throw err;
|
|
16784
17084
|
}
|
|
@@ -16898,6 +17198,12 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
|
|
|
16898
17198
|
message: `${err.message} Verification URL: ${apiErr.error.verification_url}`
|
|
16899
17199
|
});
|
|
16900
17200
|
}
|
|
17201
|
+
if (apiErr?.error?.support_url) {
|
|
17202
|
+
return c.error({
|
|
17203
|
+
code: err.code,
|
|
17204
|
+
message: `${err.message} Support URL: ${apiErr.error.support_url}`
|
|
17205
|
+
});
|
|
17206
|
+
}
|
|
16901
17207
|
}
|
|
16902
17208
|
throw err;
|
|
16903
17209
|
}
|
|
@@ -17850,7 +18156,7 @@ function cacheUpdateInfo(value, ttlMs = UPDATE_CACHE_TTL_MS) {
|
|
|
17850
18156
|
}
|
|
17851
18157
|
|
|
17852
18158
|
// src/cli.tsx
|
|
17853
|
-
var cliVersion = "0.
|
|
18159
|
+
var cliVersion = "0.10.1";
|
|
17854
18160
|
var cliName = "@stripe/link-cli";
|
|
17855
18161
|
var defaultHeaders = {
|
|
17856
18162
|
"User-Agent": `link-cli/${cliVersion}`
|
|
@@ -17941,7 +18247,14 @@ if (!hiddenCli) {
|
|
|
17941
18247
|
envAccessToken
|
|
17942
18248
|
)
|
|
17943
18249
|
);
|
|
17944
|
-
cli.command(
|
|
18250
|
+
cli.command(
|
|
18251
|
+
createMppCli(
|
|
18252
|
+
spendRequestRepo,
|
|
18253
|
+
() => factory.createPaymentMethodsResource(),
|
|
18254
|
+
authStorage,
|
|
18255
|
+
envAccessToken
|
|
18256
|
+
)
|
|
18257
|
+
);
|
|
17945
18258
|
cli.command(
|
|
17946
18259
|
createReportCli(
|
|
17947
18260
|
() => factory.createReportResource(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stripe/link-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"link-cli": "./dist/cli.js"
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"dist",
|
|
10
10
|
"!dist/sea",
|
|
11
|
+
"postinstall.mjs",
|
|
11
12
|
"README.md",
|
|
12
13
|
"LICENSE"
|
|
13
14
|
],
|
|
@@ -49,6 +50,7 @@
|
|
|
49
50
|
"scripts": {
|
|
50
51
|
"build": "tsup",
|
|
51
52
|
"build:sea": "tsup --config tsup.sea.config.ts",
|
|
53
|
+
"postinstall": "node postinstall.mjs",
|
|
52
54
|
"typecheck": "tsc",
|
|
53
55
|
"test": "vitest run",
|
|
54
56
|
"dev": "tsx src/cli.tsx"
|
package/postinstall.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Refresh the create-payment-credential skill whenever the CLI is (re)installed
|
|
4
|
+
// or upgraded via npm. Delegates to the openclaw `skills` CLI so the skill file
|
|
5
|
+
// stays in sync with the installed CLI version. Must never fail the install.
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const REPO = 'stripe/link-cli';
|
|
12
|
+
|
|
13
|
+
function run() {
|
|
14
|
+
// Skip when running from the source monorepo (dev `pnpm install`). Installed
|
|
15
|
+
// copies live under node_modules; the dev tree does not.
|
|
16
|
+
if (!fileURLToPath(import.meta.url).includes('node_modules')) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (process.env.CI || process.env.LINK_CLI_SKIP_SKILL_INSTALL) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
process.stdout.write(
|
|
25
|
+
'link-cli: refreshing the create-payment-credential skill…\n',
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const result = spawnSync(
|
|
29
|
+
'npx',
|
|
30
|
+
['--yes', 'skills', 'add', REPO, '-g', '-y'],
|
|
31
|
+
{
|
|
32
|
+
stdio: 'inherit',
|
|
33
|
+
timeout: 60_000,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (result.error || result.status !== 0) {
|
|
38
|
+
process.stdout.write(
|
|
39
|
+
`link-cli: skipped skill refresh; run 'npx skills add ${REPO}' manually.\n`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
run();
|
|
46
|
+
} catch {
|
|
47
|
+
// Never fail the install.
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
process.exit(0);
|