@farthershore/cli 0.3.6 → 0.3.8
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 +160 -0
- package/dist/client.d.ts +31 -0
- package/dist/client.js +32 -3
- package/dist/commands/apply.js +74 -20
- package/dist/commands/billing.d.ts +3 -0
- package/dist/commands/billing.js +59 -0
- package/dist/commands/feature.d.ts +3 -0
- package/dist/commands/feature.js +80 -0
- package/dist/commands/helpers.d.ts +15 -0
- package/dist/commands/helpers.js +93 -0
- package/dist/commands/login.js +47 -38
- package/dist/commands/meter.d.ts +3 -0
- package/dist/commands/meter.js +94 -0
- package/dist/commands/plan-transition.d.ts +40 -0
- package/dist/commands/plan-transition.js +504 -0
- package/dist/commands/plan.d.ts +3 -0
- package/dist/commands/plan.js +185 -0
- package/dist/commands/product.d.ts +3 -0
- package/dist/commands/product.js +101 -0
- package/dist/commands/transition.d.ts +3 -0
- package/dist/commands/transition.js +63 -0
- package/dist/index.js +14 -0
- package/dist/types.d.ts +2 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -25,6 +25,9 @@ farthershore set-key
|
|
|
25
25
|
# Non-interactive — pass token directly
|
|
26
26
|
farthershore set-key mk_xxx
|
|
27
27
|
|
|
28
|
+
# Agent-friendly alias
|
|
29
|
+
farthershore auth set-key mk_xxx
|
|
30
|
+
|
|
28
31
|
# Environment variable (CI / agents)
|
|
29
32
|
export FARTHERSHORE_TOKEN=mk_xxx
|
|
30
33
|
```
|
|
@@ -54,6 +57,125 @@ The created repo contains:
|
|
|
54
57
|
- `docs/` — developer portal documentation
|
|
55
58
|
- `.skills/` — task-specific instructions
|
|
56
59
|
|
|
60
|
+
### `farthershore product create <name>`
|
|
61
|
+
|
|
62
|
+
Create a product through the API and select it as the active product for
|
|
63
|
+
subsequent CLI commands. This is the fastest path for an agent that wants
|
|
64
|
+
to configure a product without opening the UI.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
farthershore product create weather-api \
|
|
68
|
+
--base-url https://api.example.com \
|
|
69
|
+
--strategy prepaid_credits \
|
|
70
|
+
--format json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### `farthershore product status`
|
|
74
|
+
|
|
75
|
+
Return the latest accepted internal product config metadata, including
|
|
76
|
+
the accepted config hash and GitHub sync state.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
farthershore product status --format json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `farthershore product config`
|
|
83
|
+
|
|
84
|
+
Print the latest accepted product config from FartherShore. GitHub, the
|
|
85
|
+
UI, and the CLI are proposal surfaces; the accepted internal config is the
|
|
86
|
+
valid source of truth.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
farthershore product config --format json
|
|
90
|
+
farthershore product config --format yaml
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `farthershore product attempts`
|
|
94
|
+
|
|
95
|
+
List rejected config proposals. Invalid proposals are recorded for
|
|
96
|
+
diagnostics but do not replace the accepted internal config or mutate live
|
|
97
|
+
product state.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
farthershore product attempts --format json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `farthershore meter`
|
|
104
|
+
|
|
105
|
+
Create and manage usage meters on the accepted product config.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
farthershore meter add ai_tokens \
|
|
109
|
+
--selector model \
|
|
110
|
+
--measure input_tokens \
|
|
111
|
+
--measure output_tokens \
|
|
112
|
+
--rating provider_catalog \
|
|
113
|
+
--catalog llm_models \
|
|
114
|
+
--format json
|
|
115
|
+
|
|
116
|
+
farthershore meter list --format json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `farthershore feature`
|
|
120
|
+
|
|
121
|
+
Create features and bind or unbind them from plans.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
farthershore feature add chat_completions \
|
|
125
|
+
--route POST:/v1/chat/completions \
|
|
126
|
+
--format json
|
|
127
|
+
|
|
128
|
+
farthershore feature bind chat_completions --plan pro --format json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `farthershore plan`
|
|
132
|
+
|
|
133
|
+
Create and update plans in the accepted product config. Server-side
|
|
134
|
+
validation remains authoritative for rules like one free plan, duplicate
|
|
135
|
+
paid prices, and product-level billing strategy consistency.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
farthershore plan add free \
|
|
139
|
+
--free \
|
|
140
|
+
--limit ai_tokens:month:100000:enforce \
|
|
141
|
+
--feature chat_completions \
|
|
142
|
+
--format json
|
|
143
|
+
|
|
144
|
+
farthershore plan add pro \
|
|
145
|
+
--price-monthly 2900 \
|
|
146
|
+
--credits-monthly-micros 500000000 \
|
|
147
|
+
--meter ai_tokens \
|
|
148
|
+
--feature chat_completions \
|
|
149
|
+
--format json
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `farthershore billing`
|
|
153
|
+
|
|
154
|
+
Read or change the product-level billing strategy and subscriber change
|
|
155
|
+
policy.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
farthershore billing strategy get --format json
|
|
159
|
+
|
|
160
|
+
farthershore billing strategy set prepaid_credits \
|
|
161
|
+
--transition preserve_current_period \
|
|
162
|
+
--format json
|
|
163
|
+
|
|
164
|
+
farthershore billing policy set \
|
|
165
|
+
--default preserve_current_period \
|
|
166
|
+
--proration none \
|
|
167
|
+
--format json
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### `farthershore transition preview`
|
|
171
|
+
|
|
172
|
+
Preview how billing-affecting config changes apply to existing
|
|
173
|
+
subscribers before applying them.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
farthershore transition preview --format json
|
|
177
|
+
```
|
|
178
|
+
|
|
57
179
|
### `farthershore validate [file]`
|
|
58
180
|
|
|
59
181
|
Validate a local `product.yaml` file without making any API calls. Checks structure, required fields, and launch constraints for env-scoped plans and meters.
|
|
@@ -76,6 +198,9 @@ farthershore apply
|
|
|
76
198
|
|
|
77
199
|
# Or pass the product slug
|
|
78
200
|
farthershore apply my-weather-api
|
|
201
|
+
|
|
202
|
+
# Validate and remote-compile without applying
|
|
203
|
+
farthershore apply --dry-run --format json
|
|
79
204
|
```
|
|
80
205
|
|
|
81
206
|
### `farthershore set-key [token]`
|
|
@@ -120,6 +245,41 @@ farthershore validate
|
|
|
120
245
|
git add -A && git commit -m "Configure product" && git push
|
|
121
246
|
```
|
|
122
247
|
|
|
248
|
+
Agents can also complete the flow entirely through API-backed CLI
|
|
249
|
+
commands:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
farthershore auth set-key "$FARTHERSHORE_TOKEN"
|
|
253
|
+
farthershore product create weather-api \
|
|
254
|
+
--base-url https://api.example.com \
|
|
255
|
+
--strategy prepaid_credits \
|
|
256
|
+
--format json
|
|
257
|
+
farthershore meter add ai_tokens \
|
|
258
|
+
--selector model \
|
|
259
|
+
--measure input_tokens \
|
|
260
|
+
--measure output_tokens \
|
|
261
|
+
--rating provider_catalog \
|
|
262
|
+
--catalog llm_models \
|
|
263
|
+
--format json
|
|
264
|
+
farthershore feature add chat_completions \
|
|
265
|
+
--route POST:/v1/chat/completions \
|
|
266
|
+
--format json
|
|
267
|
+
farthershore plan add free \
|
|
268
|
+
--free \
|
|
269
|
+
--limit ai_tokens:month:100000:enforce \
|
|
270
|
+
--feature chat_completions \
|
|
271
|
+
--format json
|
|
272
|
+
farthershore plan add pro \
|
|
273
|
+
--price-monthly 2900 \
|
|
274
|
+
--credits-monthly-micros 500000000 \
|
|
275
|
+
--meter ai_tokens \
|
|
276
|
+
--feature chat_completions \
|
|
277
|
+
--format json
|
|
278
|
+
farthershore transition preview --format json
|
|
279
|
+
farthershore apply --dry-run --format json
|
|
280
|
+
farthershore product status --format json
|
|
281
|
+
```
|
|
282
|
+
|
|
123
283
|
## Global Flags
|
|
124
284
|
|
|
125
285
|
| Flag | Description |
|
package/dist/client.d.ts
CHANGED
|
@@ -23,14 +23,43 @@ export declare function createClient(opts: {
|
|
|
23
23
|
}>;
|
|
24
24
|
}>;
|
|
25
25
|
listProducts: () => Promise<Product[]>;
|
|
26
|
+
createProduct: (data: {
|
|
27
|
+
name: string;
|
|
28
|
+
baseUrl?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
displayName?: string;
|
|
31
|
+
billingStrategy?: string;
|
|
32
|
+
subscriberChangePolicy?: unknown;
|
|
33
|
+
}) => Promise<Product>;
|
|
26
34
|
initProduct: (data: {
|
|
27
35
|
name: string;
|
|
28
36
|
baseUrl?: string;
|
|
29
37
|
description?: string;
|
|
30
38
|
displayName?: string;
|
|
39
|
+
billingStrategy?: string;
|
|
40
|
+
subscriberChangePolicy?: unknown;
|
|
31
41
|
}) => Promise<InitProductResponse>;
|
|
42
|
+
updateProduct: (productId: string, data: Record<string, unknown>, opts?: {
|
|
43
|
+
env?: string;
|
|
44
|
+
}) => Promise<Product>;
|
|
45
|
+
createPlan: (productId: string, data: unknown, opts?: {
|
|
46
|
+
env?: string;
|
|
47
|
+
}) => Promise<unknown>;
|
|
48
|
+
updatePlan: (productId: string, planId: string, data: unknown, opts?: {
|
|
49
|
+
env?: string;
|
|
50
|
+
}) => Promise<unknown>;
|
|
51
|
+
deletePlan: (productId: string, planId: string, opts?: {
|
|
52
|
+
env?: string;
|
|
53
|
+
}) => Promise<void>;
|
|
54
|
+
getProductConfig: (productId: string) => Promise<unknown>;
|
|
55
|
+
getProductConfigYaml: (productId: string) => Promise<string>;
|
|
56
|
+
getProductAttempts: (productId: string) => Promise<unknown>;
|
|
57
|
+
updateProductConfig: (productId: string, spec: unknown, opts?: {
|
|
58
|
+
dryRun?: boolean;
|
|
59
|
+
}) => Promise<unknown>;
|
|
32
60
|
compileProduct: (productId: string, opts?: {
|
|
33
61
|
branch?: string;
|
|
62
|
+
dryRun?: boolean;
|
|
34
63
|
}) => Promise<{
|
|
35
64
|
success: boolean;
|
|
36
65
|
errors?: CompileDiagnostic[];
|
|
@@ -38,6 +67,7 @@ export declare function createClient(opts: {
|
|
|
38
67
|
}>;
|
|
39
68
|
managementCompileSelf: (opts?: {
|
|
40
69
|
branch?: string;
|
|
70
|
+
dryRun?: boolean;
|
|
41
71
|
}) => Promise<{
|
|
42
72
|
success: boolean;
|
|
43
73
|
productId: string;
|
|
@@ -47,6 +77,7 @@ export declare function createClient(opts: {
|
|
|
47
77
|
}>;
|
|
48
78
|
managementCompile: (productId: string, opts?: {
|
|
49
79
|
branch?: string;
|
|
80
|
+
dryRun?: boolean;
|
|
50
81
|
}) => Promise<{
|
|
51
82
|
success: boolean;
|
|
52
83
|
errors?: CompileDiagnostic[];
|
package/dist/client.js
CHANGED
|
@@ -34,20 +34,49 @@ export function createClient(opts) {
|
|
|
34
34
|
return undefined;
|
|
35
35
|
return res.json();
|
|
36
36
|
}
|
|
37
|
+
async function requestText(method, path) {
|
|
38
|
+
const res = await fetch(`${opts.apiUrl}${path}`, {
|
|
39
|
+
method,
|
|
40
|
+
headers: { Authorization: `Bearer ${opts.token}` },
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok)
|
|
43
|
+
throw new CliError(res.statusText, res.status);
|
|
44
|
+
return res.text();
|
|
45
|
+
}
|
|
37
46
|
return {
|
|
38
47
|
// --- Auth ---
|
|
39
48
|
bootstrap: () => request("POST", "/builder/context/bootstrap"),
|
|
40
49
|
// --- Products ---
|
|
41
50
|
listProducts: () => request("GET", "/products"),
|
|
51
|
+
createProduct: (data) => request("POST", "/products", data),
|
|
42
52
|
initProduct: (data) => request("POST", "/products/init", data),
|
|
53
|
+
updateProduct: (productId, data, opts) => request("PATCH", `/products/${productId}${opts?.env ? `?env=${encodeURIComponent(opts.env)}` : ""}`, data),
|
|
54
|
+
createPlan: (productId, data, opts) => request("POST", `/products/${productId}/plans${opts?.env ? `?env=${encodeURIComponent(opts.env)}` : ""}`, data),
|
|
55
|
+
updatePlan: (productId, planId, data, opts) => request("PATCH", `/products/${productId}/plans/${planId}${opts?.env ? `?env=${encodeURIComponent(opts.env)}` : ""}`, data),
|
|
56
|
+
deletePlan: (productId, planId, opts) => request("DELETE", `/products/${productId}/plans/${planId}${opts?.env ? `?env=${encodeURIComponent(opts.env)}` : ""}`),
|
|
57
|
+
getProductConfig: (productId) => request("GET", `/products/${productId}/config?format=json`),
|
|
58
|
+
getProductConfigYaml: (productId) => requestText("GET", `/products/${productId}/config?format=yaml`),
|
|
59
|
+
getProductAttempts: (productId) => request("GET", `/products/${productId}/config-attempts`),
|
|
60
|
+
updateProductConfig: (productId, spec, opts) => request("PATCH", `/products/${productId}/config`, {
|
|
61
|
+
spec,
|
|
62
|
+
dryRun: opts?.dryRun === true,
|
|
63
|
+
}),
|
|
43
64
|
// --- Compile ---
|
|
44
|
-
compileProduct: (productId, opts) => request("POST", `/products/${productId}/compile`, opts
|
|
65
|
+
compileProduct: (productId, opts) => request("POST", `/products/${productId}/compile`, compileBody(opts)),
|
|
45
66
|
// --- Management (maker token) ---
|
|
46
67
|
// Compile the product associated with the token — no product ID needed.
|
|
47
68
|
// Pass `branch` to scope compilation to an env branch's plans.
|
|
48
|
-
managementCompileSelf: (opts) => request("POST", "/management/compile", opts
|
|
49
|
-
managementCompile: (productId, opts) => request("POST", `/management/products/${productId}/compile`, opts
|
|
69
|
+
managementCompileSelf: (opts) => request("POST", "/management/compile", compileBody(opts)),
|
|
70
|
+
managementCompile: (productId, opts) => request("POST", `/management/products/${productId}/compile`, compileBody(opts)),
|
|
50
71
|
managementListProducts: () => request("GET", "/management/products"),
|
|
51
72
|
isMakerToken: () => opts.token.startsWith("mk_"),
|
|
52
73
|
};
|
|
53
74
|
}
|
|
75
|
+
function compileBody(opts) {
|
|
76
|
+
const body = {};
|
|
77
|
+
if (opts?.branch)
|
|
78
|
+
body.branch = opts.branch;
|
|
79
|
+
if (opts?.dryRun)
|
|
80
|
+
body.dryRun = true;
|
|
81
|
+
return Object.keys(body).length > 0 ? body : undefined;
|
|
82
|
+
}
|
package/dist/commands/apply.js
CHANGED
|
@@ -49,7 +49,7 @@ function readSlugFromProductYaml() {
|
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
function validateLocalProductYamlBeforeRemoteCompile() {
|
|
52
|
+
function validateLocalProductYamlBeforeRemoteCompile(format) {
|
|
53
53
|
const filePath = resolve("product.yaml");
|
|
54
54
|
if (!existsSync(filePath))
|
|
55
55
|
return true;
|
|
@@ -61,7 +61,19 @@ function validateLocalProductYamlBeforeRemoteCompile() {
|
|
|
61
61
|
if (CI) {
|
|
62
62
|
console.log(`::error file=product.yaml::${message}`);
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
if (format === "json") {
|
|
65
|
+
console.log(output.json({
|
|
66
|
+
ok: false,
|
|
67
|
+
success: false,
|
|
68
|
+
phase: "local_validation",
|
|
69
|
+
errors: [{ message }],
|
|
70
|
+
warnings: [],
|
|
71
|
+
nextActions: ["Fix product.yaml and run apply again"],
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
output.error(message);
|
|
76
|
+
}
|
|
65
77
|
process.exitCode = 1;
|
|
66
78
|
return false;
|
|
67
79
|
}
|
|
@@ -75,24 +87,38 @@ function validateLocalProductYamlBeforeRemoteCompile() {
|
|
|
75
87
|
console.log(`::warning file=product.yaml::${warning}`);
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
if (format === "json") {
|
|
91
|
+
console.log(output.json({
|
|
92
|
+
ok: false,
|
|
93
|
+
success: false,
|
|
94
|
+
phase: "local_validation",
|
|
95
|
+
errors: result.errors.map((message) => ({ message })),
|
|
96
|
+
warnings: result.warnings.map((message) => ({ message })),
|
|
97
|
+
nextActions: ["Fix product.yaml and run apply again"],
|
|
98
|
+
}));
|
|
81
99
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for (const
|
|
85
|
-
|
|
100
|
+
else {
|
|
101
|
+
output.error("Local product.yaml failed validation; remote compile was not started.\n");
|
|
102
|
+
for (const err of result.errors) {
|
|
103
|
+
console.log(` • ${err}`);
|
|
104
|
+
}
|
|
105
|
+
if (result.warnings.length > 0) {
|
|
106
|
+
console.log();
|
|
107
|
+
for (const warning of result.warnings) {
|
|
108
|
+
output.warn(warning);
|
|
109
|
+
}
|
|
86
110
|
}
|
|
87
111
|
}
|
|
88
112
|
process.exitCode = 1;
|
|
89
113
|
return false;
|
|
90
114
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
115
|
+
if (format !== "json") {
|
|
116
|
+
output.success("Local product.yaml passed validation");
|
|
117
|
+
for (const warning of result.warnings) {
|
|
118
|
+
output.warn(warning);
|
|
119
|
+
}
|
|
120
|
+
output.info("Remote compile checks the pushed branch state; unpushed local edits are not included.");
|
|
94
121
|
}
|
|
95
|
-
output.info("Remote compile checks the pushed branch state; unpushed local edits are not included.");
|
|
96
122
|
return true;
|
|
97
123
|
}
|
|
98
124
|
function shouldValidateLocalProductYaml(productArg) {
|
|
@@ -134,6 +160,12 @@ async function resolveProductId(client, arg) {
|
|
|
134
160
|
return null;
|
|
135
161
|
}
|
|
136
162
|
}
|
|
163
|
+
function compileOptions(branch, dryRun) {
|
|
164
|
+
return {
|
|
165
|
+
...(branch ? { branch } : {}),
|
|
166
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
137
169
|
// Render a diagnostic with an optional plan-key prefix so users can see which
|
|
138
170
|
// plan an error belongs to when the compiler scopes it. `compiledPlanId`
|
|
139
171
|
// itself is an internal pointer — `planKey` is the human-readable label.
|
|
@@ -142,7 +174,7 @@ function formatDiag(d) {
|
|
|
142
174
|
const planLabel = d.planKey ? `(plan: ${d.planKey}) ` : "";
|
|
143
175
|
return `${prefix}${planLabel}${d.message}`;
|
|
144
176
|
}
|
|
145
|
-
function handleResult(result) {
|
|
177
|
+
function handleResult(result, format) {
|
|
146
178
|
// GitHub Actions annotations
|
|
147
179
|
if (CI) {
|
|
148
180
|
for (const err of result.errors ?? []) {
|
|
@@ -152,6 +184,20 @@ function handleResult(result) {
|
|
|
152
184
|
console.log(`::warning file=product.yaml::${formatDiag(w)}`);
|
|
153
185
|
}
|
|
154
186
|
}
|
|
187
|
+
if (format === "json") {
|
|
188
|
+
console.log(output.json({
|
|
189
|
+
ok: result.success,
|
|
190
|
+
success: result.success,
|
|
191
|
+
errors: result.errors ?? [],
|
|
192
|
+
warnings: result.warnings ?? [],
|
|
193
|
+
nextActions: result.success
|
|
194
|
+
? ["Product config is valid for apply"]
|
|
195
|
+
: ["Fix errors and run farthershore apply --dry-run --format json"],
|
|
196
|
+
}));
|
|
197
|
+
if (!result.success)
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
155
201
|
if (result.success) {
|
|
156
202
|
output.success("Remote compile passed");
|
|
157
203
|
if (result.warnings?.length) {
|
|
@@ -176,11 +222,15 @@ export function registerApplyCommand(program, getClient) {
|
|
|
176
222
|
"Unpushed local edits are not included. Pass a product slug, or run inside a product repo to auto-detect from product.yaml. " +
|
|
177
223
|
"Automatically scopes to the current git branch so env branches compile against their own plans.")
|
|
178
224
|
.option("--branch <branch>", "Override the branch used for env-scoped compilation (default: auto-detected)")
|
|
225
|
+
.option("--dry-run", "Run local validation and remote compile without applying product state")
|
|
179
226
|
.action(async (productArg, opts) => {
|
|
180
227
|
const client = getClient();
|
|
228
|
+
const globalFormat = program.opts().format;
|
|
229
|
+
const format = globalFormat === "json" ? "json" : "table";
|
|
181
230
|
const branch = detectBranch(opts.branch);
|
|
231
|
+
const compileOpts = compileOptions(branch, opts.dryRun);
|
|
182
232
|
if (shouldValidateLocalProductYaml(productArg) &&
|
|
183
|
-
!validateLocalProductYamlBeforeRemoteCompile()) {
|
|
233
|
+
!validateLocalProductYamlBeforeRemoteCompile(format)) {
|
|
184
234
|
return;
|
|
185
235
|
}
|
|
186
236
|
if (branch && CI) {
|
|
@@ -191,8 +241,8 @@ export function registerApplyCommand(program, getClient) {
|
|
|
191
241
|
// no slug lookup, single API call.
|
|
192
242
|
if (client.isMakerToken() && !productArg) {
|
|
193
243
|
try {
|
|
194
|
-
const result = await client.managementCompileSelf(
|
|
195
|
-
handleResult(result);
|
|
244
|
+
const result = await client.managementCompileSelf(compileOpts);
|
|
245
|
+
handleResult(result, format);
|
|
196
246
|
return;
|
|
197
247
|
}
|
|
198
248
|
catch (err) {
|
|
@@ -216,9 +266,13 @@ export function registerApplyCommand(program, getClient) {
|
|
|
216
266
|
}
|
|
217
267
|
try {
|
|
218
268
|
const result = client.isMakerToken()
|
|
219
|
-
? await client.managementCompile(productId, {
|
|
220
|
-
|
|
221
|
-
|
|
269
|
+
? await client.managementCompile(productId, {
|
|
270
|
+
...compileOpts,
|
|
271
|
+
})
|
|
272
|
+
: await client.compileProduct(productId, {
|
|
273
|
+
...compileOpts,
|
|
274
|
+
});
|
|
275
|
+
handleResult(result, format);
|
|
222
276
|
}
|
|
223
277
|
catch (err) {
|
|
224
278
|
const msg = err instanceof Error ? err.message : "Compilation check failed";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { loadAcceptedSpec, objectAt, printResult, resolveProductId, updateSpec, } from "./helpers.js";
|
|
2
|
+
export function registerBillingCommands(program, getClient) {
|
|
3
|
+
const billing = program.command("billing").description("Manage billing");
|
|
4
|
+
const strategy = billing.command("strategy").description("Billing strategy");
|
|
5
|
+
strategy
|
|
6
|
+
.command("get")
|
|
7
|
+
.option("--product <product>", "Product id or name")
|
|
8
|
+
.action(async (opts) => {
|
|
9
|
+
const client = getClient();
|
|
10
|
+
const productId = await resolveProductId(client, opts.product);
|
|
11
|
+
const config = (await client.getProductConfig(productId));
|
|
12
|
+
printResult(program, "Billing strategy loaded", {
|
|
13
|
+
ok: true,
|
|
14
|
+
productId,
|
|
15
|
+
strategy: config.acceptedConfig?.billing?.strategy ?? null,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
strategy
|
|
19
|
+
.command("set <strategy>")
|
|
20
|
+
.option("--product <product>", "Product id or name")
|
|
21
|
+
.option("--transition <action>", "Default subscriber transition action", "preserve_current_period")
|
|
22
|
+
.option("--proration <mode>", "Proration mode", "none")
|
|
23
|
+
.option("--dry-run", "Validate without mutating")
|
|
24
|
+
.action(async (nextStrategy, opts) => {
|
|
25
|
+
const client = getClient();
|
|
26
|
+
const productId = await resolveProductId(client, opts.product);
|
|
27
|
+
const spec = await loadAcceptedSpec(client, productId);
|
|
28
|
+
const billing = objectAt(spec, "billing");
|
|
29
|
+
billing.strategy = nextStrategy;
|
|
30
|
+
billing.subscriberChangePolicy = {
|
|
31
|
+
default: opts.transition,
|
|
32
|
+
proration: opts.proration,
|
|
33
|
+
};
|
|
34
|
+
await updateSpec(program, client, productId, spec, {
|
|
35
|
+
dryRun: opts.dryRun,
|
|
36
|
+
message: "Billing strategy updated",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
billing
|
|
40
|
+
.command("policy set")
|
|
41
|
+
.option("--product <product>", "Product id or name")
|
|
42
|
+
.option("--default <action>", "Default subscriber action", "preserve_current_period")
|
|
43
|
+
.option("--proration <mode>", "Proration mode", "none")
|
|
44
|
+
.option("--dry-run", "Validate without mutating")
|
|
45
|
+
.action(async (opts) => {
|
|
46
|
+
const client = getClient();
|
|
47
|
+
const productId = await resolveProductId(client, opts.product);
|
|
48
|
+
const spec = await loadAcceptedSpec(client, productId);
|
|
49
|
+
const billing = objectAt(spec, "billing");
|
|
50
|
+
billing.subscriberChangePolicy = {
|
|
51
|
+
default: opts.default,
|
|
52
|
+
proration: opts.proration,
|
|
53
|
+
};
|
|
54
|
+
await updateSpec(program, client, productId, spec, {
|
|
55
|
+
dryRun: opts.dryRun,
|
|
56
|
+
message: "Billing policy updated",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { findPlan, loadAcceptedSpec, objectAt, printResult, resolveProductId, stringArrayAt, updateSpec, } from "./helpers.js";
|
|
2
|
+
export function registerFeatureCommands(program, getClient) {
|
|
3
|
+
const feature = program.command("feature").description("Manage features");
|
|
4
|
+
feature
|
|
5
|
+
.command("list")
|
|
6
|
+
.option("--product <product>", "Product id or name")
|
|
7
|
+
.action(async (opts) => {
|
|
8
|
+
const client = getClient();
|
|
9
|
+
const productId = await resolveProductId(client, opts.product);
|
|
10
|
+
const spec = await loadAcceptedSpec(client, productId);
|
|
11
|
+
printResult(program, "Features loaded", {
|
|
12
|
+
ok: true,
|
|
13
|
+
productId,
|
|
14
|
+
features: objectAt(spec, "features"),
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
feature
|
|
18
|
+
.command("add <key>")
|
|
19
|
+
.option("--product <product>", "Product id or name")
|
|
20
|
+
.option("--route <route>", "Route binding METHOD:/path; repeatable", collect, [])
|
|
21
|
+
.option("--dry-run", "Validate without mutating")
|
|
22
|
+
.action(async (key, opts) => {
|
|
23
|
+
const client = getClient();
|
|
24
|
+
const productId = await resolveProductId(client, opts.product);
|
|
25
|
+
const spec = await loadAcceptedSpec(client, productId);
|
|
26
|
+
const features = objectAt(spec, "features");
|
|
27
|
+
features[key] = { routes: opts.route.map(parseRoute) };
|
|
28
|
+
await updateSpec(program, client, productId, spec, {
|
|
29
|
+
dryRun: opts.dryRun,
|
|
30
|
+
message: `Feature "${key}" saved`,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
feature
|
|
34
|
+
.command("bind <key>")
|
|
35
|
+
.requiredOption("--plan <plan>", "Plan key")
|
|
36
|
+
.option("--product <product>", "Product id or name")
|
|
37
|
+
.option("--dry-run", "Validate without mutating")
|
|
38
|
+
.action(async (key, opts) => {
|
|
39
|
+
const client = getClient();
|
|
40
|
+
const productId = await resolveProductId(client, opts.product);
|
|
41
|
+
const spec = await loadAcceptedSpec(client, productId);
|
|
42
|
+
const plan = findPlan(spec, opts.plan);
|
|
43
|
+
const features = stringArrayAt(plan, "features");
|
|
44
|
+
if (!features.includes(key))
|
|
45
|
+
features.push(key);
|
|
46
|
+
await updateSpec(program, client, productId, spec, {
|
|
47
|
+
dryRun: opts.dryRun,
|
|
48
|
+
message: `Feature "${key}" bound to "${opts.plan}"`,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
feature
|
|
52
|
+
.command("unbind <key>")
|
|
53
|
+
.requiredOption("--plan <plan>", "Plan key")
|
|
54
|
+
.option("--product <product>", "Product id or name")
|
|
55
|
+
.option("--dry-run", "Validate without mutating")
|
|
56
|
+
.action(async (key, opts) => {
|
|
57
|
+
const client = getClient();
|
|
58
|
+
const productId = await resolveProductId(client, opts.product);
|
|
59
|
+
const spec = await loadAcceptedSpec(client, productId);
|
|
60
|
+
const plan = findPlan(spec, opts.plan);
|
|
61
|
+
plan.features = stringArrayAt(plan, "features").filter((featureKey) => featureKey !== key);
|
|
62
|
+
await updateSpec(program, client, productId, spec, {
|
|
63
|
+
dryRun: opts.dryRun,
|
|
64
|
+
message: `Feature "${key}" unbound from "${opts.plan}"`,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function collect(value, previous) {
|
|
69
|
+
previous.push(value);
|
|
70
|
+
return previous;
|
|
71
|
+
}
|
|
72
|
+
function parseRoute(value) {
|
|
73
|
+
const separator = value.indexOf(":");
|
|
74
|
+
if (separator === -1)
|
|
75
|
+
return { method: "*", path: value };
|
|
76
|
+
return {
|
|
77
|
+
method: value.slice(0, separator).toUpperCase(),
|
|
78
|
+
path: value.slice(separator + 1),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { ApiClient } from "../client.js";
|
|
3
|
+
export declare function commandFormat(program: Command): "table" | "json";
|
|
4
|
+
export declare function printResult(program: Command, humanMessage: string, payload: Record<string, unknown>): void;
|
|
5
|
+
export declare function resolveProductId(client: ApiClient, productArg?: string): Promise<string>;
|
|
6
|
+
export declare function resolvePlanId(client: ApiClient, productId: string, planArg: string): Promise<string>;
|
|
7
|
+
export declare function loadAcceptedSpec(client: ApiClient, productId: string): Promise<Record<string, unknown>>;
|
|
8
|
+
export declare function plans(spec: Record<string, unknown>): Array<Record<string, unknown>>;
|
|
9
|
+
export declare function objectAt(parent: Record<string, unknown>, key: string): Record<string, unknown>;
|
|
10
|
+
export declare function stringArrayAt(parent: Record<string, unknown>, key: string): string[];
|
|
11
|
+
export declare function findPlan(spec: Record<string, unknown>, planKey: string): Record<string, unknown>;
|
|
12
|
+
export declare function updateSpec(program: Command, client: ApiClient, productId: string, spec: Record<string, unknown>, opts?: {
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
message?: string;
|
|
15
|
+
}): Promise<unknown>;
|