@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 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?.branch ? { branch: opts.branch } : undefined),
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?.branch ? { branch: opts.branch } : undefined),
49
- managementCompile: (productId, opts) => request("POST", `/management/products/${productId}/compile`, opts?.branch ? { branch: opts.branch } : undefined),
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
+ }
@@ -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
- output.error(message);
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
- output.error("Local product.yaml failed validation; remote compile was not started.\n");
79
- for (const err of result.errors) {
80
- console.log(` • ${err}`);
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
- if (result.warnings.length > 0) {
83
- console.log();
84
- for (const warning of result.warnings) {
85
- output.warn(warning);
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
- output.success("Local product.yaml passed validation");
92
- for (const warning of result.warnings) {
93
- output.warn(warning);
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({ branch });
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, { branch })
220
- : await client.compileProduct(productId, { branch });
221
- handleResult(result);
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,3 @@
1
+ import { Command } from "commander";
2
+ import type { ApiClient } from "../client.js";
3
+ export declare function registerBillingCommands(program: Command, getClient: () => ApiClient): void;
@@ -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,3 @@
1
+ import { Command } from "commander";
2
+ import type { ApiClient } from "../client.js";
3
+ export declare function registerFeatureCommands(program: Command, getClient: () => ApiClient): void;
@@ -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>;