@fml-inc/fml 0.5.0 → 0.6.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/.claude-plugin/plugin.json +1 -1
- package/dist/{chunk-OI562MUP.js → chunk-2H6D7RUN.js} +3 -3
- package/dist/chunk-2H6D7RUN.js.map +1 -0
- package/dist/{chunk-5BHBYQ2F.js → chunk-VDW5L7OB.js} +1 -1
- package/dist/chunk-VDW5L7OB.js.map +1 -0
- package/dist/cli.js +99 -95
- package/dist/cli.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/{token-store-T2RNLXRM.js → token-store-JZJ3FJHA.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-5BHBYQ2F.js.map +0 -1
- package/dist/chunk-OI562MUP.js.map +0 -1
- /package/dist/{token-store-T2RNLXRM.js.map → token-store-JZJ3FJHA.js.map} +0 -0
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
CONVEX_URL,
|
|
3
3
|
getSelectedOrg,
|
|
4
4
|
getValidToken
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-VDW5L7OB.js";
|
|
6
6
|
|
|
7
7
|
// src/fml-client.ts
|
|
8
8
|
import { resolveRepoFromCwd } from "@fml-inc/panopticon/repo";
|
|
@@ -74,7 +74,7 @@ function createFmlClient(token) {
|
|
|
74
74
|
const repo = resolveRepoFromCwd(process.cwd());
|
|
75
75
|
if (repo) body.repo = repo.repo;
|
|
76
76
|
if (isServiceToken) {
|
|
77
|
-
const { readTokens } = await import("./token-store-
|
|
77
|
+
const { readTokens } = await import("./token-store-JZJ3FJHA.js");
|
|
78
78
|
const userExternalId = process.env.FML_USER_EXTERNAL_ID ?? readTokens()?.user.id;
|
|
79
79
|
if (userExternalId) body.userExternalId = userExternalId;
|
|
80
80
|
}
|
|
@@ -214,4 +214,4 @@ export {
|
|
|
214
214
|
createFmlClient,
|
|
215
215
|
getAuthenticatedClient
|
|
216
216
|
};
|
|
217
|
-
//# sourceMappingURL=chunk-
|
|
217
|
+
//# sourceMappingURL=chunk-2H6D7RUN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fml-client.ts"],"sourcesContent":["import { resolveRepoFromCwd } from \"@fml-inc/panopticon/repo\";\nimport { getSelectedOrg, getValidToken } from \"./auth/token-store.js\";\nimport { CONVEX_URL } from \"./config.js\";\nimport type {\n RepoConfigSnapshotDetail,\n RepoConfigSnapshotSummary,\n ResolvedRepo,\n UserConfigSnapshotDetail,\n UserConfigSnapshotSummary,\n} from \"./types.js\";\n\n// ── Shared plumbing ─────────────────────────────────────────────────────────\n\nexport type ToolCategory =\n | \"messages\"\n | \"slack\"\n | \"skills\"\n | \"analysis\"\n | \"integrations\"\n | \"engineering\"\n | \"memory\"\n | \"automations\"\n | \"amplitude\"\n | \"posthog\"\n | \"meta-ads\";\n\nexport interface PublicToolDescriptor {\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n category?: ToolCategory;\n experimental?: boolean;\n}\n\nexport type KnownToolErrorCode =\n | \"UNAUTHENTICATED\"\n | \"TOKEN_EXPIRED\"\n | \"ACCESS_DENIED\"\n | \"ORG_REQUIRED\"\n | \"ORG_NOT_FOUND\"\n | \"REPO_NOT_FOUND\"\n | \"UNKNOWN_TOOL\"\n | \"INVALID_ARGS\"\n | \"RATE_LIMITED\"\n | \"INTERNAL_ERROR\";\n\nexport type ToolErrorCode = KnownToolErrorCode | (string & {});\n\nexport interface ToolResult {\n ok: boolean;\n result?: unknown;\n error?: string;\n code?: ToolErrorCode;\n}\n\nexport interface OrgInfo {\n _id: string;\n name: string;\n slug?: string;\n repos?: Array<{\n _id: string;\n fullName: string;\n owner: string;\n name: string;\n private: boolean;\n }>;\n}\n\nconst LOGIN_EXPIRED_MESSAGE =\n \"Authentication expired. Run `fml login` to sign in again, then restart Claude Code.\";\n\nfunction isAuthError(data: { error?: string; code?: string }): boolean {\n return (\n data.code === \"UNAUTHENTICATED\" ||\n data.code === \"TOKEN_EXPIRED\" ||\n data.error?.includes(\"Unauthorized\") === true ||\n data.error?.includes(\"not authenticated\") === true\n );\n}\n\nfunction normalizeToolResult(data: ToolResult): ToolResult {\n if (!data.ok && isAuthError(data)) {\n return { ok: false, error: LOGIN_EXPIRED_MESSAGE, code: data.code };\n }\n return data;\n}\n\nfunction requireToolResult<T>(result: ToolResult): T {\n if (!result.ok) {\n throw new Error(result.error ?? \"Backend request failed\");\n }\n return result.result as T;\n}\n\n// ── API client factory ──────────────────────────────────────────────────────\n\nexport function createFmlClient(token: string) {\n const isServiceToken = token.startsWith(\"fml_st_\");\n\n /**\n * Derive the Convex site URL (HTTP actions) from the cloud URL.\n * Convex uses paired domains: *.convex.cloud for client APIs,\n * *.convex.site for HTTP actions. This is a stable Convex convention.\n */\n function getSiteUrl(): string {\n return CONVEX_URL.replace(\".convex.cloud\", \".convex.site\").replace(\n /\\/$/,\n \"\",\n );\n }\n\n /** Authed fetch to a fml-be HTTP action, parsing the JSON envelope. */\n async function fetchContext(\n pathAndQuery: string,\n init: RequestInit,\n ): Promise<ToolResult> {\n try {\n const res = await fetch(`${getSiteUrl()}${pathAndQuery}`, {\n ...init,\n headers: {\n Authorization: `Bearer ${token}`,\n ...(init.body ? { \"Content-Type\": \"application/json\" } : {}),\n },\n });\n const text = await res.text();\n let data: unknown;\n try {\n data = JSON.parse(text);\n } catch {\n return { ok: false, error: `HTTP ${res.status}: ${text.slice(0, 200)}` };\n }\n if (!res.ok) {\n const envelope = data as { error?: string; code?: ToolErrorCode };\n return normalizeToolResult({\n ok: false,\n error: envelope.error ?? `HTTP ${res.status}`,\n code: envelope.code,\n });\n }\n return { ok: true, result: data };\n } catch (err) {\n return { ok: false, error: err instanceof Error ? err.message : String(err) };\n }\n }\n\n return {\n // ── Orgs ──────────────────────────────────────────────────────────────\n\n async queryOrgs(): Promise<OrgInfo[]> {\n const result = await this.callBackend(\"list-orgs\", {});\n return requireToolResult<OrgInfo[] | null>(result) ?? [];\n },\n\n // ── Tool gateway ─────────────────────────────────────────────────────\n\n async callBackend(\n toolName: string,\n args: Record<string, unknown>,\n opts?: { org?: string },\n ): Promise<ToolResult> {\n try {\n // Unified CLI/MCP path: POST to the dual-auth HTTP endpoint for both\n // service tokens and OAuth/JWT user tokens. This keeps command handlers\n // independent of token class and matches the backend's agent tool contract.\n const body: Record<string, unknown> = { toolName, args };\n\n // Explicit org > stored org selection > repo-based inference.\n const org = opts?.org ?? getSelectedOrg();\n if (org) body.org = org;\n const repo = resolveRepoFromCwd(process.cwd());\n if (repo) body.repo = repo.repo;\n\n // Thread user identity for service-token-backed sandbox agents. For\n // JWT callers this is ignored by the backend; for service tokens the\n // backend validates membership before honoring the override.\n if (isServiceToken) {\n const { readTokens } = await import(\"./auth/token-store.js\");\n const userExternalId =\n process.env.FML_USER_EXTERNAL_ID ?? readTokens()?.user.id;\n if (userExternalId) body.userExternalId = userExternalId;\n }\n\n const res = await fetch(`${getSiteUrl()}/api/tools/execute`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify(body),\n });\n const text = await res.text();\n let data: ToolResult;\n try {\n data = JSON.parse(text) as ToolResult;\n } catch {\n return {\n ok: false,\n error: `HTTP ${res.status}: ${text.slice(0, 200)}`,\n };\n }\n if (!res.ok && !data.error) {\n return { ok: false, error: `HTTP ${res.status}` };\n }\n return normalizeToolResult(data);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes(\"Unauthorized\") || msg.includes(\"not authenticated\")) {\n return { ok: false, error: LOGIN_EXPIRED_MESSAGE };\n }\n return { ok: false, error: msg };\n }\n },\n\n // ── Tool catalog ─────────────────────────────────────────────────────\n\n async listTools(pluginVersion?: string): Promise<PublicToolDescriptor[]> {\n try {\n // Always use the HTTP catalog endpoint — it accepts both a service\n // token and a user OAuth JWT. A raw client.query rejects the user JWT\n // (no `aud` claim, which Convex's client-protocol auth requires), so\n // the httpAction path is the only one that works for both.\n const url = new URL(`${getSiteUrl()}/api/tools/list`);\n if (pluginVersion) url.searchParams.set(\"pluginVersion\", pluginVersion);\n const res = await fetch(url.toString(), {\n headers: { Authorization: `Bearer ${token}` },\n });\n const text = await res.text();\n let data: {\n ok: boolean;\n descriptors?: PublicToolDescriptor[];\n error?: string;\n code?: ToolErrorCode;\n };\n try {\n data = JSON.parse(text) as typeof data;\n } catch {\n throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);\n }\n if (!data.ok || !data.descriptors) {\n if (isAuthError(data)) {\n throw new Error(LOGIN_EXPIRED_MESSAGE);\n }\n throw new Error(data.error ?? `HTTP ${res.status}`);\n }\n return data.descriptors;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes(\"Unauthorized\") || msg.includes(\"not authenticated\")) {\n throw new Error(LOGIN_EXPIRED_MESSAGE);\n }\n throw err;\n }\n },\n\n // ── Anamnesis ground-truth context ───────────────────────────────────\n\n /**\n * Call a read-only anamnesis context endpoint\n * (/v1/anamnesis/context/{path,commit,pr}). Sends the current token\n * (service token or user JWT) — the endpoint accepts either.\n */\n async anamnesisContext(\n kind: \"path\" | \"commit\" | \"pr\",\n params: Record<string, string | number | undefined>,\n ): Promise<ToolResult> {\n const qs = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) {\n if (v != null) qs.set(k, String(v));\n }\n return fetchContext(`/v1/anamnesis/context/${kind}?${qs.toString()}`, {\n method: \"GET\",\n });\n },\n\n /** POST /v1/anamnesis/context/query — generic predicate query. */\n async anamnesisQuery(body: Record<string, unknown>): Promise<ToolResult> {\n return fetchContext(\"/v1/anamnesis/context/query\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n },\n\n // ── Repo resolution ──────────────────────────────────────────────────\n\n async resolveRepo(\n orgSlug: string,\n repoFullName: string,\n ): Promise<ResolvedRepo | null> {\n const result = await this.callBackend(\n \"resolve-repo\",\n { orgSlug, repoFullName },\n { org: orgSlug },\n );\n return requireToolResult<ResolvedRepo | null>(result);\n },\n\n // ── Config snapshots ─────────────────────────────────────────────────\n\n async listUserConfigSnapshots(\n orgSlug: string,\n ): Promise<UserConfigSnapshotSummary[]> {\n const result = await this.callBackend(\n \"list-user-config-snapshots\",\n { orgSlug },\n { org: orgSlug },\n );\n return requireToolResult<UserConfigSnapshotSummary[] | null>(result) ?? [];\n },\n\n async getUserConfigDetail(\n orgSlug: string,\n githubUsername: string,\n ): Promise<UserConfigSnapshotDetail | null> {\n const result = await this.callBackend(\n \"get-user-config-snapshot\",\n { orgSlug, githubUsername },\n { org: orgSlug },\n );\n return requireToolResult<UserConfigSnapshotDetail | null>(result);\n },\n\n async listRepoConfigSnapshots(\n orgSlug: string,\n repository?: string,\n ): Promise<RepoConfigSnapshotSummary[]> {\n const result = await this.callBackend(\n \"list-repo-config-snapshots\",\n { orgSlug, repository },\n { org: orgSlug },\n );\n return requireToolResult<RepoConfigSnapshotSummary[] | null>(result) ?? [];\n },\n\n async getRepoConfigDetail(\n orgSlug: string,\n repository: string,\n ): Promise<RepoConfigSnapshotDetail | null> {\n const result = await this.callBackend(\n \"get-repo-config-snapshot\",\n { orgSlug, repository },\n { org: orgSlug },\n );\n return requireToolResult<RepoConfigSnapshotDetail | null>(result);\n },\n };\n}\n\n// ── Convenience: auto-authenticated client ──────────────────────────────────\n\n/**\n * Create an API client using the stored auth token.\n * Returns null if not authenticated.\n */\nexport async function getAuthenticatedClient() {\n const token = await getValidToken();\n if (!token) return null;\n return createFmlClient(token);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auth/token-store.ts","../src/config.ts","../src/dirs.ts","../src/sentry.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport {\n CONVEX_URL,\n WORKOS_API_URL,\n authStorePath,\n authStorePathFor,\n resolveEnvConvexUrl,\n} from \"../config.js\";\nimport { FML_DATA_DIR } from \"../dirs.js\";\nimport { Sentry } from \"../sentry.js\";\n\ninterface StoredAuth {\n accessToken: string;\n refreshToken: string;\n expiresAt: number; // Unix timestamp in ms\n user: {\n id: string;\n email: string;\n name: string;\n };\n orgSlug?: string;\n /** OAuth client ID used to obtain these tokens (needed for refresh) */\n workosClientId?: string;\n /**\n * Token family marker for older stores.\n *\n * New human login paths (browser OAuth and device OAuth) store user\n * credentials and omit this field. `\"service\"` is retained only for\n * legacy opaque service-token-family sessions and explicit machine auth\n * compatibility.\n */\n tokenType?: \"oauth\" | \"service\";\n}\n\nfunction storePathFor(envName?: string): string {\n return envName ? authStorePathFor(envName) : authStorePath();\n}\n\nfunction ensureDir(envName?: string): void {\n const dir = path.dirname(storePathFor(envName));\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n}\n\nexport function readTokens(envName?: string): StoredAuth | null {\n try {\n const data = fs.readFileSync(storePathFor(envName), \"utf-8\");\n return JSON.parse(data) as StoredAuth;\n } catch {\n return null;\n }\n}\n\nexport function writeTokens(auth: StoredAuth, envName?: string): void {\n ensureDir(envName);\n fs.writeFileSync(storePathFor(envName), JSON.stringify(auth, null, 2), {\n mode: 0o600,\n });\n}\n\nexport function getSelectedOrg(): string | null {\n return readTokens()?.orgSlug ?? null;\n}\n\nexport function setSelectedOrg(orgSlug: string): void {\n const stored = readTokens();\n if (!stored) return;\n writeTokens({ ...stored, orgSlug });\n}\n\n// ── Service Token Refresh (sandbox fml_srt_* → fml_st_*) ────────────────────\n\ninterface ServiceTokenCache {\n accessToken: string;\n expiresAt: number;\n}\n\n/**\n * Caches and refresh-deduplication promises are keyed by env so in-process\n * callers that read multiple envs (`getValidToken({ env: \"a\" })` followed by\n * `getValidToken({ env: \"b\" })`) don't cross-contaminate. The sentinel\n * \"__active__\" stands in for calls with no explicit env.\n */\nconst ACTIVE_KEY = \"__active__\";\nconst serviceTokenCaches = new Map<string, ServiceTokenCache>();\nconst serviceRefreshPromises = new Map<string, Promise<string | null>>();\nconst oauthRefreshPromises = new Map<string, Promise<string | null>>();\nconst cacheKey = (envName?: string): string => envName ?? ACTIVE_KEY;\n\n/** File where the current access token is written for panopticon's tokenCommand */\nconst SERVICE_ACCESS_TOKEN_PATH = path.join(FML_DATA_DIR, \"access_token\");\n\nfunction getSiteUrl(envName?: string): string {\n const explicit = process.env.CONVEX_SITE_URL;\n if (explicit) return explicit.replace(/\\/$/, \"\");\n if (envName) {\n const envConvexUrl = resolveEnvConvexUrl(envName);\n if (envConvexUrl) {\n return envConvexUrl.replace(\".convex.cloud\", \".convex.site\");\n }\n }\n return CONVEX_URL.replace(\".convex.cloud\", \".convex.site\");\n}\n\nasync function refreshServiceToken(\n refreshToken: string,\n envName?: string,\n): Promise<string | null> {\n try {\n const response = await fetch(`${getSiteUrl(envName)}/api/tokens/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${refreshToken}`,\n },\n });\n\n if (!response.ok) {\n console.error(\n `[fml] Service token refresh failed: HTTP ${response.status}`,\n );\n return null;\n }\n\n const data = (await response.json()) as {\n ok: boolean;\n accessToken: string;\n expiresAt: number;\n error?: string;\n };\n\n if (!data.ok || !data.accessToken) {\n console.error(`[fml] Service token refresh error: ${data.error}`);\n return null;\n }\n\n serviceTokenCaches.set(cacheKey(envName), {\n accessToken: data.accessToken,\n expiresAt: data.expiresAt,\n });\n\n // Write to file for panopticon's tokenCommand\n try {\n const dir = path.dirname(SERVICE_ACCESS_TOKEN_PATH);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n fs.writeFileSync(SERVICE_ACCESS_TOKEN_PATH, data.accessToken, {\n mode: 0o600,\n });\n } catch {\n // Non-fatal — in-memory cache still works for the plugin itself\n }\n\n return data.accessToken;\n } catch (err: unknown) {\n Sentry.captureException(err);\n console.error(`[fml] Service token refresh error: ${err}`);\n return null;\n }\n}\n\n// ── Token Resolution ────────────────────────────────────────────────────────\n\n/**\n * Get a valid access token, refreshing if expired.\n *\n * Three paths:\n * 1. FML_TOKEN=fml_srt_* → exchange refresh token for short-lived access token\n * 2. FML_TOKEN=fml_st_* → use directly (legacy/CI)\n * 3. No FML_TOKEN → OAuth flow (interactive user)\n */\nexport async function getValidToken(opts?: {\n env?: string;\n}): Promise<string | null> {\n const envName = opts?.env;\n const key = cacheKey(envName);\n const envToken = process.env.FML_TOKEN;\n\n // Service refresh token → exchange for short-lived access token\n if (envToken?.startsWith(\"fml_srt_\")) {\n const cached = serviceTokenCaches.get(key);\n if (cached && cached.expiresAt > Date.now() + 60_000) {\n return cached.accessToken;\n }\n\n const inflight = serviceRefreshPromises.get(key);\n if (inflight) return inflight;\n const promise = refreshServiceToken(envToken, envName);\n serviceRefreshPromises.set(key, promise);\n try {\n return await promise;\n } finally {\n serviceRefreshPromises.delete(key);\n }\n }\n\n // Static service access token.\n if (envToken?.startsWith(\"fml_st_\")) return envToken;\n\n if (envToken) {\n console.error(\n \"[fml] Auth: unsupported FML_TOKEN format. Expected fml_srt_* or fml_st_*.\",\n );\n return null;\n }\n\n // Stored token path (OAuth user credentials or legacy service-family credentials)\n const stored = readTokens(envName);\n if (!stored) {\n console.error(\n envName\n ? `[fml] Auth: no stored tokens for env \"${envName}\"`\n : \"[fml] Auth: no stored tokens\",\n );\n return null;\n }\n\n // If token expires in more than 60 seconds, use it\n if (stored.expiresAt > Date.now() + 60_000) {\n return stored.accessToken;\n }\n\n // Legacy stored service-family credentials refresh via /api/tokens/refresh.\n // Current device OAuth login no longer writes tokenType: \"service\".\n if (stored.tokenType === \"service\") {\n const inflight = serviceRefreshPromises.get(key);\n if (inflight) return inflight;\n const promise = (async () => {\n const newAccessToken = await refreshServiceToken(\n stored.refreshToken,\n envName,\n );\n const cached = serviceTokenCaches.get(key);\n if (newAccessToken && cached) {\n writeTokens(\n {\n ...stored,\n accessToken: newAccessToken,\n expiresAt: cached.expiresAt,\n },\n envName,\n );\n }\n return newAccessToken;\n })();\n serviceRefreshPromises.set(key, promise);\n try {\n return await promise;\n } finally {\n serviceRefreshPromises.delete(key);\n }\n }\n\n // OAuth user tokens (browser or device): refresh via OAuth provider\n const inflight = oauthRefreshPromises.get(key);\n if (inflight) return inflight;\n const promise = refreshToken(stored, envName);\n oauthRefreshPromises.set(key, promise);\n try {\n return await promise;\n } finally {\n oauthRefreshPromises.delete(key);\n }\n}\n\nasync function refreshToken(\n stored: StoredAuth,\n envName?: string,\n): Promise<string | null> {\n if (!stored.workosClientId) {\n console.error(\n \"[fml] Auth: token expired, missing workosClientId — run `fml login`\",\n );\n return null;\n }\n\n try {\n const response = await fetch(\n `${WORKOS_API_URL}/user_management/authenticate`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n grant_type: \"refresh_token\",\n client_id: stored.workosClientId,\n refresh_token: stored.refreshToken,\n }),\n },\n );\n\n if (!response.ok) {\n console.error(\n `[fml] Auth: refresh failed (HTTP ${response.status}) — token preserved`,\n );\n return null;\n }\n\n const data = (await response.json()) as {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n user: {\n id: string;\n email: string;\n first_name?: string;\n last_name?: string;\n };\n };\n\n const userName = [data.user.first_name, data.user.last_name]\n .filter(Boolean)\n .join(\" \");\n\n const refreshed: StoredAuth = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt: Date.now() + (data.expires_in ?? 300) * 1000,\n user: {\n id: data.user.id,\n email: data.user.email,\n name: userName || stored.user.name,\n },\n orgSlug: stored.orgSlug,\n workosClientId: stored.workosClientId,\n };\n\n writeTokens(refreshed, envName);\n return refreshed.accessToken;\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(\n `[fml] Auth: refresh failed (network error: ${msg}) — token preserved`,\n );\n Sentry.captureException(err);\n return null;\n }\n}\n","import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { FML_DATA_DIR } from \"./dirs.js\";\n\n// ── Default production deployment ───────────────────────────────────────────\n\nexport const DEFAULT_PROD_URL =\n \"https://trustworthy-chihuahua-382.convex.cloud\";\n\n/** Convex site URL for sync/HTTP actions — derived from DEFAULT_PROD_URL. */\nexport const DEFAULT_SYNC_URL = DEFAULT_PROD_URL.replace(\n \".convex.cloud\",\n \".convex.site\",\n);\n\n/** Default sync target name created by `fml install` */\nexport const DEFAULT_TARGET_NAME = \"fml\";\n\n// ── Persistent env selection ────────────────────────────────────────────────\n\ninterface EnvConfig {\n /** Name of the active sync target */\n active: string;\n}\n\nconst ENV_CONFIG_PATH = path.join(FML_DATA_DIR, \"env.json\");\n\nfunction readEnvConfig(): EnvConfig {\n try {\n const raw = fs.readFileSync(ENV_CONFIG_PATH, \"utf-8\");\n return JSON.parse(raw) as EnvConfig;\n } catch {\n return { active: DEFAULT_TARGET_NAME };\n }\n}\n\nexport function writeEnvConfig(config: EnvConfig): void {\n const dir = path.dirname(ENV_CONFIG_PATH);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n fs.writeFileSync(ENV_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\\n`, {\n mode: 0o600,\n });\n}\n\n/** Whether an env selection has already been persisted (not a fresh install). */\nexport function envConfigExists(): boolean {\n return fs.existsSync(ENV_CONFIG_PATH);\n}\n\n/**\n * Look up a panopticon sync target by name and return its Convex deployment\n * URL (`.convex.cloud`). Returns null if panopticon isn't installed or the\n * target isn't configured.\n */\nexport function resolveEnvConvexUrl(envName: string): string | null {\n try {\n const panoDataDir =\n process.env.PANOPTICON_DATA_DIR ??\n (process.platform === \"darwin\"\n ? path.join(\n os.homedir(),\n \"Library\",\n \"Application Support\",\n \"panopticon\",\n )\n : process.platform === \"win32\"\n ? path.join(\n process.env.APPDATA ??\n path.join(os.homedir(), \"AppData\", \"Roaming\"),\n \"panopticon\",\n )\n : path.join(os.homedir(), \".local\", \"share\", \"panopticon\"));\n const raw = fs.readFileSync(path.join(panoDataDir, \"config.json\"), \"utf-8\");\n const panoConf = JSON.parse(raw) as {\n sync?: { targets?: Array<{ name: string; url: string }> };\n };\n const target = panoConf.sync?.targets?.find((t) => t.name === envName);\n if (target) {\n return target.url.replace(\".convex.site\", \".convex.cloud\");\n }\n } catch {\n // Panopticon config not available\n }\n return null;\n}\n\n/**\n * Resolve the active environment by looking up the sync target.\n * Derives the .convex.cloud URL from the target's .convex.site URL.\n * Returns null convexUrl if the target is not found (panopticon not installed).\n */\nexport function getActiveEnv(): { name: string; convexUrl: string | null } {\n const name = readEnvConfig().active;\n return { name, convexUrl: resolveEnvConvexUrl(name) };\n}\n\n/** Auth store path for a specific env name (tokens are segregated per env). */\nexport function authStorePathFor(envName: string): string {\n return path.join(FML_DATA_DIR, `auth.${envName}.json`);\n}\n\n/**\n * Env names flow into `tokenCommand` strings that panopticon shells out, so\n * refuse anything that could carry shell metacharacters. Target names are\n * also written to panopticon's config by this rule (see `fml env use`).\n */\nconst ENV_NAME_RE = /^[A-Za-z0-9_-]+$/;\nexport function isValidEnvName(envName: string): boolean {\n return ENV_NAME_RE.test(envName);\n}\n\n/**\n * Require a resolved Convex URL. Exits with an error if the sync target\n * is not configured (panopticon not installed).\n */\nexport function requireConvexUrl(): string {\n const { name, convexUrl } = getActiveEnv();\n if (!convexUrl) {\n console.error(\n `Sync target \"${name}\" not found. Run \\`fml install\\` or \\`fml sync setup\\`.`,\n );\n process.exit(1);\n }\n return convexUrl;\n}\n\n// ── Exports ─────────────────────────────────────────────────────────────────\n\n/**\n * Convex deployment URL (switches with `fml env`).\n *\n * Resolution order:\n * 1. `FML_CONVEX_URL` env var (dev/preview overrides)\n * 2. Active panopticon sync target\n * 3. `DEFAULT_PROD_URL` — lets a fresh `npm install -g` log in against\n * prod before `fml install` has had a chance to seed a sync target.\n */\nexport const CONVEX_URL: string = (\n process.env.FML_CONVEX_URL ??\n getActiveEnv().convexUrl ??\n DEFAULT_PROD_URL\n).replace(/\\/$/, \"\");\n\n/** OAuth provider API base URL */\nexport const WORKOS_API_URL = \"https://api.workos.com\";\n\n/** OAuth authorization base URL */\nexport const WORKOS_AUTH_URL = \"https://auth.fml.inc\";\n\n/**\n * Path to the auth token store for the *currently* active env. Resolved\n * lazily so long-running processes that outlive an `fml env use` switch\n * still read the correct file on the next call.\n */\nexport function authStorePath(): string {\n return authStorePathFor(getActiveEnv().name);\n}\n\n/** Convex site URL (HTTP actions) — derived from CONVEX_URL */\nexport function getSiteUrl(): string {\n return CONVEX_URL.replace(\".convex.cloud\", \".convex.site\").replace(\n /:\\d+$/,\n \"\",\n );\n}\n","/**\n * Platform-native directories for FML runtime data.\n *\n * macOS: ~/Library/Application Support/fml + ~/Library/Logs/fml\n * Linux: ~/.local/share/fml + ~/.local/state/fml/log\n * Windows: %APPDATA%/fml + %APPDATA%/fml/logs\n */\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nconst home = os.homedir();\n\nfunction dataDir(): string {\n // Override (tests, eval replays, sandboxes) so they never touch a real\n // user's auth/config in the platform-default location.\n if (process.env.FML_DATA_DIR) return process.env.FML_DATA_DIR;\n switch (process.platform) {\n case \"darwin\":\n return path.join(home, \"Library\", \"Application Support\", \"fml\");\n case \"win32\":\n return path.join(\n process.env.APPDATA ?? path.join(home, \"AppData\", \"Roaming\"),\n \"fml\",\n );\n default:\n return path.join(\n process.env.XDG_DATA_HOME ?? path.join(home, \".local\", \"share\"),\n \"fml\",\n );\n }\n}\n\nfunction logDir(): string {\n if (process.env.FML_LOG_DIR) return process.env.FML_LOG_DIR;\n switch (process.platform) {\n case \"darwin\":\n return path.join(home, \"Library\", \"Logs\", \"fml\");\n case \"win32\":\n return path.join(\n process.env.APPDATA ?? path.join(home, \"AppData\", \"Roaming\"),\n \"fml\",\n \"logs\",\n );\n default:\n return path.join(\n process.env.XDG_STATE_HOME ?? path.join(home, \".local\", \"state\"),\n \"fml\",\n \"log\",\n );\n }\n}\n\n/** Runtime data — auth tokens, env config, PID files */\nexport const FML_DATA_DIR = dataDir();\n\n/** Log files — daemon logs, MCP logs */\nexport const FML_LOG_DIR = logDir();\n\n/** Ensure both directories exist (called once at startup). */\nexport function ensureDirs(): void {\n fs.mkdirSync(FML_DATA_DIR, { recursive: true, mode: 0o700 });\n fs.mkdirSync(FML_LOG_DIR, { recursive: true, mode: 0o700 });\n}\n","declare const __SENTRY_DSN__: string;\n\nlet initialized = false;\nlet sentry: typeof import(\"@sentry/core\") | null = null;\n\nasync function loadSentry(): Promise<typeof import(\"@sentry/core\") | null> {\n if (sentry) return sentry;\n try {\n sentry = await import(\"@sentry/core\");\n return sentry;\n } catch {\n return null;\n }\n}\n\nexport async function initSentry(): Promise<void> {\n if (initialized) return;\n initialized = true;\n\n const mod = await loadSentry();\n if (!mod) return;\n\n const client = new mod.ServerRuntimeClient({\n dsn: __SENTRY_DSN__,\n tracesSampleRate: 0,\n integrations: [],\n stackParser: mod.createStackParser(mod.nodeStackLineParser()),\n transport: (options) =>\n mod.createTransport(options, async (request) => {\n const res = await fetch(options.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-sentry-envelope\" },\n body: request.body as BodyInit,\n });\n return { statusCode: res.status };\n }),\n });\n\n mod.setCurrentClient(client);\n client.init();\n}\n\nexport const Sentry = {\n captureException: (err: unknown) => sentry?.captureException(err),\n captureMessage: (msg: string) => sentry?.captureMessage(msg),\n} as const;\n"],"mappings":"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","names":["fs","path","fs","os","path","path","fs","os","path","fs","getSiteUrl","refreshToken","inflight","promise"]}
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
createFmlClient,
|
|
4
4
|
getAuthenticatedClient
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-2H6D7RUN.js";
|
|
6
6
|
import {
|
|
7
7
|
CONVEX_URL,
|
|
8
8
|
DEFAULT_SYNC_URL,
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
setSelectedOrg,
|
|
25
25
|
writeEnvConfig,
|
|
26
26
|
writeTokens
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-VDW5L7OB.js";
|
|
28
28
|
import {
|
|
29
29
|
handleStart,
|
|
30
30
|
handleStop,
|
|
@@ -705,7 +705,7 @@ function handleUninstall(opts) {
|
|
|
705
705
|
// src/commands/update.ts
|
|
706
706
|
import { execSync as execSync2 } from "child_process";
|
|
707
707
|
async function handleUpdate() {
|
|
708
|
-
const currentVersion = true ? "0.
|
|
708
|
+
const currentVersion = true ? "0.6.1+569d0d2" : "unknown";
|
|
709
709
|
console.log(`Current: ${currentVersion}`);
|
|
710
710
|
console.log("Updating via npm...\n");
|
|
711
711
|
try {
|
|
@@ -800,7 +800,7 @@ async function login() {
|
|
|
800
800
|
throw new Error(`Token exchange failed: ${error}`);
|
|
801
801
|
}
|
|
802
802
|
const data = await tokenResponse.json();
|
|
803
|
-
const
|
|
803
|
+
const userName2 = [data.user.first_name, data.user.last_name].filter(Boolean).join(" ");
|
|
804
804
|
writeTokens({
|
|
805
805
|
accessToken: data.access_token,
|
|
806
806
|
refreshToken: data.refresh_token,
|
|
@@ -808,11 +808,11 @@ async function login() {
|
|
|
808
808
|
user: {
|
|
809
809
|
id: data.user.id,
|
|
810
810
|
email: data.user.email,
|
|
811
|
-
name:
|
|
811
|
+
name: userName2
|
|
812
812
|
},
|
|
813
813
|
workosClientId: clientId
|
|
814
814
|
});
|
|
815
|
-
return { email: data.user.email, name:
|
|
815
|
+
return { email: data.user.email, name: userName2 };
|
|
816
816
|
}
|
|
817
817
|
function startCallbackServer(expectedState) {
|
|
818
818
|
return new Promise((resolve, reject) => {
|
|
@@ -883,90 +883,109 @@ function startCallbackServer(expectedState) {
|
|
|
883
883
|
}
|
|
884
884
|
|
|
885
885
|
// src/auth/device-flow.ts
|
|
886
|
-
import { createInterface } from "readline";
|
|
887
886
|
function sleep(ms) {
|
|
888
887
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
889
888
|
}
|
|
890
|
-
function
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
}
|
|
896
|
-
return
|
|
897
|
-
rl.question(question, (answer) => {
|
|
898
|
-
rl.close();
|
|
899
|
-
resolve(answer.trim());
|
|
900
|
-
});
|
|
901
|
-
});
|
|
889
|
+
async function fetchWorkosClientId2() {
|
|
890
|
+
const response = await fetch(`${getSiteUrl()}/api/auth/config`);
|
|
891
|
+
const data = await response.json();
|
|
892
|
+
if (!response.ok || data.ok === false || !data.workosClientId) {
|
|
893
|
+
throw new Error(data.error ?? `HTTP ${response.status}`);
|
|
894
|
+
}
|
|
895
|
+
return data.workosClientId;
|
|
902
896
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
897
|
+
function userName(user) {
|
|
898
|
+
return [user.first_name, user.last_name].filter(Boolean).join(" ");
|
|
899
|
+
}
|
|
900
|
+
async function requestDeviceAuthorization(clientId) {
|
|
901
|
+
const response = await fetch(
|
|
902
|
+
`${WORKOS_API_URL}/user_management/authorize/device`,
|
|
903
|
+
{
|
|
904
|
+
method: "POST",
|
|
905
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
906
|
+
body: new URLSearchParams({ client_id: clientId })
|
|
907
|
+
}
|
|
908
|
+
);
|
|
909
|
+
const data = await response.json();
|
|
910
|
+
if (!response.ok || !data.device_code || !data.user_code || !data.verification_uri) {
|
|
911
|
+
throw new Error(
|
|
912
|
+
data.error_description ?? data.error ?? `HTTP ${response.status}`
|
|
913
|
+
);
|
|
912
914
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
if (!confirmationCode) {
|
|
921
|
-
throw new Error("No confirmation code entered.");
|
|
922
|
-
}
|
|
923
|
-
const normalizedCode = confirmationCode.toUpperCase().replace(/[\s-]/g, "").replace(/^(.{4})/, "$1-");
|
|
924
|
-
console.log(" Verifying...");
|
|
925
|
-
let pollInterval = codeData.interval * 1e3;
|
|
926
|
-
while (Date.now() < codeData.expiresAt) {
|
|
927
|
-
const pollRes = await fetch(`${siteUrl}/api/auth/device/poll`, {
|
|
915
|
+
return data;
|
|
916
|
+
}
|
|
917
|
+
async function pollForTokens(clientId, authorization) {
|
|
918
|
+
let intervalMs = (authorization.interval ?? 5) * 1e3;
|
|
919
|
+
const deadline = Date.now() + (authorization.expires_in ?? 300) * 1e3;
|
|
920
|
+
while (Date.now() < deadline) {
|
|
921
|
+
const response = await fetch(`${WORKOS_API_URL}/user_management/authenticate`, {
|
|
928
922
|
method: "POST",
|
|
929
|
-
headers: { "Content-Type": "application/
|
|
930
|
-
body:
|
|
931
|
-
|
|
932
|
-
|
|
923
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
924
|
+
body: new URLSearchParams({
|
|
925
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
926
|
+
device_code: authorization.device_code,
|
|
927
|
+
client_id: clientId
|
|
933
928
|
})
|
|
934
929
|
});
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
930
|
+
const data = await response.json();
|
|
931
|
+
if (response.ok) {
|
|
932
|
+
const success = data;
|
|
933
|
+
if (!success.access_token || !success.refresh_token || !success.user) {
|
|
934
|
+
throw new Error("Device authorization returned an incomplete token response.");
|
|
935
|
+
}
|
|
936
|
+
return success;
|
|
940
937
|
}
|
|
941
|
-
const
|
|
942
|
-
if (
|
|
943
|
-
await sleep(
|
|
938
|
+
const error = data.error;
|
|
939
|
+
if (error === "authorization_pending") {
|
|
940
|
+
await sleep(intervalMs);
|
|
944
941
|
continue;
|
|
945
942
|
}
|
|
946
|
-
if (
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
expiresAt: data.expiresAt,
|
|
951
|
-
user: data.user,
|
|
952
|
-
orgSlug: data.orgSlug ?? void 0,
|
|
953
|
-
tokenType: "service"
|
|
954
|
-
});
|
|
955
|
-
return { email: data.user.email, name: data.user.name };
|
|
943
|
+
if (error === "slow_down") {
|
|
944
|
+
intervalMs += 5e3;
|
|
945
|
+
await sleep(intervalMs);
|
|
946
|
+
continue;
|
|
956
947
|
}
|
|
957
|
-
if (
|
|
958
|
-
throw new Error(
|
|
959
|
-
"Invalid confirmation code. Make sure you copied the code from the browser correctly."
|
|
960
|
-
);
|
|
948
|
+
if (error === "access_denied") {
|
|
949
|
+
throw new Error("Device authorization was denied.");
|
|
961
950
|
}
|
|
962
|
-
if (
|
|
951
|
+
if (error === "expired_token") {
|
|
963
952
|
throw new Error("Device authorization expired. Run `fml login` again.");
|
|
964
953
|
}
|
|
965
|
-
throw new Error(
|
|
954
|
+
throw new Error(
|
|
955
|
+
data.error_description ?? error ?? `HTTP ${response.status}`
|
|
956
|
+
);
|
|
966
957
|
}
|
|
967
|
-
throw new Error(
|
|
968
|
-
|
|
958
|
+
throw new Error("Device authorization timed out. Run `fml login` again.");
|
|
959
|
+
}
|
|
960
|
+
async function deviceLogin() {
|
|
961
|
+
const clientId = await fetchWorkosClientId2();
|
|
962
|
+
const authorization = await requestDeviceAuthorization(clientId);
|
|
963
|
+
console.log("");
|
|
964
|
+
console.log(" Sign in with this code:");
|
|
965
|
+
console.log("");
|
|
966
|
+
console.log(` ${authorization.user_code}`);
|
|
967
|
+
console.log("");
|
|
968
|
+
console.log(" Open this URL in your browser:");
|
|
969
|
+
console.log("");
|
|
970
|
+
console.log(
|
|
971
|
+
` ${authorization.verification_uri_complete ?? authorization.verification_uri}`
|
|
969
972
|
);
|
|
973
|
+
console.log("");
|
|
974
|
+
console.log(" Waiting for authorization...");
|
|
975
|
+
const data = await pollForTokens(clientId, authorization);
|
|
976
|
+
const name = userName(data.user);
|
|
977
|
+
writeTokens({
|
|
978
|
+
accessToken: data.access_token,
|
|
979
|
+
refreshToken: data.refresh_token,
|
|
980
|
+
expiresAt: Date.now() + (data.expires_in ?? 300) * 1e3,
|
|
981
|
+
user: {
|
|
982
|
+
id: data.user.id,
|
|
983
|
+
email: data.user.email,
|
|
984
|
+
name
|
|
985
|
+
},
|
|
986
|
+
workosClientId: clientId
|
|
987
|
+
});
|
|
988
|
+
return { email: data.user.email, name };
|
|
970
989
|
}
|
|
971
990
|
|
|
972
991
|
// src/commands/login.ts
|
|
@@ -977,19 +996,6 @@ async function linkGitHubIdentity() {
|
|
|
977
996
|
return;
|
|
978
997
|
}
|
|
979
998
|
try {
|
|
980
|
-
const response = await fetch("https://api.github.com/user", {
|
|
981
|
-
headers: {
|
|
982
|
-
Authorization: `Bearer ${token}`,
|
|
983
|
-
Accept: "application/vnd.github+json"
|
|
984
|
-
}
|
|
985
|
-
});
|
|
986
|
-
if (!response.ok) {
|
|
987
|
-
console.warn(
|
|
988
|
-
`[fml] GitHub API returned ${response.status} \u2014 skipping identity link`
|
|
989
|
-
);
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
const user = await response.json();
|
|
993
999
|
const fmlToken = await getValidToken();
|
|
994
1000
|
if (!fmlToken) {
|
|
995
1001
|
console.warn("[fml] No FML token available \u2014 skipping identity link");
|
|
@@ -1004,17 +1010,13 @@ async function linkGitHubIdentity() {
|
|
|
1004
1010
|
"Content-Type": "application/json",
|
|
1005
1011
|
Authorization: `Bearer ${fmlToken}`
|
|
1006
1012
|
},
|
|
1007
|
-
body: JSON.stringify({
|
|
1008
|
-
githubUsername: user.login,
|
|
1009
|
-
githubId: user.id,
|
|
1010
|
-
githubEmail: user.email ?? void 0
|
|
1011
|
-
})
|
|
1013
|
+
body: JSON.stringify({ githubAccessToken: token })
|
|
1012
1014
|
});
|
|
1013
1015
|
const linkResult = await linkResponse.json();
|
|
1014
1016
|
if (!linkResponse.ok || linkResult.ok === false) {
|
|
1015
1017
|
throw new Error(linkResult.error ?? `HTTP ${linkResponse.status}`);
|
|
1016
1018
|
}
|
|
1017
|
-
console.log(
|
|
1019
|
+
console.log("Linked GitHub account");
|
|
1018
1020
|
} catch (err) {
|
|
1019
1021
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1020
1022
|
console.warn(`[fml] Failed to link GitHub identity: ${msg}`);
|
|
@@ -1075,7 +1077,7 @@ async function handleLogin(opts) {
|
|
|
1075
1077
|
const { name: envName } = getActiveEnv();
|
|
1076
1078
|
const existingToken = await getValidToken();
|
|
1077
1079
|
if (existingToken) {
|
|
1078
|
-
const { readTokens: readTokens2 } = await import("./token-store-
|
|
1080
|
+
const { readTokens: readTokens2 } = await import("./token-store-JZJ3FJHA.js");
|
|
1079
1081
|
const stored = readTokens2();
|
|
1080
1082
|
console.log(
|
|
1081
1083
|
`Already logged in as ${stored?.user.name} (${stored?.user.email}) on ${envName}.`
|
|
@@ -1959,7 +1961,7 @@ async function handleConfigDetail(opts) {
|
|
|
1959
1961
|
// src/commands/tools.ts
|
|
1960
1962
|
import { readFileSync } from "fs";
|
|
1961
1963
|
import path3 from "path";
|
|
1962
|
-
var pluginVersion = true ? "0.
|
|
1964
|
+
var pluginVersion = true ? "0.6.1+569d0d2" : void 0;
|
|
1963
1965
|
async function getApi() {
|
|
1964
1966
|
const api = await getAuthenticatedClient();
|
|
1965
1967
|
if (!api) {
|
|
@@ -2090,7 +2092,7 @@ if (process.argv.length <= 2 || process.argv.includes("--help") || process.argv.
|
|
|
2090
2092
|
printBanner();
|
|
2091
2093
|
}
|
|
2092
2094
|
var program = new Command().name("fml").description("FML agent tools for Claude Code").version(
|
|
2093
|
-
true ? "0.
|
|
2095
|
+
true ? "0.6.1+569d0d2" : "dev"
|
|
2094
2096
|
);
|
|
2095
2097
|
var tools = program.command("tools", { hidden: true }).description("List all available CLI commands (including hidden agent tools)").action(() => {
|
|
2096
2098
|
function formatCmd(cmd, prefix = "") {
|
|
@@ -2133,6 +2135,8 @@ program.command("env").description("Show or switch environment (dev/prod)").argu
|
|
|
2133
2135
|
if (target) handleEnvSwitch(target);
|
|
2134
2136
|
else handleEnvShow();
|
|
2135
2137
|
});
|
|
2138
|
+
program.command("start").description("Start the panopticon server").action(() => handleStart());
|
|
2139
|
+
program.command("stop").description("Stop the panopticon server").action(() => handleStop());
|
|
2136
2140
|
var panopticon = program.command("panopticon").description("Start or stop the panopticon server");
|
|
2137
2141
|
panopticon.command("start").description("Start the panopticon server").action(() => handleStart());
|
|
2138
2142
|
panopticon.command("stop").description("Stop the panopticon server").action(() => handleStop());
|