@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 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 <product-id>`
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
- farthershore apply prod_abc123
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 | Description |
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` | Output as JSON (default for non-TTY) |
117
- | `--version` | Show version |
118
- | `--help` | Show 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 | Description |
123
- |----------|-------------|
124
- | `FARTHERSHORE_TOKEN` | API token (overrides stored credentials) |
125
- | `FARTHERSHORE_API_URL` | API base URL (default: `https://api.farthershore.com`) |
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
  }
@@ -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 <product-id>")
6
- .description("Run the server-side compiler to check if the current config is valid")
7
- .action(async (productId) => {
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 = await client.compileProduct(productId);
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 ?? []) {
@@ -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 meters = (metering.meters ?? []);
35
- if (meters.length === 0)
36
- errors.push("At least one meter is required in metering.meters");
37
- if (meters.length > 10)
38
- errors.push("Maximum 10 meters allowed");
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.valid) {
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://api.farthershore.com",
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.2.0",
4
- "description": "FartherShore CLI — manage API products, plans, and billing from the terminal",
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",
@@ -1,3 +0,0 @@
1
- import { Command } from "commander";
2
- import type { ApiClient } from "../client.js";
3
- export declare function registerBillingCommands(program: Command, getClient: () => ApiClient): void;
@@ -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
- }
@@ -1,3 +0,0 @@
1
- import { Command } from "commander";
2
- import type { ApiClient } from "../client.js";
3
- export declare function registerDryRunCommand(program: Command, getClient: () => ApiClient): void;
@@ -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
- }
@@ -1,3 +0,0 @@
1
- import { Command } from "commander";
2
- import type { ApiClient } from "../client.js";
3
- export declare function registerPlanCommands(program: Command, getClient: () => ApiClient): void;
@@ -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,3 +0,0 @@
1
- import { Command } from "commander";
2
- import type { ApiClient } from "../client.js";
3
- export declare function registerProductCommands(program: Command, getClient: () => ApiClient): void;
@@ -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
- }
@@ -1,3 +0,0 @@
1
- import { Command } from "commander";
2
- import type { ApiClient } from "../client.js";
3
- export declare function registerTeamCommands(program: Command, getClient: () => ApiClient): void;
@@ -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
- }
@@ -1,3 +0,0 @@
1
- import { Command } from "commander";
2
- import type { ApiClient } from "../client.js";
3
- export declare function registerTokenCommands(program: Command, getClient: () => ApiClient): void;
@@ -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
- }
@@ -1,3 +0,0 @@
1
- import { Command } from "commander";
2
- import type { ApiClient } from "../client.js";
3
- export declare function registerUsageCommands(program: Command, getClient: () => ApiClient): void;
@@ -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
- }