@farthershore/cli 0.3.7 → 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/auth.js +17 -0
- package/dist/build-info.js +10 -0
- package/dist/client.d.ts +31 -0
- package/dist/client.js +82 -0
- package/dist/commands/apply.js +285 -0
- 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/init.js +43 -0
- package/dist/commands/login.js +129 -0
- 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/commands/validate.js +206 -0
- package/dist/config.js +58 -0
- package/dist/index.js +76 -1234
- package/dist/output.js +28 -0
- package/dist/remediation.js +53 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +23 -0
- package/package.json +5 -8
|
@@ -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>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { loadConfig } from "../config.js";
|
|
2
|
+
import * as output from "../output.js";
|
|
3
|
+
export function commandFormat(program) {
|
|
4
|
+
return output.outputFormat(program.opts().format);
|
|
5
|
+
}
|
|
6
|
+
export function printResult(program, humanMessage, payload) {
|
|
7
|
+
if (commandFormat(program) === "json") {
|
|
8
|
+
console.log(output.json(payload));
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
output.success(humanMessage);
|
|
12
|
+
}
|
|
13
|
+
export async function resolveProductId(client, productArg) {
|
|
14
|
+
const config = loadConfig();
|
|
15
|
+
if (productArg) {
|
|
16
|
+
if (productArg.length === 36)
|
|
17
|
+
return productArg;
|
|
18
|
+
const products = client.isMakerToken()
|
|
19
|
+
? await client.managementListProducts()
|
|
20
|
+
: await client.listProducts();
|
|
21
|
+
const product = products.find((p) => p.id === productArg ||
|
|
22
|
+
p.name.toLowerCase() === productArg.toLowerCase());
|
|
23
|
+
if (product)
|
|
24
|
+
return product.id;
|
|
25
|
+
}
|
|
26
|
+
if (config.activeProductId)
|
|
27
|
+
return config.activeProductId;
|
|
28
|
+
throw new Error("No product selected. Pass --product <id-or-name> or run `farthershore product create` first.");
|
|
29
|
+
}
|
|
30
|
+
export async function resolvePlanId(client, productId, planArg) {
|
|
31
|
+
if (planArg.length === 36)
|
|
32
|
+
return planArg;
|
|
33
|
+
const products = client.isMakerToken()
|
|
34
|
+
? await client.managementListProducts()
|
|
35
|
+
: await client.listProducts();
|
|
36
|
+
const product = products.find((p) => p.id === productId);
|
|
37
|
+
const plan = product?.plans?.find((p) => p.id === planArg ||
|
|
38
|
+
p.key === planArg ||
|
|
39
|
+
p.name.toLowerCase() === planArg.toLowerCase());
|
|
40
|
+
if (!plan)
|
|
41
|
+
throw new Error(`Plan "${planArg}" not found`);
|
|
42
|
+
return plan.id;
|
|
43
|
+
}
|
|
44
|
+
export async function loadAcceptedSpec(client, productId) {
|
|
45
|
+
const config = (await client.getProductConfig(productId));
|
|
46
|
+
if (!config.acceptedConfig ||
|
|
47
|
+
typeof config.acceptedConfig !== "object" ||
|
|
48
|
+
Array.isArray(config.acceptedConfig)) {
|
|
49
|
+
throw new Error("Product has no accepted internal config yet.");
|
|
50
|
+
}
|
|
51
|
+
return config.acceptedConfig;
|
|
52
|
+
}
|
|
53
|
+
export function plans(spec) {
|
|
54
|
+
if (!Array.isArray(spec.plans))
|
|
55
|
+
spec.plans = [];
|
|
56
|
+
return spec.plans;
|
|
57
|
+
}
|
|
58
|
+
export function objectAt(parent, key) {
|
|
59
|
+
const current = parent[key];
|
|
60
|
+
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
61
|
+
return current;
|
|
62
|
+
}
|
|
63
|
+
const next = {};
|
|
64
|
+
parent[key] = next;
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
export function stringArrayAt(parent, key) {
|
|
68
|
+
if (!Array.isArray(parent[key]))
|
|
69
|
+
parent[key] = [];
|
|
70
|
+
const values = parent[key];
|
|
71
|
+
const strings = values.filter((value) => typeof value === "string");
|
|
72
|
+
if (strings.length !== values.length)
|
|
73
|
+
parent[key] = strings;
|
|
74
|
+
return parent[key];
|
|
75
|
+
}
|
|
76
|
+
export function findPlan(spec, planKey) {
|
|
77
|
+
const plan = plans(spec).find((candidate) => candidate.key === planKey);
|
|
78
|
+
if (!plan)
|
|
79
|
+
throw new Error(`Plan "${planKey}" not found`);
|
|
80
|
+
return plan;
|
|
81
|
+
}
|
|
82
|
+
export async function updateSpec(program, client, productId, spec, opts) {
|
|
83
|
+
const result = await client.updateProductConfig(productId, spec, {
|
|
84
|
+
dryRun: opts?.dryRun,
|
|
85
|
+
});
|
|
86
|
+
printResult(program, opts?.message ?? "Product config updated", {
|
|
87
|
+
ok: true,
|
|
88
|
+
productId,
|
|
89
|
+
dryRun: opts?.dryRun === true,
|
|
90
|
+
...(typeof result === "object" && result ? result : {}),
|
|
91
|
+
});
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as output from "../output.js";
|
|
2
|
+
export function registerInitCommand(program, getClient) {
|
|
3
|
+
program
|
|
4
|
+
.command("init <name>")
|
|
5
|
+
.description("Create a new product with a GitHub repo for agent-first configuration")
|
|
6
|
+
.option("--base-url <url>", "Backend API base URL")
|
|
7
|
+
.option("--description <desc>", "Product description")
|
|
8
|
+
.option("--display-name <name>", "Display name")
|
|
9
|
+
.action(async (name, opts) => {
|
|
10
|
+
const client = getClient();
|
|
11
|
+
const result = await client.initProduct({
|
|
12
|
+
name,
|
|
13
|
+
baseUrl: opts.baseUrl,
|
|
14
|
+
description: opts.description,
|
|
15
|
+
displayName: opts.displayName,
|
|
16
|
+
});
|
|
17
|
+
// `program.opts()` is typed `OptionValues` (string-indexed any).
|
|
18
|
+
// Narrow at the boundary so `outputFormat`'s `string | undefined`
|
|
19
|
+
// parameter type is satisfied without an unsafe-arg lint.
|
|
20
|
+
const formatOpt = program.opts().format;
|
|
21
|
+
const format = output.outputFormat(formatOpt);
|
|
22
|
+
if (format === "json") {
|
|
23
|
+
console.log(output.json(result));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
output.success(`Created product "${result.product.name}" (DRAFT)`);
|
|
27
|
+
console.log();
|
|
28
|
+
if (result.repo) {
|
|
29
|
+
console.log(` Repository: ${result.repo.htmlUrl}`);
|
|
30
|
+
console.log(` Clone: git clone ${result.repo.cloneUrl}`);
|
|
31
|
+
console.log();
|
|
32
|
+
}
|
|
33
|
+
console.log(" Next steps:");
|
|
34
|
+
console.log(" 1. Clone the repository");
|
|
35
|
+
console.log(" 2. Read AGENTS.md for the full configuration reference");
|
|
36
|
+
console.log(" 3. Edit product.yaml — add your base URL, plans, and meters");
|
|
37
|
+
console.log(" 4. Push to main — a valid config goes live automatically");
|
|
38
|
+
console.log();
|
|
39
|
+
if (result.agent.agentsMdUrl) {
|
|
40
|
+
console.log(` Docs: ${result.agent.agentsMdUrl}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as readline from "node:readline/promises";
|
|
2
|
+
import { createClient } from "../client.js";
|
|
3
|
+
import { loadConfig, saveConfig, saveCredentials, clearCredentials, } from "../config.js";
|
|
4
|
+
import { resolveToken } from "../auth.js";
|
|
5
|
+
import * as output from "../output.js";
|
|
6
|
+
export function registerAuthCommands(program) {
|
|
7
|
+
const auth = program.command("auth").description("Manage authentication");
|
|
8
|
+
auth
|
|
9
|
+
.command("set-key [token]")
|
|
10
|
+
.description("Set your API token")
|
|
11
|
+
.action(async (tokenArg) => {
|
|
12
|
+
await setKey(tokenArg);
|
|
13
|
+
});
|
|
14
|
+
program
|
|
15
|
+
.command("set-key [token]")
|
|
16
|
+
.description("Set your API token (interactive or pass as argument)")
|
|
17
|
+
.action(async (tokenArg) => {
|
|
18
|
+
await setKey(tokenArg);
|
|
19
|
+
});
|
|
20
|
+
program
|
|
21
|
+
.command("logout")
|
|
22
|
+
.description("Clear stored credentials")
|
|
23
|
+
.action(() => {
|
|
24
|
+
clearCredentials();
|
|
25
|
+
output.success("Credentials cleared.");
|
|
26
|
+
});
|
|
27
|
+
program
|
|
28
|
+
.command("whoami")
|
|
29
|
+
.description("Show current authentication context")
|
|
30
|
+
.action(async () => {
|
|
31
|
+
const config = loadConfig();
|
|
32
|
+
const formatOpt = program.opts().format;
|
|
33
|
+
const format = output.outputFormat(formatOpt);
|
|
34
|
+
try {
|
|
35
|
+
const token = resolveToken();
|
|
36
|
+
const client = createClient({ apiUrl: config.apiUrl, token });
|
|
37
|
+
const ctx = await client.bootstrap();
|
|
38
|
+
const authSource = process.env.FARTHERSHORE_TOKEN
|
|
39
|
+
? "env:FARTHERSHORE_TOKEN"
|
|
40
|
+
: "credentials-file";
|
|
41
|
+
if (format === "json") {
|
|
42
|
+
// Machine-readable shape — stable contract for CI scripts
|
|
43
|
+
// ("am I logged in?"). Don't include the token itself.
|
|
44
|
+
console.log(output.json({
|
|
45
|
+
organization: {
|
|
46
|
+
id: ctx.activeOrganization.id,
|
|
47
|
+
name: ctx.activeOrganization.name ?? null,
|
|
48
|
+
slug: ctx.activeOrganization.slug ?? null,
|
|
49
|
+
},
|
|
50
|
+
user: { id: ctx.user.id },
|
|
51
|
+
apiUrl: config.apiUrl,
|
|
52
|
+
authSource,
|
|
53
|
+
}));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
output.heading("Current Context");
|
|
57
|
+
console.log(` Organization: ${ctx.activeOrganization.name ?? ctx.activeOrganization.id}`);
|
|
58
|
+
console.log(` API URL: ${config.apiUrl}`);
|
|
59
|
+
if (authSource === "env:FARTHERSHORE_TOKEN") {
|
|
60
|
+
output.info(" Auth: FARTHERSHORE_TOKEN env var");
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
output.info(" Auth: ~/.farthershore/credentials.json");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
if (format === "json") {
|
|
68
|
+
console.log(output.json({ authenticated: false }));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
output.error("Not authenticated. Run `farthershore set-key` or set FARTHERSHORE_TOKEN.");
|
|
72
|
+
}
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
program
|
|
77
|
+
.command("set-url <url>")
|
|
78
|
+
.description("Override the API base URL (for staging/testing)")
|
|
79
|
+
.action((url) => {
|
|
80
|
+
saveConfig({ apiUrl: url });
|
|
81
|
+
output.success(`API URL set to ${url}`);
|
|
82
|
+
});
|
|
83
|
+
program
|
|
84
|
+
.command("reset-url")
|
|
85
|
+
.description("Reset the API URL to production default")
|
|
86
|
+
.action(() => {
|
|
87
|
+
saveConfig({ apiUrl: "https://core.farthershore.com" });
|
|
88
|
+
output.success("API URL reset to https://core.farthershore.com");
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async function setKey(tokenArg) {
|
|
92
|
+
let token = tokenArg?.trim();
|
|
93
|
+
if (!token) {
|
|
94
|
+
if (!process.stdin.isTTY) {
|
|
95
|
+
output.error("No token provided. Pass it as an argument: farthershore auth set-key <token>");
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
console.log("Set your FartherShore API token\n");
|
|
100
|
+
console.log(" Create a token at https://farthershore.com/settings/tokens\n");
|
|
101
|
+
const rl = readline.createInterface({
|
|
102
|
+
input: process.stdin,
|
|
103
|
+
output: process.stdout,
|
|
104
|
+
});
|
|
105
|
+
token = (await rl.question("Token: ")).trim();
|
|
106
|
+
rl.close();
|
|
107
|
+
}
|
|
108
|
+
if (!token) {
|
|
109
|
+
output.error("No token provided.");
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const config = loadConfig();
|
|
114
|
+
try {
|
|
115
|
+
const client = createClient({ apiUrl: config.apiUrl, token });
|
|
116
|
+
const ctx = await client.bootstrap();
|
|
117
|
+
saveCredentials({
|
|
118
|
+
token,
|
|
119
|
+
orgId: ctx.activeOrganization.id,
|
|
120
|
+
userId: ctx.user.id,
|
|
121
|
+
});
|
|
122
|
+
output.success("Authenticated");
|
|
123
|
+
console.log(` Organization: ${ctx.activeOrganization.name ?? ctx.activeOrganization.id}`);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
output.error("Invalid token. Check it and try again.");
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { loadAcceptedSpec, objectAt, printResult, resolveProductId, updateSpec, } from "./helpers.js";
|
|
2
|
+
export function registerMeterCommands(program, getClient) {
|
|
3
|
+
const meter = program.command("meter").description("Manage usage meters");
|
|
4
|
+
meter
|
|
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
|
+
const usage = objectAt(spec, "usage");
|
|
12
|
+
printResult(program, "Meters loaded", {
|
|
13
|
+
ok: true,
|
|
14
|
+
productId,
|
|
15
|
+
meters: objectAt(usage, "meters"),
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
meter
|
|
19
|
+
.command("add <key>")
|
|
20
|
+
.option("--product <product>", "Product id or name")
|
|
21
|
+
.option("--selector <field>", "Selector field, for example model")
|
|
22
|
+
.option("--measure <measure>", "Measure name; repeatable", collect, [])
|
|
23
|
+
.option("--rating <source>", "Rating source", "fixed")
|
|
24
|
+
.option("--catalog <catalog>", "Catalog key for provider_catalog rating")
|
|
25
|
+
.option("--resolver <resolver>", "Resolver id for external/custom rating")
|
|
26
|
+
.option("--amount-field <path>", "Amount field for upstream_reported rating")
|
|
27
|
+
.option("--currency-field <path>", "Currency field for upstream_reported rating")
|
|
28
|
+
.option("--dry-run", "Validate without mutating")
|
|
29
|
+
.action(async (key, opts) => {
|
|
30
|
+
const client = getClient();
|
|
31
|
+
const productId = await resolveProductId(client, opts.product);
|
|
32
|
+
const spec = await loadAcceptedSpec(client, productId);
|
|
33
|
+
const usage = objectAt(spec, "usage");
|
|
34
|
+
const meters = objectAt(usage, "meters");
|
|
35
|
+
meters[key] = {
|
|
36
|
+
...(opts.selector ? { selector: opts.selector } : {}),
|
|
37
|
+
measures: opts.measure.length > 0 ? opts.measure : ["count"],
|
|
38
|
+
rating: ratingFromOptions(opts),
|
|
39
|
+
};
|
|
40
|
+
await updateSpec(program, client, productId, spec, {
|
|
41
|
+
dryRun: opts.dryRun,
|
|
42
|
+
message: `Meter "${key}" saved`,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
meter
|
|
46
|
+
.command("rating set <key>")
|
|
47
|
+
.option("--product <product>", "Product id or name")
|
|
48
|
+
.requiredOption("--rating <source>", "Rating source")
|
|
49
|
+
.option("--catalog <catalog>", "Catalog key for provider_catalog rating")
|
|
50
|
+
.option("--resolver <resolver>", "Resolver id for external/custom rating")
|
|
51
|
+
.option("--amount-field <path>", "Amount field for upstream_reported rating")
|
|
52
|
+
.option("--currency-field <path>", "Currency field for upstream_reported rating")
|
|
53
|
+
.option("--dry-run", "Validate without mutating")
|
|
54
|
+
.action(async (key, opts) => {
|
|
55
|
+
const client = getClient();
|
|
56
|
+
const productId = await resolveProductId(client, opts.product);
|
|
57
|
+
const spec = await loadAcceptedSpec(client, productId);
|
|
58
|
+
const usage = objectAt(spec, "usage");
|
|
59
|
+
const meters = objectAt(usage, "meters");
|
|
60
|
+
const current = meters[key];
|
|
61
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
62
|
+
throw new Error(`Meter "${key}" not found`);
|
|
63
|
+
}
|
|
64
|
+
current.rating = ratingFromOptions(opts);
|
|
65
|
+
await updateSpec(program, client, productId, spec, {
|
|
66
|
+
dryRun: opts.dryRun,
|
|
67
|
+
message: `Meter "${key}" rating saved`,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function collect(value, previous) {
|
|
72
|
+
previous.push(value);
|
|
73
|
+
return previous;
|
|
74
|
+
}
|
|
75
|
+
function ratingFromOptions(opts) {
|
|
76
|
+
if (opts.rating === "provider_catalog") {
|
|
77
|
+
return {
|
|
78
|
+
source: opts.rating,
|
|
79
|
+
catalog: opts.catalog ?? "default",
|
|
80
|
+
pricePolicy: "pass_through",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (opts.rating === "upstream_reported") {
|
|
84
|
+
return {
|
|
85
|
+
source: opts.rating,
|
|
86
|
+
amountField: opts.amountField ?? "usage.cost_micros",
|
|
87
|
+
...(opts.currencyField ? { currencyField: opts.currencyField } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (opts.rating === "external_rate_api" || opts.rating === "custom") {
|
|
91
|
+
return { source: opts.rating, resolver: opts.resolver ?? "default" };
|
|
92
|
+
}
|
|
93
|
+
return { source: "fixed", rates: {} };
|
|
94
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
type ChangeKind = "strategy_changed" | "price_increase" | "price_decrease" | "feature_added" | "feature_removed" | "limit_increased" | "limit_reduced" | "credit_increased" | "credit_reduced" | "rating_changed" | "plan_added" | "plan_removed";
|
|
3
|
+
type SubscriberAction = "preserve_current_period" | "switch_immediately" | "switch_immediately_prorate" | "new_subscribers_only";
|
|
4
|
+
type TransitionChange = {
|
|
5
|
+
kind: ChangeKind;
|
|
6
|
+
planKey?: string;
|
|
7
|
+
meter?: string;
|
|
8
|
+
feature?: string;
|
|
9
|
+
from?: unknown;
|
|
10
|
+
to?: unknown;
|
|
11
|
+
subscriberAction: SubscriberAction;
|
|
12
|
+
requiresAcknowledgment: boolean;
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
type TransitionAnalysis = {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
from: string;
|
|
18
|
+
to: string;
|
|
19
|
+
strategy: {
|
|
20
|
+
from?: string;
|
|
21
|
+
to?: string;
|
|
22
|
+
};
|
|
23
|
+
policy: {
|
|
24
|
+
default: SubscriberAction;
|
|
25
|
+
proration: string;
|
|
26
|
+
when: Record<string, SubscriberAction>;
|
|
27
|
+
};
|
|
28
|
+
changes: TransitionChange[];
|
|
29
|
+
errors: string[];
|
|
30
|
+
warnings: string[];
|
|
31
|
+
agentHints: string[];
|
|
32
|
+
};
|
|
33
|
+
export declare function analyzePlanTransition(options: {
|
|
34
|
+
fromSpec: unknown;
|
|
35
|
+
toSpec: unknown;
|
|
36
|
+
fromLabel?: string;
|
|
37
|
+
toLabel?: string;
|
|
38
|
+
}): TransitionAnalysis;
|
|
39
|
+
export declare function registerPlanTransitionCommand(program: Command): void;
|
|
40
|
+
export {};
|