@farthershore/cli 0.3.6 → 0.3.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farthershore/cli",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "FartherShore CLI — create and configure API products",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,9 @@
21
21
  },
22
22
  "scripts": {
23
23
  "dev": "tsx src/index.ts",
24
- "build": "tsc",
24
+ "build": "rm -rf dist && npm run build:types && npm run build:bundle",
25
+ "build:types": "tsc --emitDeclarationOnly",
26
+ "build:bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/index.js --external:chalk --external:commander --external:yaml --external:zod && chmod +x dist/index.js",
25
27
  "prepublishOnly": "npm run build",
26
28
  "test": "vitest run",
27
29
  "lint": "prettier --check \"src/**/*.ts\" && eslint .",
@@ -39,14 +41,15 @@
39
41
  "author": "Farther Shore",
40
42
  "license": "MIT",
41
43
  "dependencies": {
42
- "@farther-shore/shared-types": "^0.28.2",
43
44
  "chalk": "^5.4.1",
44
45
  "commander": "^13.1.0",
45
46
  "yaml": "^2.8.3",
46
47
  "zod": "^4.3.6"
47
48
  },
48
49
  "devDependencies": {
50
+ "@farther-shore/shared-types": "^0.28.2",
49
51
  "@types/node": "^22.19.17",
52
+ "esbuild": "^0.28.0",
50
53
  "eslint": "^9.39.4",
51
54
  "eslint-plugin-sonarjs": "^4.0.3",
52
55
  "prettier": "^3.8.1",
package/dist/auth.js DELETED
@@ -1,17 +0,0 @@
1
- // Token resolution: env var → credentials file → error
2
- import { loadCredentials } from "./config.js";
3
- import { CliError } from "./types.js";
4
- export function resolveToken(overrideToken) {
5
- // 1. Explicit override (--token flag)
6
- if (overrideToken)
7
- return overrideToken;
8
- // 2. Environment variable
9
- const envToken = process.env.FARTHERSHORE_TOKEN;
10
- if (envToken)
11
- return envToken;
12
- // 3. Stored credentials (from `farthershore login`)
13
- const creds = loadCredentials();
14
- if (creds?.token)
15
- return creds.token;
16
- throw new CliError("Not authenticated. Run `farthershore set-key` or set FARTHERSHORE_TOKEN environment variable.");
17
- }
@@ -1,10 +0,0 @@
1
- // Build-time constants baked into the CLI binary at publish time.
2
- //
3
- // To publish a staging variant:
4
- // 1. Change BUILD_API_URL below to the staging URL
5
- // 2. Change the npm package name/tag in package.json
6
- // 3. Run `npm publish`
7
- //
8
- // The CLI uses this as the default API URL — users don't need to set
9
- // FARTHERSHORE_API_URL or pass --api-url for their target environment.
10
- export const BUILD_API_URL = "https://core.farthershore.com";
package/dist/client.js DELETED
@@ -1,53 +0,0 @@
1
- // Typed API client for the FartherShore platform
2
- import { CliError } from "./types.js";
3
- export function createClient(opts) {
4
- async function request(method, path, body) {
5
- const res = await fetch(`${opts.apiUrl}${path}`, {
6
- method,
7
- headers: {
8
- Authorization: `Bearer ${opts.token}`,
9
- "Content-Type": "application/json",
10
- },
11
- body: body ? JSON.stringify(body) : undefined,
12
- });
13
- if (!res.ok) {
14
- // Try the canonical Wave-4 envelope first; fall back to a string
15
- // body or the bare statusText so the error path is robust to old
16
- // server builds and HTML/text responses (e.g. Cloudflare 502s).
17
- const parsed = (await res.json().catch(() => null));
18
- const errEnvelope = parsed && typeof parsed === "object" && parsed.error;
19
- let message = res.statusText;
20
- let code;
21
- let details;
22
- if (typeof errEnvelope === "string") {
23
- // Legacy { error: "..." } shape.
24
- message = errEnvelope;
25
- }
26
- else if (errEnvelope && typeof errEnvelope === "object") {
27
- message = errEnvelope.message ?? message;
28
- code = errEnvelope.code;
29
- details = errEnvelope.details;
30
- }
31
- throw new CliError(message, res.status, { code, details });
32
- }
33
- if (res.status === 204)
34
- return undefined;
35
- return res.json();
36
- }
37
- return {
38
- // --- Auth ---
39
- bootstrap: () => request("POST", "/builder/context/bootstrap"),
40
- // --- Products ---
41
- listProducts: () => request("GET", "/products"),
42
- initProduct: (data) => request("POST", "/products/init", data),
43
- // --- Compile ---
44
- compileProduct: (productId, opts) => request("POST", `/products/${productId}/compile`, opts?.branch ? { branch: opts.branch } : undefined),
45
- // --- Management (maker token) ---
46
- // Compile the product associated with the token — no product ID needed.
47
- // Pass `branch` to scope compilation to an env branch's plans.
48
- managementCompileSelf: (opts) => request("POST", "/management/compile", opts?.branch ? { branch: opts.branch } : undefined),
49
- managementCompile: (productId, opts) => request("POST", `/management/products/${productId}/compile`, opts?.branch ? { branch: opts.branch } : undefined),
50
- managementListProducts: () => request("GET", "/management/products"),
51
- isMakerToken: () => opts.token.startsWith("mk_"),
52
- };
53
- }
@@ -1,231 +0,0 @@
1
- import { execSync } from "node:child_process";
2
- import { readFileSync, existsSync } from "node:fs";
3
- import { resolve } from "node:path";
4
- import YAML from "yaml";
5
- import * as output from "../output.js";
6
- import { loadProductYaml, validateProductYaml } from "./validate.js";
7
- const CI = !!process.env.CI || !!process.env.GITHUB_ACTIONS;
8
- /**
9
- * Detect the git branch the compiler should scope to. Order of preference:
10
- * 1. --branch CLI flag (explicit override)
11
- * 2. GITHUB_HEAD_REF — pull_request source branch (the branch being proposed)
12
- * 3. GITHUB_REF_NAME — push event branch
13
- * 4. `git rev-parse --abbrev-ref HEAD` — local dev
14
- * Returns undefined when nothing is detectable.
15
- */
16
- function detectBranch(explicit) {
17
- if (explicit)
18
- return explicit;
19
- const ghHead = process.env.GITHUB_HEAD_REF;
20
- if (ghHead)
21
- return ghHead;
22
- const ghRef = process.env.GITHUB_REF_NAME;
23
- if (ghRef)
24
- return ghRef;
25
- try {
26
- const local = execSync("git rev-parse --abbrev-ref HEAD", {
27
- encoding: "utf-8",
28
- stdio: ["pipe", "pipe", "pipe"],
29
- }).trim();
30
- return local && local !== "HEAD" ? local : undefined;
31
- }
32
- catch {
33
- return undefined;
34
- }
35
- }
36
- /**
37
- * Read the product name/slug from product.yaml in the current directory.
38
- */
39
- function readSlugFromProductYaml() {
40
- const yamlPath = resolve("product.yaml");
41
- if (!existsSync(yamlPath))
42
- return null;
43
- try {
44
- const spec = YAML.parse(readFileSync(yamlPath, "utf-8"));
45
- const product = spec.product;
46
- return product?.name ?? null;
47
- }
48
- catch {
49
- return null;
50
- }
51
- }
52
- function validateLocalProductYamlBeforeRemoteCompile() {
53
- const filePath = resolve("product.yaml");
54
- if (!existsSync(filePath))
55
- return true;
56
- const loaded = loadProductYaml(filePath);
57
- if (!loaded.ok) {
58
- const message = loaded.reason === "parse"
59
- ? `Local product.yaml failed to parse; remote compile was not started.\n${loaded.message}`
60
- : `Local product.yaml could not be read; remote compile was not started.\n${loaded.message}`;
61
- if (CI) {
62
- console.log(`::error file=product.yaml::${message}`);
63
- }
64
- output.error(message);
65
- process.exitCode = 1;
66
- return false;
67
- }
68
- const result = validateProductYaml(loaded.spec);
69
- if (!result.valid) {
70
- if (CI) {
71
- for (const err of result.errors) {
72
- console.log(`::error file=product.yaml::${err}`);
73
- }
74
- for (const warning of result.warnings) {
75
- console.log(`::warning file=product.yaml::${warning}`);
76
- }
77
- }
78
- output.error("Local product.yaml failed validation; remote compile was not started.\n");
79
- for (const err of result.errors) {
80
- console.log(` • ${err}`);
81
- }
82
- if (result.warnings.length > 0) {
83
- console.log();
84
- for (const warning of result.warnings) {
85
- output.warn(warning);
86
- }
87
- }
88
- process.exitCode = 1;
89
- return false;
90
- }
91
- output.success("Local product.yaml passed validation");
92
- for (const warning of result.warnings) {
93
- output.warn(warning);
94
- }
95
- output.info("Remote compile checks the pushed branch state; unpushed local edits are not included.");
96
- return true;
97
- }
98
- function shouldValidateLocalProductYaml(productArg) {
99
- const filePath = resolve("product.yaml");
100
- if (!existsSync(filePath))
101
- return false;
102
- if (!productArg)
103
- return true;
104
- const localSlug = readSlugFromProductYaml();
105
- return (typeof localSlug === "string" &&
106
- localSlug.toLowerCase() === productArg.toLowerCase());
107
- }
108
- /**
109
- * Resolve a product identifier to an API product ID.
110
- * If no arg, reads product.name from product.yaml in the current directory.
111
- * If arg looks like a UUID, uses it directly. Otherwise treats it as a slug.
112
- */
113
- async function resolveProductId(client, arg) {
114
- const slug = arg ?? readSlugFromProductYaml();
115
- if (!slug)
116
- return null;
117
- // UUID — use directly
118
- if (slug.length === 36 &&
119
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(slug)) {
120
- return slug;
121
- }
122
- // Slug — resolve via product list (use management API for maker tokens)
123
- try {
124
- const products = client.isMakerToken()
125
- ? await client.managementListProducts()
126
- : await client.listProducts();
127
- const match = products.find((p) => p.name === slug || p.name.toLowerCase() === slug.toLowerCase());
128
- return match?.id ?? null;
129
- }
130
- catch (err) {
131
- // Don't swallow network errors silently
132
- const msg = err instanceof Error ? err.message : String(err);
133
- process.stderr.write(`Warning: Failed to resolve product slug: ${msg}\n`);
134
- return null;
135
- }
136
- }
137
- // Render a diagnostic with an optional plan-key prefix so users can see which
138
- // plan an error belongs to when the compiler scopes it. `compiledPlanId`
139
- // itself is an internal pointer — `planKey` is the human-readable label.
140
- function formatDiag(d) {
141
- const prefix = d.code ? `[${d.code}] ` : "";
142
- const planLabel = d.planKey ? `(plan: ${d.planKey}) ` : "";
143
- return `${prefix}${planLabel}${d.message}`;
144
- }
145
- function handleResult(result) {
146
- // GitHub Actions annotations
147
- if (CI) {
148
- for (const err of result.errors ?? []) {
149
- console.log(`::error file=product.yaml::${formatDiag(err)}`);
150
- }
151
- for (const w of result.warnings ?? []) {
152
- console.log(`::warning file=product.yaml::${formatDiag(w)}`);
153
- }
154
- }
155
- if (result.success) {
156
- output.success("Remote compile passed");
157
- if (result.warnings?.length) {
158
- console.log();
159
- for (const w of result.warnings) {
160
- output.warn(formatDiag(w));
161
- }
162
- }
163
- }
164
- else {
165
- output.error("Remote compile failed\n");
166
- for (const err of result.errors ?? []) {
167
- console.log(` • ${formatDiag(err)}`);
168
- }
169
- process.exitCode = 1;
170
- }
171
- }
172
- export function registerApplyCommand(program, getClient) {
173
- program
174
- .command("apply [product]")
175
- .description("Validate the current repo's product.yaml before remote compile when applying that repo's product, then run the server-side compiler against the pushed branch state for this product. " +
176
- "Unpushed local edits are not included. Pass a product slug, or run inside a product repo to auto-detect from product.yaml. " +
177
- "Automatically scopes to the current git branch so env branches compile against their own plans.")
178
- .option("--branch <branch>", "Override the branch used for env-scoped compilation (default: auto-detected)")
179
- .action(async (productArg, opts) => {
180
- const client = getClient();
181
- const branch = detectBranch(opts.branch);
182
- if (shouldValidateLocalProductYaml(productArg) &&
183
- !validateLocalProductYamlBeforeRemoteCompile()) {
184
- return;
185
- }
186
- if (branch && CI) {
187
- console.log(`::notice::Compiling against branch '${branch}'`);
188
- }
189
- // Fast path for CI: product-scoped maker token with no argument.
190
- // Server auto-resolves product from the token — no product.yaml,
191
- // no slug lookup, single API call.
192
- if (client.isMakerToken() && !productArg) {
193
- try {
194
- const result = await client.managementCompileSelf({ branch });
195
- handleResult(result);
196
- return;
197
- }
198
- catch (err) {
199
- const msg = err instanceof Error ? err.message : "Compilation check failed";
200
- if (CI)
201
- console.log(`::error::${msg}`);
202
- output.error(msg);
203
- process.exitCode = 1;
204
- return;
205
- }
206
- }
207
- const productId = await resolveProductId(client, productArg);
208
- if (!productId) {
209
- const hint = productArg
210
- ? `Product "${productArg}" not found. Check the name and try again.`
211
- : "No product specified and no product.yaml found.\n" +
212
- " Run from inside a product repo, or pass the slug: farthershore apply my-api";
213
- output.error(hint);
214
- process.exitCode = 1;
215
- return;
216
- }
217
- try {
218
- const result = client.isMakerToken()
219
- ? await client.managementCompile(productId, { branch })
220
- : await client.compileProduct(productId, { branch });
221
- handleResult(result);
222
- }
223
- catch (err) {
224
- const msg = err instanceof Error ? err.message : "Compilation check failed";
225
- if (CI)
226
- console.log(`::error::${msg}`);
227
- output.error(msg);
228
- process.exitCode = 1;
229
- }
230
- });
231
- }
@@ -1,43 +0,0 @@
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
- }
@@ -1,120 +0,0 @@
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
- program
8
- .command("set-key [token]")
9
- .description("Set your API token (interactive or pass as argument)")
10
- .action(async (tokenArg) => {
11
- let token = tokenArg?.trim();
12
- if (!token) {
13
- // Interactive mode
14
- if (!process.stdin.isTTY) {
15
- output.error("No token provided. Pass it as an argument: farthershore set-key <token>");
16
- process.exitCode = 1;
17
- return;
18
- }
19
- console.log("Set your FartherShore API token\n");
20
- console.log(" Create a token at https://farthershore.com/settings/tokens\n");
21
- const rl = readline.createInterface({
22
- input: process.stdin,
23
- output: process.stdout,
24
- });
25
- token = (await rl.question("Token: ")).trim();
26
- rl.close();
27
- }
28
- if (!token) {
29
- output.error("No token provided.");
30
- process.exitCode = 1;
31
- return;
32
- }
33
- const config = loadConfig();
34
- try {
35
- const client = createClient({ apiUrl: config.apiUrl, token });
36
- const ctx = await client.bootstrap();
37
- saveCredentials({
38
- token,
39
- orgId: ctx.activeOrganization.id,
40
- userId: ctx.user.id,
41
- });
42
- output.success("Authenticated");
43
- console.log(` Organization: ${ctx.activeOrganization.name ?? ctx.activeOrganization.id}`);
44
- }
45
- catch {
46
- output.error("Invalid token. Check it and try again.");
47
- process.exitCode = 1;
48
- }
49
- });
50
- program
51
- .command("logout")
52
- .description("Clear stored credentials")
53
- .action(() => {
54
- clearCredentials();
55
- output.success("Credentials cleared.");
56
- });
57
- program
58
- .command("whoami")
59
- .description("Show current authentication context")
60
- .action(async () => {
61
- const config = loadConfig();
62
- const formatOpt = program.opts().format;
63
- const format = output.outputFormat(formatOpt);
64
- try {
65
- const token = resolveToken();
66
- const client = createClient({ apiUrl: config.apiUrl, token });
67
- const ctx = await client.bootstrap();
68
- const authSource = process.env.FARTHERSHORE_TOKEN
69
- ? "env:FARTHERSHORE_TOKEN"
70
- : "credentials-file";
71
- if (format === "json") {
72
- // Machine-readable shape — stable contract for CI scripts
73
- // ("am I logged in?"). Don't include the token itself.
74
- console.log(output.json({
75
- organization: {
76
- id: ctx.activeOrganization.id,
77
- name: ctx.activeOrganization.name ?? null,
78
- slug: ctx.activeOrganization.slug ?? null,
79
- },
80
- user: { id: ctx.user.id },
81
- apiUrl: config.apiUrl,
82
- authSource,
83
- }));
84
- return;
85
- }
86
- output.heading("Current Context");
87
- console.log(` Organization: ${ctx.activeOrganization.name ?? ctx.activeOrganization.id}`);
88
- console.log(` API URL: ${config.apiUrl}`);
89
- if (authSource === "env:FARTHERSHORE_TOKEN") {
90
- output.info(" Auth: FARTHERSHORE_TOKEN env var");
91
- }
92
- else {
93
- output.info(" Auth: ~/.farthershore/credentials.json");
94
- }
95
- }
96
- catch {
97
- if (format === "json") {
98
- console.log(output.json({ authenticated: false }));
99
- }
100
- else {
101
- output.error("Not authenticated. Run `farthershore set-key` or set FARTHERSHORE_TOKEN.");
102
- }
103
- process.exitCode = 1;
104
- }
105
- });
106
- program
107
- .command("set-url <url>")
108
- .description("Override the API base URL (for staging/testing)")
109
- .action((url) => {
110
- saveConfig({ apiUrl: url });
111
- output.success(`API URL set to ${url}`);
112
- });
113
- program
114
- .command("reset-url")
115
- .description("Reset the API URL to production default")
116
- .action(() => {
117
- saveConfig({ apiUrl: "https://core.farthershore.com" });
118
- output.success("API URL reset to https://core.farthershore.com");
119
- });
120
- }