@farthershore/cli 0.2.0 → 0.3.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 +26 -12
- package/dist/client.d.ts +13 -0
- package/dist/client.js +4 -0
- package/dist/commands/apply.js +65 -4
- package/dist/commands/login.js +16 -1
- package/dist/commands/validate.js +50 -7
- package/dist/config.js +5 -3
- package/package.json +3 -6
- package/dist/commands/billing.d.ts +0 -3
- package/dist/commands/billing.js +0 -31
- package/dist/commands/dry-run.d.ts +0 -3
- package/dist/commands/dry-run.js +0 -33
- package/dist/commands/plans.d.ts +0 -3
- package/dist/commands/plans.js +0 -204
- package/dist/commands/products.d.ts +0 -3
- package/dist/commands/products.js +0 -76
- package/dist/commands/team.d.ts +0 -3
- package/dist/commands/team.js +0 -46
- package/dist/commands/tokens.d.ts +0 -3
- package/dist/commands/tokens.js +0 -30
- package/dist/commands/usage.d.ts +0 -3
- package/dist/commands/usage.js +0 -23
package/README.md
CHANGED
|
@@ -42,12 +42,14 @@ farthershore init my-weather-api
|
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Options:
|
|
45
|
+
|
|
45
46
|
- `--base-url <url>` — Backend API base URL
|
|
46
47
|
- `--description <text>` — Product description
|
|
47
48
|
- `--display-name <name>` — Display name for the portal
|
|
48
49
|
- `--billing <strategy>` — `subscription` (default), `usage_based`, or `hybrid`
|
|
49
50
|
|
|
50
51
|
The created repo contains:
|
|
52
|
+
|
|
51
53
|
- `product.yaml` — template config (edit and push to go live)
|
|
52
54
|
- `AGENTS.md` — full configuration reference for AI agents
|
|
53
55
|
- `docs/` — developer portal documentation
|
|
@@ -65,12 +67,16 @@ farthershore validate
|
|
|
65
67
|
farthershore validate path/to/product.yaml
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
### `farthershore apply
|
|
70
|
+
### `farthershore apply [product]`
|
|
69
71
|
|
|
70
72
|
Run the server-side compiler against the current product configuration. Returns compilation errors and warnings. Exits with code 1 on failure — suitable for CI checks before merging.
|
|
71
73
|
|
|
72
74
|
```bash
|
|
73
|
-
|
|
75
|
+
# Inside a product repo (auto-detects from product.yaml)
|
|
76
|
+
farthershore apply
|
|
77
|
+
|
|
78
|
+
# Or pass the product slug
|
|
79
|
+
farthershore apply my-weather-api
|
|
74
80
|
```
|
|
75
81
|
|
|
76
82
|
### `farthershore set-key [token]`
|
|
@@ -85,6 +91,14 @@ Show your current authentication context (organization, auth method).
|
|
|
85
91
|
|
|
86
92
|
Clear stored credentials.
|
|
87
93
|
|
|
94
|
+
### `farthershore set-url <url>`
|
|
95
|
+
|
|
96
|
+
Override the API base URL (persisted in `~/.farthershore/config.json`).
|
|
97
|
+
|
|
98
|
+
### `farthershore reset-url`
|
|
99
|
+
|
|
100
|
+
Reset the API base URL to the default (`https://core.farthershore.com`).
|
|
101
|
+
|
|
88
102
|
## Agent Workflow
|
|
89
103
|
|
|
90
104
|
The CLI is designed for AI agents to create and configure API products:
|
|
@@ -109,17 +123,17 @@ git add -A && git commit -m "Configure product" && git push
|
|
|
109
123
|
|
|
110
124
|
## Global Flags
|
|
111
125
|
|
|
112
|
-
| Flag
|
|
113
|
-
|
|
126
|
+
| Flag | Description |
|
|
127
|
+
| ----------------- | ------------------------------------ |
|
|
114
128
|
| `--token <token>` | Override auth token for this command |
|
|
115
|
-
| `--api-url <url>` | Override API base URL
|
|
116
|
-
| `--format json`
|
|
117
|
-
| `--version`
|
|
118
|
-
| `--help`
|
|
129
|
+
| `--api-url <url>` | Override API base URL |
|
|
130
|
+
| `--format json` | Output as JSON (default for non-TTY) |
|
|
131
|
+
| `--version` | Show version |
|
|
132
|
+
| `--help` | Show help |
|
|
119
133
|
|
|
120
134
|
## Environment Variables
|
|
121
135
|
|
|
122
|
-
| Variable
|
|
123
|
-
|
|
124
|
-
| `FARTHERSHORE_TOKEN`
|
|
125
|
-
| `FARTHERSHORE_API_URL` | API base URL (default: `https://
|
|
136
|
+
| Variable | Description |
|
|
137
|
+
| ---------------------- | ------------------------------------------------------- |
|
|
138
|
+
| `FARTHERSHORE_TOKEN` | API token (overrides stored credentials) |
|
|
139
|
+
| `FARTHERSHORE_API_URL` | API base URL (default: `https://core.farthershore.com`) |
|
package/dist/client.d.ts
CHANGED
|
@@ -81,5 +81,18 @@ export declare function createClient(opts: {
|
|
|
81
81
|
message: string;
|
|
82
82
|
}>;
|
|
83
83
|
}>;
|
|
84
|
+
managementCompile: (productId: string) => Promise<{
|
|
85
|
+
success: boolean;
|
|
86
|
+
errors?: Array<{
|
|
87
|
+
code?: string;
|
|
88
|
+
message: string;
|
|
89
|
+
}>;
|
|
90
|
+
warnings?: Array<{
|
|
91
|
+
code?: string;
|
|
92
|
+
message: string;
|
|
93
|
+
}>;
|
|
94
|
+
}>;
|
|
95
|
+
managementListProducts: () => Promise<Product[]>;
|
|
96
|
+
isMakerToken: () => boolean;
|
|
84
97
|
};
|
|
85
98
|
export type ApiClient = ReturnType<typeof createClient>;
|
package/dist/client.js
CHANGED
|
@@ -68,5 +68,9 @@ export function createClient(opts) {
|
|
|
68
68
|
getUsage: (productId) => request("GET", `/products/${productId}/usage`),
|
|
69
69
|
// --- Compile ---
|
|
70
70
|
compileProduct: (productId) => request("POST", `/products/${productId}/compile`),
|
|
71
|
+
// --- Management (maker token) ---
|
|
72
|
+
managementCompile: (productId) => request("POST", `/management/products/${productId}/compile`),
|
|
73
|
+
managementListProducts: () => request("GET", "/management/products"),
|
|
74
|
+
isMakerToken: () => opts.token.startsWith("mk_"),
|
|
71
75
|
};
|
|
72
76
|
}
|
package/dist/commands/apply.js
CHANGED
|
@@ -1,13 +1,74 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
1
4
|
import * as output from "../output.js";
|
|
2
5
|
const CI = !!process.env.CI || !!process.env.GITHUB_ACTIONS;
|
|
6
|
+
/**
|
|
7
|
+
* Read the product name/slug from product.yaml in the current directory.
|
|
8
|
+
*/
|
|
9
|
+
function readSlugFromProductYaml() {
|
|
10
|
+
const yamlPath = resolve("product.yaml");
|
|
11
|
+
if (!existsSync(yamlPath))
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
const spec = YAML.parse(readFileSync(yamlPath, "utf-8"));
|
|
15
|
+
const product = spec.product;
|
|
16
|
+
return product?.name ?? null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a product identifier to an API product ID.
|
|
24
|
+
* If no arg, reads product.name from product.yaml in the current directory.
|
|
25
|
+
* If arg looks like a UUID, uses it directly. Otherwise treats it as a slug.
|
|
26
|
+
*/
|
|
27
|
+
async function resolveProductId(client, arg) {
|
|
28
|
+
const slug = arg ?? readSlugFromProductYaml();
|
|
29
|
+
if (!slug)
|
|
30
|
+
return null;
|
|
31
|
+
// UUID — use directly
|
|
32
|
+
if (slug.length === 36 &&
|
|
33
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(slug)) {
|
|
34
|
+
return slug;
|
|
35
|
+
}
|
|
36
|
+
// Slug — resolve via product list (use management API for maker tokens)
|
|
37
|
+
try {
|
|
38
|
+
const products = client.isMakerToken()
|
|
39
|
+
? await client.managementListProducts()
|
|
40
|
+
: await client.listProducts();
|
|
41
|
+
const match = products.find((p) => p.name === slug || p.name.toLowerCase() === slug.toLowerCase());
|
|
42
|
+
return match?.id ?? null;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
// Don't swallow network errors silently
|
|
46
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47
|
+
process.stderr.write(`Warning: Failed to resolve product slug: ${msg}\n`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
3
51
|
export function registerApplyCommand(program, getClient) {
|
|
4
52
|
program
|
|
5
|
-
.command("apply
|
|
6
|
-
.description("Run the server-side compiler to check if the current config is valid"
|
|
7
|
-
|
|
53
|
+
.command("apply [product]")
|
|
54
|
+
.description("Run the server-side compiler to check if the current config is valid. " +
|
|
55
|
+
"Pass a product slug, or run inside a product repo to auto-detect from product.yaml.")
|
|
56
|
+
.action(async (productArg) => {
|
|
8
57
|
const client = getClient();
|
|
58
|
+
const productId = await resolveProductId(client, productArg);
|
|
59
|
+
if (!productId) {
|
|
60
|
+
const hint = productArg
|
|
61
|
+
? `Product "${productArg}" not found. Check the name and try again.`
|
|
62
|
+
: "No product specified and no product.yaml found.\n" +
|
|
63
|
+
" Run from inside a product repo, or pass the slug: farthershore apply my-api";
|
|
64
|
+
output.error(hint);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
9
68
|
try {
|
|
10
|
-
const result =
|
|
69
|
+
const result = client.isMakerToken()
|
|
70
|
+
? await client.managementCompile(productId)
|
|
71
|
+
: await client.compileProduct(productId);
|
|
11
72
|
// GitHub Actions annotations
|
|
12
73
|
if (CI) {
|
|
13
74
|
for (const err of result.errors ?? []) {
|
package/dist/commands/login.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as readline from "node:readline/promises";
|
|
2
2
|
import { createClient } from "../client.js";
|
|
3
|
-
import { loadConfig, saveCredentials, clearCredentials } from "../config.js";
|
|
3
|
+
import { loadConfig, saveConfig, saveCredentials, clearCredentials, } from "../config.js";
|
|
4
4
|
import { resolveToken } from "../auth.js";
|
|
5
5
|
import * as output from "../output.js";
|
|
6
6
|
export function registerAuthCommands(program) {
|
|
@@ -65,6 +65,7 @@ export function registerAuthCommands(program) {
|
|
|
65
65
|
const ctx = await client.bootstrap();
|
|
66
66
|
output.heading("Current Context");
|
|
67
67
|
console.log(` Organization: ${ctx.activeOrganization.name ?? ctx.activeOrganization.id}`);
|
|
68
|
+
console.log(` API URL: ${config.apiUrl}`);
|
|
68
69
|
if (process.env.FARTHERSHORE_TOKEN) {
|
|
69
70
|
output.info(" Auth: FARTHERSHORE_TOKEN env var");
|
|
70
71
|
}
|
|
@@ -77,4 +78,18 @@ export function registerAuthCommands(program) {
|
|
|
77
78
|
process.exitCode = 1;
|
|
78
79
|
}
|
|
79
80
|
});
|
|
81
|
+
program
|
|
82
|
+
.command("set-url <url>")
|
|
83
|
+
.description("Override the API base URL (for staging/testing)")
|
|
84
|
+
.action((url) => {
|
|
85
|
+
saveConfig({ apiUrl: url });
|
|
86
|
+
output.success(`API URL set to ${url}`);
|
|
87
|
+
});
|
|
88
|
+
program
|
|
89
|
+
.command("reset-url")
|
|
90
|
+
.description("Reset the API URL to production default")
|
|
91
|
+
.action(() => {
|
|
92
|
+
saveConfig({ apiUrl: "https://core.farthershore.com" });
|
|
93
|
+
output.success("API URL reset to https://core.farthershore.com");
|
|
94
|
+
});
|
|
80
95
|
}
|
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
2
3
|
import { resolve } from "node:path";
|
|
3
4
|
import YAML from "yaml";
|
|
4
5
|
import * as output from "../output.js";
|
|
5
6
|
const CI = !!process.env.CI || !!process.env.GITHUB_ACTIONS;
|
|
7
|
+
/**
|
|
8
|
+
* Read product.yaml from the main branch using git, without checking it out.
|
|
9
|
+
* Returns null if not in a git repo or main doesn't have the file.
|
|
10
|
+
*/
|
|
11
|
+
function readMainBranchProductName() {
|
|
12
|
+
for (const branch of ["main", "master"]) {
|
|
13
|
+
try {
|
|
14
|
+
const yaml = execSync(`git show ${branch}:product.yaml 2>/dev/null`, {
|
|
15
|
+
encoding: "utf-8",
|
|
16
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
17
|
+
});
|
|
18
|
+
const spec = YAML.parse(yaml);
|
|
19
|
+
const product = spec.product;
|
|
20
|
+
return product?.name ?? null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
6
28
|
// Inline a lightweight validation since we can't import the core schema directly.
|
|
7
29
|
// Checks structure, required fields, and strategy/model alignment.
|
|
8
30
|
function validateProductYaml(spec) {
|
|
@@ -31,13 +53,25 @@ function validateProductYaml(spec) {
|
|
|
31
53
|
if (!metering)
|
|
32
54
|
errors.push("Missing 'metering' section");
|
|
33
55
|
else {
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
36
|
-
errors.push("
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
const rawMeters = metering.meters ?? [];
|
|
57
|
+
if (!Array.isArray(rawMeters)) {
|
|
58
|
+
errors.push("'metering.meters' must be an array");
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const meters = rawMeters;
|
|
62
|
+
if (meters.length === 0)
|
|
63
|
+
errors.push("At least one meter is required in metering.meters");
|
|
64
|
+
if (meters.length > 10)
|
|
65
|
+
errors.push("Maximum 10 meters allowed");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const rawPlans = spec.plans ?? [];
|
|
69
|
+
const plans = Array.isArray(rawPlans)
|
|
70
|
+
? rawPlans
|
|
71
|
+
: [];
|
|
72
|
+
if (spec.plans != null && !Array.isArray(spec.plans)) {
|
|
73
|
+
errors.push("'plans' must be an array");
|
|
39
74
|
}
|
|
40
|
-
const plans = (spec.plans ?? []);
|
|
41
75
|
if (plans.length === 0)
|
|
42
76
|
errors.push("At least one plan is required to go live");
|
|
43
77
|
if (plans.length > 4)
|
|
@@ -113,6 +147,15 @@ export function registerValidateCommand(program) {
|
|
|
113
147
|
return;
|
|
114
148
|
}
|
|
115
149
|
const result = validateProductYaml(spec);
|
|
150
|
+
// Check product.name consistency against main branch
|
|
151
|
+
const currentName = spec.product
|
|
152
|
+
?.name;
|
|
153
|
+
if (currentName) {
|
|
154
|
+
const mainName = readMainBranchProductName();
|
|
155
|
+
if (mainName && mainName !== currentName) {
|
|
156
|
+
result.errors.push(`product.name "${currentName}" differs from main branch "${mainName}" — product name must not change`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
116
159
|
// GitHub Actions annotations
|
|
117
160
|
if (CI) {
|
|
118
161
|
for (const err of result.errors) {
|
|
@@ -122,7 +165,7 @@ export function registerValidateCommand(program) {
|
|
|
122
165
|
console.log(`::warning file=product.yaml::${w}`);
|
|
123
166
|
}
|
|
124
167
|
}
|
|
125
|
-
if (result.
|
|
168
|
+
if (result.errors.length === 0) {
|
|
126
169
|
output.success("product.yaml is valid");
|
|
127
170
|
if (result.warnings.length > 0) {
|
|
128
171
|
for (const w of result.warnings) {
|
package/dist/config.js
CHANGED
|
@@ -8,11 +8,11 @@ const CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
|
|
|
8
8
|
const CONFIGS_DIR = join(CONFIG_DIR, "configs");
|
|
9
9
|
function ensureDir(dir) {
|
|
10
10
|
if (!existsSync(dir))
|
|
11
|
-
mkdirSync(dir, { recursive: true });
|
|
11
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
12
12
|
}
|
|
13
13
|
// --- Config ---
|
|
14
14
|
const DEFAULT_CONFIG = {
|
|
15
|
-
apiUrl: "https://
|
|
15
|
+
apiUrl: "https://core.farthershore.com",
|
|
16
16
|
defaultFormat: "table",
|
|
17
17
|
};
|
|
18
18
|
export function loadConfig() {
|
|
@@ -47,7 +47,9 @@ export function loadCredentials() {
|
|
|
47
47
|
}
|
|
48
48
|
export function saveCredentials(creds) {
|
|
49
49
|
ensureDir(CONFIG_DIR);
|
|
50
|
-
writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2) + "\n"
|
|
50
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2) + "\n", {
|
|
51
|
+
mode: 0o600,
|
|
52
|
+
});
|
|
51
53
|
}
|
|
52
54
|
export function clearCredentials() {
|
|
53
55
|
if (existsSync(CREDENTIALS_FILE)) {
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farthershore/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "FartherShore CLI —
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "FartherShore CLI — create and configure API products",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"farthershore": "./dist/index.js"
|
|
8
|
-
"cli": "./dist/index.js"
|
|
7
|
+
"farthershore": "./dist/index.js"
|
|
9
8
|
},
|
|
10
9
|
"files": [
|
|
11
10
|
"dist"
|
|
@@ -41,11 +40,9 @@
|
|
|
41
40
|
"dependencies": {
|
|
42
41
|
"chalk": "^5.4.1",
|
|
43
42
|
"commander": "^13.1.0",
|
|
44
|
-
"js-yaml": "^4.1.0",
|
|
45
43
|
"yaml": "^2.8.3"
|
|
46
44
|
},
|
|
47
45
|
"devDependencies": {
|
|
48
|
-
"@types/js-yaml": "^4.0.9",
|
|
49
46
|
"@types/node": "^22.19.17",
|
|
50
47
|
"prettier": "^3.8.1",
|
|
51
48
|
"tsx": "^4.21.0",
|
package/dist/commands/billing.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import * as output from "../output.js";
|
|
2
|
-
export function registerBillingCommands(program, getClient) {
|
|
3
|
-
const billing = program
|
|
4
|
-
.command("billing")
|
|
5
|
-
.description("Stripe billing integration");
|
|
6
|
-
billing
|
|
7
|
-
.command("status")
|
|
8
|
-
.description("Show Stripe connection status")
|
|
9
|
-
.action(async () => {
|
|
10
|
-
const client = getClient();
|
|
11
|
-
const status = await client.getStripeStatus();
|
|
12
|
-
if (status.connected) {
|
|
13
|
-
output.success("Stripe connected");
|
|
14
|
-
console.log(` Account: ${status.accountId}`);
|
|
15
|
-
console.log(` Charges enabled: ${status.chargesEnabled ? "✓" : "✗"}`);
|
|
16
|
-
}
|
|
17
|
-
else {
|
|
18
|
-
output.warn("Stripe not connected.");
|
|
19
|
-
output.info("Run `farthershore billing connect` to start onboarding.");
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
billing
|
|
23
|
-
.command("connect")
|
|
24
|
-
.description("Start Stripe Connect onboarding (opens browser)")
|
|
25
|
-
.action(async () => {
|
|
26
|
-
const client = getClient();
|
|
27
|
-
const result = await client.startStripeOnboard();
|
|
28
|
-
output.success("Stripe onboarding started.");
|
|
29
|
-
console.log(`\n Open this URL in your browser:\n ${result.url}\n`);
|
|
30
|
-
});
|
|
31
|
-
}
|
package/dist/commands/dry-run.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import * as output from "../output.js";
|
|
2
|
-
export function registerDryRunCommand(program, getClient) {
|
|
3
|
-
program
|
|
4
|
-
.command("dry-run <product-id>")
|
|
5
|
-
.description("Check if the current product config would pass compilation")
|
|
6
|
-
.action(async (productId) => {
|
|
7
|
-
const client = getClient();
|
|
8
|
-
try {
|
|
9
|
-
const result = await client.compileProduct(productId);
|
|
10
|
-
if (result.success) {
|
|
11
|
-
output.success("Compilation passed — changes would be accepted");
|
|
12
|
-
if (result.warnings?.length) {
|
|
13
|
-
console.log();
|
|
14
|
-
output.warn(`${result.warnings.length} warning(s):`);
|
|
15
|
-
for (const w of result.warnings) {
|
|
16
|
-
console.log(` • ${w.message ?? w}`);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
output.error("Compilation failed — changes would be rejected\n");
|
|
22
|
-
for (const err of result.errors ?? []) {
|
|
23
|
-
console.log(` • ${err.code ? `[${err.code}] ` : ""}${err.message}`);
|
|
24
|
-
}
|
|
25
|
-
process.exitCode = 1;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
output.error(err instanceof Error ? err.message : "Compilation check failed");
|
|
30
|
-
process.exitCode = 1;
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
}
|
package/dist/commands/plans.d.ts
DELETED
package/dist/commands/plans.js
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import * as readline from "node:readline/promises";
|
|
2
|
-
import { writeConfigFile, readConfigFile, getConfigPath } from "../config.js";
|
|
3
|
-
import * as output from "../output.js";
|
|
4
|
-
export function registerPlanCommands(program, getClient) {
|
|
5
|
-
const plans = program.command("plans").description("Manage product plans");
|
|
6
|
-
plans
|
|
7
|
-
.command("list")
|
|
8
|
-
.description("List plans for a product")
|
|
9
|
-
.requiredOption("--product <id>", "Product ID")
|
|
10
|
-
.action(async (opts) => {
|
|
11
|
-
const client = getClient();
|
|
12
|
-
const product = await client.getProduct(opts.product);
|
|
13
|
-
if (!product.plans?.length) {
|
|
14
|
-
output.info("No plans found.");
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
console.log(output.table(["Name", "Key", "Type", "Price", "Active"], product.plans.map((p) => [
|
|
18
|
-
p.name,
|
|
19
|
-
p.key,
|
|
20
|
-
p.type,
|
|
21
|
-
p.monthlyPriceCents > 0
|
|
22
|
-
? output.formatPrice(p.monthlyPriceCents) + "/mo"
|
|
23
|
-
: "free",
|
|
24
|
-
p.isActive ? "✓" : "✗",
|
|
25
|
-
])));
|
|
26
|
-
});
|
|
27
|
-
// --- Config-as-code commands ---
|
|
28
|
-
plans
|
|
29
|
-
.command("pull")
|
|
30
|
-
.description("Pull plan config from server to local YAML file")
|
|
31
|
-
.requiredOption("--product <id>", "Product ID or slug")
|
|
32
|
-
.action(async (opts) => {
|
|
33
|
-
const client = getClient();
|
|
34
|
-
const yamlStr = await client.exportPlanConfig(opts.product);
|
|
35
|
-
// Derive slug for local storage
|
|
36
|
-
const product = await client.getProduct(opts.product);
|
|
37
|
-
const slug = product.runtimeHostname?.split(".")[0] ??
|
|
38
|
-
product.name.toLowerCase().replace(/\s+/g, "-");
|
|
39
|
-
const path = writeConfigFile(slug, yamlStr);
|
|
40
|
-
output.success(`Pulled ${product.plans?.length ?? 0} plans from "${product.name}"`);
|
|
41
|
-
console.log(` Written to ${path}`);
|
|
42
|
-
});
|
|
43
|
-
plans
|
|
44
|
-
.command("push")
|
|
45
|
-
.description("Validate, diff, and apply local plan config")
|
|
46
|
-
.requiredOption("--product <id>", "Product ID or slug")
|
|
47
|
-
.option("--yes", "Skip confirmation prompt (CI mode)")
|
|
48
|
-
.option("--force", "Override config hash check")
|
|
49
|
-
.action(async (opts) => {
|
|
50
|
-
const client = getClient();
|
|
51
|
-
// Load local config
|
|
52
|
-
const product = await client.getProduct(opts.product);
|
|
53
|
-
const slug = product.runtimeHostname?.split(".")[0] ??
|
|
54
|
-
product.name.toLowerCase().replace(/\s+/g, "-");
|
|
55
|
-
const yamlStr = readConfigFile(slug);
|
|
56
|
-
if (!yamlStr) {
|
|
57
|
-
output.error(`No local config found. Run \`farthershore plans pull --product ${opts.product}\` first.`);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
// Extract configHash
|
|
61
|
-
const hashMatch = yamlStr.match(/configHash:\s*['"]?([a-f0-9]+)['"]?/);
|
|
62
|
-
const configHash = hashMatch?.[1];
|
|
63
|
-
// Step 1: Validate + diff
|
|
64
|
-
output.info("Validating...");
|
|
65
|
-
const diff = await client.applyPlanConfig(opts.product, yamlStr, {
|
|
66
|
-
configHash,
|
|
67
|
-
force: opts.force,
|
|
68
|
-
validateOnly: true,
|
|
69
|
-
});
|
|
70
|
-
if (!diff.valid) {
|
|
71
|
-
output.error("Validation failed:\n");
|
|
72
|
-
for (const err of diff.errors) {
|
|
73
|
-
console.error(` ${err.line != null ? `Line ${err.line}: ` : ""}${err.message}`);
|
|
74
|
-
}
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
if (diff.changes.length === 0) {
|
|
78
|
-
output.success("No changes detected.");
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
// Step 2: Show diff
|
|
82
|
-
const creates = diff.changes.filter((c) => c.action === "create").length;
|
|
83
|
-
const updates = diff.changes.filter((c) => c.action === "update").length;
|
|
84
|
-
const deactivates = diff.changes.filter((c) => c.action === "deactivate").length;
|
|
85
|
-
console.log(`\n Plan: ${creates ? `${creates} to add` : ""}${creates && updates ? ", " : ""}${updates ? `${updates} to change` : ""}${(creates || updates) && deactivates ? ", " : ""}${deactivates ? `${deactivates} to deactivate` : ""}\n`);
|
|
86
|
-
for (const change of diff.changes) {
|
|
87
|
-
const symbol = change.action === "create"
|
|
88
|
-
? "+"
|
|
89
|
-
: change.action === "update"
|
|
90
|
-
? "~"
|
|
91
|
-
: "-";
|
|
92
|
-
console.log(` ${symbol} ${change.name} (${change.key}): ${change.details}`);
|
|
93
|
-
}
|
|
94
|
-
for (const w of diff.warnings) {
|
|
95
|
-
output.warn(` ${w.message}`);
|
|
96
|
-
}
|
|
97
|
-
// Step 3: Confirm
|
|
98
|
-
if (!opts.yes) {
|
|
99
|
-
const rl = readline.createInterface({
|
|
100
|
-
input: process.stdin,
|
|
101
|
-
output: process.stdout,
|
|
102
|
-
});
|
|
103
|
-
const answer = await rl.question("\n Apply these changes? (yes/no): ");
|
|
104
|
-
rl.close();
|
|
105
|
-
if (answer.trim().toLowerCase() !== "yes") {
|
|
106
|
-
console.log(" Cancelled.");
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// Step 4: Apply
|
|
111
|
-
output.info("\n Applying...");
|
|
112
|
-
const result = await client.applyPlanConfig(opts.product, yamlStr, {
|
|
113
|
-
configHash,
|
|
114
|
-
force: opts.force,
|
|
115
|
-
});
|
|
116
|
-
if (!result.applied) {
|
|
117
|
-
output.error("Apply failed:");
|
|
118
|
-
for (const err of result.errors) {
|
|
119
|
-
console.error(` ${err.message}`);
|
|
120
|
-
}
|
|
121
|
-
process.exit(1);
|
|
122
|
-
}
|
|
123
|
-
for (const r of result.results ?? []) {
|
|
124
|
-
const symbol = r.action === "create" ? "+" : r.action === "update" ? "~" : "-";
|
|
125
|
-
console.log(` ${symbol} ${r.action} ${r.key} (${r.id})`);
|
|
126
|
-
}
|
|
127
|
-
output.success(`${result.results?.length ?? 0} changes applied.`);
|
|
128
|
-
});
|
|
129
|
-
plans
|
|
130
|
-
.command("diff")
|
|
131
|
-
.description("Show what would change without applying")
|
|
132
|
-
.requiredOption("--product <id>", "Product ID or slug")
|
|
133
|
-
.action(async (opts) => {
|
|
134
|
-
const client = getClient();
|
|
135
|
-
const product = await client.getProduct(opts.product);
|
|
136
|
-
const slug = product.runtimeHostname?.split(".")[0] ??
|
|
137
|
-
product.name.toLowerCase().replace(/\s+/g, "-");
|
|
138
|
-
const yamlStr = readConfigFile(slug);
|
|
139
|
-
if (!yamlStr) {
|
|
140
|
-
output.error(`No local config found. Run \`farthershore plans pull --product ${opts.product}\` first.`);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
const hashMatch = yamlStr.match(/configHash:\s*['"]?([a-f0-9]+)['"]?/);
|
|
144
|
-
const diff = await client.applyPlanConfig(opts.product, yamlStr, {
|
|
145
|
-
configHash: hashMatch?.[1],
|
|
146
|
-
validateOnly: true,
|
|
147
|
-
});
|
|
148
|
-
if (!diff.valid) {
|
|
149
|
-
output.error("Validation errors:");
|
|
150
|
-
for (const err of diff.errors)
|
|
151
|
-
console.error(` ${err.message}`);
|
|
152
|
-
process.exit(1);
|
|
153
|
-
}
|
|
154
|
-
if (diff.changes.length === 0) {
|
|
155
|
-
output.success("No changes. Local config matches remote.");
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
for (const change of diff.changes) {
|
|
159
|
-
const symbol = change.action === "create"
|
|
160
|
-
? "+"
|
|
161
|
-
: change.action === "update"
|
|
162
|
-
? "~"
|
|
163
|
-
: "-";
|
|
164
|
-
console.log(` ${symbol} ${change.name} (${change.key}): ${change.details}`);
|
|
165
|
-
}
|
|
166
|
-
output.info("\n No changes applied. Run `farthershore plans push` to apply.");
|
|
167
|
-
});
|
|
168
|
-
plans
|
|
169
|
-
.command("validate")
|
|
170
|
-
.description("Validate local config file (offline)")
|
|
171
|
-
.action(() => {
|
|
172
|
-
// TODO: Offline YAML schema validation
|
|
173
|
-
output.info("Offline validation not yet implemented. Use `farthershore plans diff` for server-side validation.");
|
|
174
|
-
});
|
|
175
|
-
plans
|
|
176
|
-
.command("status")
|
|
177
|
-
.description("Check if local config matches remote")
|
|
178
|
-
.requiredOption("--product <id>", "Product ID or slug")
|
|
179
|
-
.action(async (opts) => {
|
|
180
|
-
const client = getClient();
|
|
181
|
-
const product = await client.getProduct(opts.product);
|
|
182
|
-
const slug = product.runtimeHostname?.split(".")[0] ??
|
|
183
|
-
product.name.toLowerCase().replace(/\s+/g, "-");
|
|
184
|
-
const localYaml = readConfigFile(slug);
|
|
185
|
-
const localPath = getConfigPath(slug);
|
|
186
|
-
if (!localYaml) {
|
|
187
|
-
output.info(`No local config at ${localPath}`);
|
|
188
|
-
output.info(`Run \`farthershore plans pull --product ${opts.product}\` to fetch.`);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const localHash = localYaml.match(/configHash:\s*['"]?([a-f0-9]+)['"]?/)?.[1];
|
|
192
|
-
const remoteYaml = await client.exportPlanConfig(opts.product);
|
|
193
|
-
const remoteHash = remoteYaml.match(/configHash:\s*['"]?([a-f0-9]+)['"]?/)?.[1];
|
|
194
|
-
console.log(` Local: ${localPath}`);
|
|
195
|
-
console.log(` Hash: ${localHash?.slice(0, 16) ?? "none"}...`);
|
|
196
|
-
console.log(` Remote: ${remoteHash?.slice(0, 16) ?? "none"}...`);
|
|
197
|
-
if (localHash === remoteHash) {
|
|
198
|
-
output.success("Up to date.");
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
output.warn("Remote has changed. Run `farthershore plans pull` to get latest.");
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import * as output from "../output.js";
|
|
2
|
-
export function registerProductCommands(program, getClient) {
|
|
3
|
-
const products = program
|
|
4
|
-
.command("products")
|
|
5
|
-
.description("Manage API products");
|
|
6
|
-
products
|
|
7
|
-
.command("list")
|
|
8
|
-
.alias("ls")
|
|
9
|
-
.description("List products in your organization")
|
|
10
|
-
.action(async () => {
|
|
11
|
-
const client = getClient();
|
|
12
|
-
const items = await client.listProducts();
|
|
13
|
-
if (items.length === 0) {
|
|
14
|
-
output.info("No products found. Create one with `farthershore products create`.");
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
console.log(output.table(["Name", "Status", "Plans", "Created"], items.map((p) => [
|
|
18
|
-
p.name,
|
|
19
|
-
p.status,
|
|
20
|
-
String(p.plans?.length ?? 0),
|
|
21
|
-
output.formatDate(p.createdAt),
|
|
22
|
-
])));
|
|
23
|
-
});
|
|
24
|
-
products
|
|
25
|
-
.command("get <id>")
|
|
26
|
-
.description("Show product details")
|
|
27
|
-
.action(async (id) => {
|
|
28
|
-
const client = getClient();
|
|
29
|
-
const product = await client.getProduct(id);
|
|
30
|
-
output.heading(product.name);
|
|
31
|
-
console.log(` ID: ${product.id}`);
|
|
32
|
-
console.log(` Status: ${product.status}`);
|
|
33
|
-
console.log(` Base URL: ${product.baseUrl}`);
|
|
34
|
-
console.log(` Gateway: ${product.runtimeHostname ?? "—"}`);
|
|
35
|
-
console.log(` Portal: ${product.portalHostname ?? "—"}`);
|
|
36
|
-
console.log(` Plans: ${product.plans?.length ?? 0}`);
|
|
37
|
-
if (product.plans?.length) {
|
|
38
|
-
for (const plan of product.plans) {
|
|
39
|
-
console.log(` • ${plan.name} (${plan.type}) ${plan.monthlyPriceCents > 0 ? output.formatPrice(plan.monthlyPriceCents) + "/mo" : "free"}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
products
|
|
44
|
-
.command("create <name>")
|
|
45
|
-
.description("Create a new draft product")
|
|
46
|
-
.requiredOption("--base-url <url>", "Backend API base URL")
|
|
47
|
-
.option("--description <desc>", "Product description")
|
|
48
|
-
.action(async (name, opts) => {
|
|
49
|
-
const client = getClient();
|
|
50
|
-
const product = await client.createProduct({
|
|
51
|
-
name,
|
|
52
|
-
baseUrl: opts.baseUrl,
|
|
53
|
-
description: opts.description,
|
|
54
|
-
});
|
|
55
|
-
output.success(`Created product "${product.name}" (${product.id})`);
|
|
56
|
-
console.log(` Status: ${product.status}`);
|
|
57
|
-
console.log(` Gateway: ${product.runtimeHostname ?? "—"}`);
|
|
58
|
-
});
|
|
59
|
-
products
|
|
60
|
-
.command("publish <id>")
|
|
61
|
-
.description("Publish a draft product")
|
|
62
|
-
.action(async (id) => {
|
|
63
|
-
const client = getClient();
|
|
64
|
-
const product = await client.publishProduct(id);
|
|
65
|
-
output.success(`Published "${product.name}"`);
|
|
66
|
-
console.log(` Gateway: ${product.runtimeHostname}`);
|
|
67
|
-
});
|
|
68
|
-
products
|
|
69
|
-
.command("delete <id>")
|
|
70
|
-
.description("Delete a product")
|
|
71
|
-
.action(async (id) => {
|
|
72
|
-
const client = getClient();
|
|
73
|
-
await client.deleteProduct(id);
|
|
74
|
-
output.success(`Deleted product ${id}`);
|
|
75
|
-
});
|
|
76
|
-
}
|
package/dist/commands/team.d.ts
DELETED
package/dist/commands/team.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { loadCredentials } from "../config.js";
|
|
2
|
-
import * as output from "../output.js";
|
|
3
|
-
export function registerTeamCommands(program, getClient) {
|
|
4
|
-
const team = program.command("team").description("Manage organization team");
|
|
5
|
-
team
|
|
6
|
-
.command("list")
|
|
7
|
-
.alias("ls")
|
|
8
|
-
.description("List organization members")
|
|
9
|
-
.action(async () => {
|
|
10
|
-
const client = getClient();
|
|
11
|
-
const creds = loadCredentials();
|
|
12
|
-
if (!creds?.orgId) {
|
|
13
|
-
output.error("No active organization. Run `farthershore login` first.");
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
-
const members = await client.listMembers(creds.orgId);
|
|
17
|
-
console.log(output.table(["User ID", "Role"], members.map((m) => [m.userId, m.role])));
|
|
18
|
-
});
|
|
19
|
-
team
|
|
20
|
-
.command("invite <email>")
|
|
21
|
-
.description("Invite a member to the organization")
|
|
22
|
-
.option("--role <role>", "Role: owner, admin, or member", "member")
|
|
23
|
-
.action(async (email, opts) => {
|
|
24
|
-
const client = getClient();
|
|
25
|
-
const creds = loadCredentials();
|
|
26
|
-
if (!creds?.orgId) {
|
|
27
|
-
output.error("No active organization. Run `farthershore login` first.");
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
await client.inviteMember(creds.orgId, email, opts.role);
|
|
31
|
-
output.success(`Invited ${email} as ${opts.role}.`);
|
|
32
|
-
});
|
|
33
|
-
team
|
|
34
|
-
.command("remove <userId>")
|
|
35
|
-
.description("Remove a member from the organization")
|
|
36
|
-
.action(async (userId) => {
|
|
37
|
-
const client = getClient();
|
|
38
|
-
const creds = loadCredentials();
|
|
39
|
-
if (!creds?.orgId) {
|
|
40
|
-
output.error("No active organization. Run `farthershore login` first.");
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
await client.removeMember(creds.orgId, userId);
|
|
44
|
-
output.success(`Removed member ${userId}.`);
|
|
45
|
-
});
|
|
46
|
-
}
|
package/dist/commands/tokens.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import * as output from "../output.js";
|
|
2
|
-
export function registerTokenCommands(program, getClient) {
|
|
3
|
-
const tokens = program.command("tokens").description("Manage API tokens");
|
|
4
|
-
tokens
|
|
5
|
-
.command("list")
|
|
6
|
-
.alias("ls")
|
|
7
|
-
.description("List maker tokens")
|
|
8
|
-
.action(async () => {
|
|
9
|
-
const client = getClient();
|
|
10
|
-
const items = await client.listTokens();
|
|
11
|
-
if (items.length === 0) {
|
|
12
|
-
output.info("No tokens found.");
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
console.log(output.table(["Label", "Last 4", "Scope", "Created"], items.map((t) => [
|
|
16
|
-
t.label,
|
|
17
|
-
`...${t.lastFour}`,
|
|
18
|
-
t.productId ? `product:${t.productId.slice(0, 8)}` : "org-wide",
|
|
19
|
-
output.formatDate(t.createdAt),
|
|
20
|
-
])));
|
|
21
|
-
});
|
|
22
|
-
tokens
|
|
23
|
-
.command("revoke <id>")
|
|
24
|
-
.description("Revoke a token")
|
|
25
|
-
.action(async (id) => {
|
|
26
|
-
const client = getClient();
|
|
27
|
-
await client.revokeToken(id);
|
|
28
|
-
output.success(`Token ${id} revoked.`);
|
|
29
|
-
});
|
|
30
|
-
}
|
package/dist/commands/usage.d.ts
DELETED
package/dist/commands/usage.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import * as output from "../output.js";
|
|
2
|
-
export function registerUsageCommands(program, getClient) {
|
|
3
|
-
program
|
|
4
|
-
.command("usage")
|
|
5
|
-
.description("Show usage analytics for a product")
|
|
6
|
-
.requiredOption("--product <id>", "Product ID")
|
|
7
|
-
.option("--live", "Poll for live updates every 5 seconds")
|
|
8
|
-
.action(async (opts) => {
|
|
9
|
-
const client = getClient();
|
|
10
|
-
async function showUsage() {
|
|
11
|
-
const data = await client.getUsage(opts.product);
|
|
12
|
-
if (opts.live)
|
|
13
|
-
console.clear();
|
|
14
|
-
output.heading("Usage Summary");
|
|
15
|
-
console.log(output.json(data));
|
|
16
|
-
}
|
|
17
|
-
await showUsage();
|
|
18
|
-
if (opts.live) {
|
|
19
|
-
output.info("Polling every 5s. Press Ctrl+C to stop.");
|
|
20
|
-
setInterval(showUsage, 5000);
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
}
|